Constants in a bot scenario: complete guide
For anyone who builds logic from blocks: what a constant is, how it enters the scenario, how to avoid mistakes in expression-enabled fields, and the order in which the engine parses loops, conditions, and plain curly-brace substitutions.
isExp. The exact list of constants at startup may differ by product—check your editor’s hints and environment docs.
Why constants exist
A scenario is a chain of steps: send text, read a table row, check a condition, branch elsewhere. So the next step is not guessing in a vacuum but using facts from earlier steps—all intermediate values live in one shared data set.
Here a constant is not the “immutable value from a C++ textbook”; it is a named slot in that set: text, number, yes/no, list, or structured fields. In a template you refer to it by name in curly braces and inject the client’s name, a found row, a condition flag, and so on.
Where values come from and how they flow
Startup and the Update event
The constant set is usually built on an Update event: the user wrote to the bot, pressed a button, a system event arrived, and so on. The “starter” bundle includes at least bot and user metadata, and the current date and time in the bot’s time zone. If downstream steps use external services, fields you explicitly request or pass through may also appear there.
Each block in the chain
The flow is simple and predictable:
- the block receives the current constant set;
- it performs its work;
- it often appends or overwrites values;
- it passes the set to the next block.
If a name already existed and you write it again, the last write wins: you always see the freshest result from the last step that touched that key.
Fields with expression support (isExp)
Not every editor field allows templates. When expression support is enabled for a parameter (often labelled “expression” or “template”;
in technical docs isExp), the string may contain substitutions like {PREFIX.TYPE.NAME} and special blocks such as {#IF}.
When it is off, the text is usually executed as typed, without substitutions. Before pasting a long {MAIN.STRING…}
or an {#IF} block, check the field’s mode toggle—otherwise braces may reach the end user verbatim.
How a constant name looks
A key has three segments separated by two dots (exactly two in the usual scheme):
{ComponentPrefix.Type.Name}
The middle segment is the data type. Common values:
STRING— plain text;ARRAY— list or object with fields (handy for table rows, catalogues);BOOL— yes/no, including the result of a condition step.
Illustrative examples (names may differ in your product):
# STRING
{MAIN.STRING.USER_TELEGRAM_ID}
# ARRAY — table row, list, or object
{DATA.ARRAY.CURRENT_ROW}
# BOOL — condition result (ID in the name)
{MAIN.BOOL.CONDITION_12_RESULT}
Editor hints that insert a ready-made key into a field follow the same idea: prefix, type, name.
Working with ARRAY
Printing the whole value
If you use only the name without indexes or paths, the output is usually the raw form—often one long JSON string. Handy for debugging; less often for customer-facing copy.
Suppose {DATA.ARRAY.USER} holds:
[
{
"name": "Anna",
"id": 10
},
{
"name": "Boris",
"id": 20
}
]
Template without indexes:
Data: {DATA.ARRAY.USER}
The message will contain one long JSON string for the whole array—useful to verify structure while debugging.
Accessing a field or element
After the constant name, add steps in square brackets: element index or field name.
# index 0, field name (quoted)
{DATA.ARRAY.USER}[0]['name']
→ Anna
# index 1, field NAME (field name case is often ignored)
{DATA.ARRAY.USER}[1][NAME]
→ Boris
If the path is wrong (missing index or key), the fragment may stay exactly as you typed—a cue to check both the data and the path.
Nesting
For nested objects and lists, brackets alternate by level, for example:
{DATA.ARRAY.ORDER}['items'][0]['title']
Meaning: order → line items → first line → title.
Processing order in expression-enabled fields
When expressions are enabled, parsing follows a fixed order:
- Loops — repeat a fragment for each array element.
- Conditions — if / else branching.
- Date and time — shift from a point on the timeline.
- Math — evaluate a formula.
- Plain substitutions for every
{…}and array path.
In practice you cannot assume constants substitute everywhere first and only then conditions run. Loops and conditions expand first (respecting nesting), then “simple” braces.
1. {#EACH} … {/EACH} — array loops
2. {#IF} … {#ELSE} … {/IF} — conditions
3. {#DATETIME; … ; …} — date and time
4. {#MATH} … {/MATH} — formulas
5. {PREFIX.TYPE.NAME} + paths — plain substitutions and [...]
Special constructs: data → template → result
7.1. Loop {#EACH} … {/EACH}
Suppose {DEMO.ARRAY.PRODUCTS} contains:
[
{
"title": "Apple",
"price": 50
},
{
"title": "Pear",
"price": 70
}
]
Loop template (line breaks for readability; a single line in the field is also fine):
Catalog:
{#EACH {DEMO.ARRAY.PRODUCTS} @N as P}
{P}['title'] — {P}['price'] RUB;
{/EACH}
Catalog: Apple — 50 RUB.; Pear — 70 RUB.;
The line above is the intended output. The loop header names the array, the row counter (@N),
and the current element (P); the body uses {@N}, {P}['field'], and other constants as needed.
If the key is not an array or the loop markup is wrong, the text may stay unchanged or the loop may not run.
7.2. Condition {#IF} … {#ELSE} … {/IF}
Same two-item array as above. Template:
{#IF LEN({DEMO.ARRAY.PRODUCTS}) > 0}
The catalogue has items
{#ELSE}
Catalogue is empty
{/IF}
The catalogue has items
Catalogue is empty
What is allowed in a {#IF} expression
The condition text—from {#IF} to the branch boundary ({#ELSE} or {/IF})—is parsed as one expression.
Below are typical grammar pieces; the exact set can depend on the editor version, but these match how the parser usually expects tokens literally, without hidden synonyms.
| Category | Elements |
|---|---|
| Comparisons | ==, !=, >, <, >=, <= |
| Logic | AND, OR — uppercase only, as in the parser. There must be no letters or digits immediately left or right of the token; otherwise a substring like SCORE or ORDER is not treated as a separator and breaks parsing. |
| Parentheses | ( ) — grouping and nesting of sub-expressions. |
| Literals | true and false; numbers — integers and with a decimal point; strings in single '…' or double "…" quotes. Inside a string, \' and \" escapes are allowed. |
| Constant references | Key {COMPONENT.TYPE.NAME} with optional path […]. Before the condition is evaluated, such inserts become literals: quoted string, number, true/false. If there is no value, an empty string '' is substituted. |
| Functions | LEN({…}[path]) — for an array returns the array length, otherwise the length of the scalar’s string form. EMPTY({…}[path]) is true when the value is null, empty string, or empty array []. These calls are parsed before paths inside the function argument are substituted. |
In practice: write AND/OR in capitals and separate them with spaces from field names and constants; watch quotes and escaping in strings.
For debugging a complex condition, temporarily print intermediate pieces in another field or simplify with parentheses until literals and references substitute as you expect.
7.3. Date and time {#DATETIME}
The block expects a correctly filled instant structure: date, time, zone, timestamp — in the shape your constant provides.
The idea: take a moment, e.g. from {MAIN.ARRAY.EVENT}['start'], add an offset (e.g. two days), and get one result string.
Missing data or wrong format — instead of a date you often see a bracketed diagnostic message, which makes drafts and debugging easier to spot.
{#DATETIME;
…source parameters (constant holding the instant)… ;
…offset and output format…
}
Shift modifier: sign + or -, then an integer, then one unit (Latin, as in the parser):
| Unit | Meaning |
|---|---|
days | shift by N calendar days |
weeks | shift by N weeks |
hours | shift by N hours |
minutes | shift by N minutes |
Meaning: + 2 days is forward two days, - 1 weeks is back one week (the exact spelling in the third part of {#DATETIME;…;…} is defined by your editor).
Other block syntax and the result string format can differ by version; what matters is a valid instant in the source constant and a modifier written consistently with the parser.
7.4. Math {#MATH} … {/MATH}
Suppose {DATA.ARRAY.LINE} holds:
{
"qty": 3,
"price": 100
}
Template:
Due:
{#MATH}
{DATA.ARRAY.LINE}['qty'] * {DATA.ARRAY.LINE}['price']
{/MATH}
RUB
Due: 300 RUB
Typical operations inside a formula:
| Operation | Form |
|---|---|
| Add / subtract | + and - |
| Multiply / divide | * and / |
| Power | ^ |
| Unary plus and minus | before a number or a parenthesized expression |
| Parentheses | ( ) |
| Rounding | ROUND, CEIL, FLOOR (where a second argument may set decimal places — if supported) |
References {…}['field'] inside MATH must resolve to a number or a string readable as a number.
Otherwise you get an error-marked block inside {#MATH}…{/MATH} — e.g. division by zero or an “empty” value.
What can go wrong
Special blocks are parsed before plain substitutions; on a markup or data error you usually get either unchanged “silent” text as in the editor, or an explicit bracketed message, or inside {#MATH} a #ERROR prefix with details.
Typical cases for each construct are summarized below.
Special-construct error table
{#EACH}
| Situation | Behaviour / what the user sees |
|---|---|
Missing {/EACH} or broken loop header syntax |
The loop does not run; text may stay as typed in the field |
| Key is not an array or value is missing | The loop body is not expanded; the original block or part of it stays unchanged |
| Nested loops mis-nested | Opening/closing tags do not pair correctly; the fragment is not treated as a loop |
{#IF} / {#ELSE} / {/IF}
| Situation | Behaviour |
|---|---|
Missing closing } on {#IF or missing {/IF} |
A bracketed error message is inserted (wording like “IF/ELSE error” depends on the product) |
| Expression does not parse after constant substitution | Same: explicit bracketed error; sometimes a list of debug strings |
| Empty or invalid comparison | Expression error in text or wrong branch — depends on parser rules |
{#DATETIME;…}
| Situation | Behaviour |
|---|---|
| No value at path, empty | Bracketed error (prefix like “DATETIME error” or similar) |
| Wrong field set (not “date + time + zone + timestamp”) | Message: value does not match expected structure |
| Invalid time zone or invalid date/time fields | Message about wrong format or zone |
{#MATH}…{/MATH}
| Situation | Behaviour |
|---|---|
| Constant reference not found | Block replaced with a variant containing #ERROR and reason text inside MATH bounds |
| Path resolves to an array or non-number | Same: #ERROR with explanation |
| After substitution the expression is invalid (parentheses, stray characters) | #ERROR with parser text |
| Division by zero | #ERROR, explicit division-by-zero |
Unknown function or wrong argument count for ROUND / CEIL / FLOOR |
#ERROR with matching text |
Constants and data outside special blocks
| Situation | What you will see |
|---|---|
| Typo in a plain constant name | The name may appear literally; for ARRAY check name and type |
ARRAY: invalid JSON or empty | Part of the text may disappear or zero out — inspect the data source |
Practical tips
- First confirm the upstream block actually writes the constant you need — from the block name and field hints.
- Insert substitution templates only where expressions are enabled.
- For arrays, match the real shape: list vs object, exact field names; to verify, temporarily print the whole constant.
- Split complex markup: a condition or loop in one step, “clean” text with simple
STRINGin another. - Bracketed error text in a message usually points to a condition or date/time issue, not a full bot failure.
Short glossary
| Term | Meaning |
|---|---|
| Constant | A named value in the scenario’s shared data set |
| Key | A string like {A.B.C} with two dots between parts |
| Update | The event that usually starts filling constants |
STRING / ARRAY / BOOL | The type in the middle of the name |
| Expression field | Mode where {…} and special blocks are allowed |
| Special block | {#EACH}, {#IF}, {#DATETIME}, {#MATH} |