Placeholders
Purpose: Dynamic value substitution in scenario action parameters.
Mechanics:
All attributes in params automatically processed by placeholders before action execution. This means:
- All values in
params— strings, numbers, arrays, objects — go through placeholder processing - All nesting levels — placeholders work at any level of
paramsstructure (in strings, array elements, object values) - Automatic processing — happens for each scenario step before action execution
- Data source — system uses accumulated event data and previous action results as value source for substitution
Placeholder Syntax:
Simple replacement:
params:
text: "Hello, {username}!"
chat_id: "{user_id}"With modifiers:
params:
text: "Hello, {username|fallback:Guest}!"
price: "{amount|*0.9|format:currency}"Literal values in quotes:
params:
text: "{'hello'|upper}" # HELLO (without passing through dictionary)
duration: "{'1d 2w'|seconds}" # 1296000 (literal time)
calculation: "{'100'|+50}" # 150 (literal arithmetic)
date_shift: "{'2024-12-25'|shift:+1 day}" # 2024-12-26 (literal date)
# Double quotes for strings with single quotes
text: "{"it's working"}" # it's working
# Escaped quotes
text: "{'it\'s working'}" # it's workingNested placeholders:
params:
text: "Answer: {status|equals:{expected}|value:OK|fallback:BAD}"⚠️ Important: YAML Quotes for Placeholders with regex
Problem: YAML processes escape sequences differently in single and double quotes, which is critical for regex patterns with backslashes.
Solution: Use double quotes for regex patterns with backslashes (\s, \d, \w, etc.).
Examples:
# ❌ ERROR - in single quotes \\s doesn't escape correctly
params:
value: '{event_text|regex:^([^\\s]+)|lower}' # \\s becomes \s (backslash + s), not space!
# ✅ CORRECT - double quotes correctly escape backslashes
params:
value: "{event_text|regex:^([^\\s]+)|lower}" # \\s becomes \s (space in regex)
# ✅ CORRECT - for regex without backslashes can use single quotes
params:
tenant_id: '{user_state|regex:tenant_(\d+)}' # Works, as \d doesn't require double escaping
# ✅ CORRECT - for simple regex with one backslash also use double quotes
params:
value: "{event_text|regex:\\s+([^\\s]+)$|lower}" # \\s for space, \\s for space
# ✅ REGULAR PLACEHOLDERS - no difference
params:
text: "Hello, {username}!" # Works
text: 'Hello, {username}!' # Also worksWhen to use double quotes:
- Regex patterns with backslashes —
{field|regex:pattern}where pattern contains\s,\d,\w,\n,\t, etc. - Important: In double quotes need to use double escaping:
\\sfor space,\\dfor digit - Example:
"{event_text|regex:^([^\\s]+)|lower}"—\\sbecomes\s(space in regex)
When can use single quotes:
- Regex patterns without backslashes — simple patterns without
\s,\d,\w, etc. - Regular placeholders —
{username},{user_id} - Simple modifiers —
{name|upper},{price|format:currency} - Text values — without special characters
Rule: If regex pattern has backslashes (\s, \d, \w, etc.) — always use double quotes with double escaping (\\s, \\d, \\w).
Access to nested data:
params:
# Dot notation for objects
text: "User: {user.profile.name|fallback:Unknown}"
text: "Message: {message.text}"
# Indexes for arrays
text: "First file: {attachment[0].file_id}"
text: "Last user: {users[-1].name}"
# Combined access
text: "Event: {event.attachment[0].type}"
text: "Data: {response.data.items[0].value}"Available Data:
All event and previous action data available for use in placeholders:
Event data:
user_id— user IDusername— usernamechat_id— chat IDevent_text— message textcallback_data— callback button dataevent_date— event timetenant_id— tenant IDmessage_id— message IDreply_to_message_id— ID of message being replied toforward_from— forwarding data- And any other fields — depending on event type
Tenant configuration:
_config— dictionary with tenant configuration (attributes fromconfig.yaml)_config.ai_token— AI API key for tenant (if set)- Other attributes — depending on tenant configuration
- Automatically available in all tenant scenarios
Previous action data:
last_message_id— last sent message IDresponse_data— any data returned by previous actions- Other fields — depending on actions in scenario
Accessing Nested Elements:
Universal data access! Support for dot notation for objects and indexes for arrays.
| Syntax | Description | Example | Result |
|---|---|---|---|
| Objects | |||
object.field | Access object field | {message.text} | Hello world |
object.field.subfield | Nested fields | {user.profile.name} | John Doe |
| Arrays | |||
array[index] | Access array element | {attachment[0].file_id} | file_1 (first file) |
array[-index] | Negative index | {attachment[-1].file_id} | file_3 (last file) |
array[index].field | Array element field | {users[1].name} | Bob (second user name) |
array[index][index] | Nested arrays | {matrix[0][1]} | 2 (matrix element) |
| Combined | |||
object.array[index].field | Mixed access | {event.attachment[0].file_id} | file_1 |
Usage Examples:
params:
# Objects
text: "Message: {message.text}"
text: "User: {user.profile.name|fallback:Unknown}"
# Arrays
text: "First file: {attachment[0].file_id}"
text: "Last user: {users[-1].name|upper}"
text: "File: {attachment[0].file_id}, size: {attachment[0].size}"
# Combined access
text: "Event: {event.attachment[0].type}"
text: "Data: {response.data.items[0].value}"
# Tenant configuration
text: "Token: {_config.ai_token|fallback:Not set}"
# Safe access
text: "{attachment[10].file_id|fallback:File not found}"Getting File Information
Practical example: Getting file_id and type for forwarding files
Create scenario that shows file_id and type of all event attachments:
file_info:
description: "Getting file_id and type of attachments for subsequent forwarding"
trigger:
- event_type: "message"
event_text: "/file"
step:
- action: "send_message"
params:
text: |
📎 File information:
file_id: <code>{event_attachment[0].file_id|fallback:{reply_attachment[0].file_id|fallback:File not found}}</code>
type: <code>{event_attachment[0].type|fallback:{reply_attachment[0].type|fallback:Type not found}}</code>Error handling:
- When accessing non-existent field/index returns
None - Can use
fallbackmodifier for default value - Negative indexes work like in Python:
-1= last element - Unlimited nesting depth supported
Modifiers:
| Modifier | Description | Example | Result |
|---|---|---|---|
| Arithmetic Operations | |||
+value | Addition | {price|+100} | 1500 (if price=1400) |
-value | Subtraction | {price|-50} | 1350 (if price=1400) |
*value | Multiplication | {price|*0.9} | 1260 (if price=1400) |
/value | Division | {seconds|/3600} | 2.5 (if seconds=9000) |
%value | Modulo | {number|%7} | 3 (if number=10) |
| Case | |||
upper | Uppercase | {name|upper} | JOHN (if name="John") |
lower | Lowercase | {name|lower} | john (if name="John") |
title | Title case each word | {name|title} | John Doe (if name="john doe") |
capitalize | First letter uppercase | {name|capitalize} | John (if name="john") |
case:type | Case conversion | {name|case:upper} | JOHN (if name="John") |
| Lists | |||
tags | Convert to tags | {users|tags} | @user1 @user2 |
list | Bulleted list | {items|list} | • item1\n• item2 |
comma | Comma separated | {items|comma} | item1, item2 |
expand | Expand array of arrays one level (in arrays only) | {keyboard|expand} | [[a, b], [c]] → [a, b], [c] (in array) |
keys | Extract keys from object (dict) to array | {storage_values|keys} | ["group1", "group2"] (if storage_values={"group1": {...}, "group2": {...}}) |
| Transformations | |||
code | Wrap value in code block | {field|code} | <code>value</code> |
| String Formatting | |||
truncate:length | Truncate text | {text|truncate:50} | Very long text... |
| Date & Time Operations | |||
shift:±interval | Shift date by interval (PostgreSQL style: +1 day, -2 hours, +1 year 2 months). Supports all date formats including ISO with timezone, correct month/year handling | {created|shift:+1 day} | 2024-12-26 (if created="2024-12-25") |
seconds | Convert time strings to seconds (supports format: Xw Yd Zh Km Ms) | {duration|seconds} | 9000 (if duration="2h 30m") |
to_date | Round date to day start (00:00:00), returns ISO format | {created|to_date} | 2024-12-25 00:00:00 |
to_hour | Round date to hour start (minutes and seconds = 0), returns ISO format | {created|to_hour} | 2024-12-25 15:00:00 (if created="2024-12-25 15:30:45") |
to_minute | Round date to minute start (seconds = 0), returns ISO format | {created|to_minute} | 2024-12-25 15:30:00 (if created="2024-12-25 15:30:45") |
to_second | Round date to second start (microseconds = 0), returns ISO format | {created|to_second} | 2024-12-25 15:30:45 |
to_week | Round date to week start (Monday 00:00:00), returns ISO format | {created|to_week} | 2024-12-23 00:00:00 (if created="2024-12-25 15:30:45") |
to_month | Round date to month start (1st day, 00:00:00), returns ISO format | {created|to_month} | 2024-12-01 00:00:00 (if created="2024-12-25 15:30:45") |
to_year | Round date to year start (January 1st, 00:00:00), returns ISO format | {created|to_year} | 2024-01-01 00:00:00 (if created="2024-12-25 15:30:45") |
| Date & Time Formatting | |||
format:timestamp | Convert to Unix timestamp | {date|format:timestamp} | 1703512200 |
format:date | Date format (dd.mm.yyyy) | {timestamp|format:date} | 25.12.2024 |
format:time | Time format (HH:MM) | {timestamp|format:time} | 14:30 |
format:time_full | Time format with seconds (HH:MM:SS) | {timestamp|format:time_full} | 14:30:45 |
format:datetime | Full format (dd.mm.yyyy HH:MM) | {timestamp|format:datetime} | 25.12.2024 14:30 |
format:datetime_full | Full format with seconds (dd.mm.yyyy HH:MM:SS) | {timestamp|format:datetime_full} | 25.12.2024 14:30:45 |
format:pg_date | PostgreSQL date format (YYYY-MM-DD) | {timestamp|format:pg_date} | 2024-12-25 |
format:pg_datetime | PostgreSQL date-time format (YYYY-MM-DD HH:MM:SS) | {timestamp|format:pg_datetime} | 2024-12-25 14:30:45 |
| Number Formatting | |||
format:currency | Currency formatting | {amount|format:currency} | 1000.00 ₽ |
format:percent | Percentage formatting | {value|format:percent} | 25.5% |
format:number | Number formatting | {value|format:number} | 1234.56 |
| Conditional | |||
equals:value | Equality check (string comparison) | {status|equals:active} | true or false |
in_list:items | Check inclusion in list | {role|in_list:admin,moderator} | true or false |
true | Truth check (recommended for boolean) | {is_active|true} | true or false |
exists | Check value existence (not None and not empty string) | {field|exists} | true or false |
is_null | Check for null (None, empty string or string "null") | {field|is_null} | true or false |
value:result | Return value when true | {status|equals:active|value:Active} | Active or null |
| Utility | |||
fallback:value | Default value | {username|fallback:Guest} | John or Guest |
length | Count length | {text|length} | 15 (character count) |
length | Count array length | {array|length} | 3 (element count) |
regex:pattern | Extract by regex | {text|regex:(\d+)} | 123 (first number) |
| Async Actions | |||
ready | Check async action readiness | {_async_action.ai_req_1|ready} | true if completed, false if executing |
not_ready | Check action still executing | {_async_action.ai_req_1|not_ready} | true if executing, false if ready |
Usage Examples:
params:
# Simple operations
result: "{value|+100|format:currency}"
# Conditional logic
status: "{user_status|equals:active|value:Active|fallback:Inactive}"
# Modifier chain
formatted: "{price|*0.9|format:currency|fallback:0.00 ₽}"
# Regex extraction (use double quotes for backslashes!)
duration: "{event_text|regex:(\\d+)\\s*min|fallback:0}"
# Dot notation
name: "{user.profile.name|fallback:Unknown}"
# Time values
duration_seconds: "{duration|seconds}"
duration_minutes: "{duration|seconds|/60}"
duration_hours: "{duration|seconds|/3600}"
formatted_time: "{duration|seconds|format:number}"
# Date shifting
tomorrow: "{created|shift:+1 day}"
next_month: "{created|shift:+1 month|format:date}"
complex_shift: "{created|shift:+1 year 2 months|format:datetime}"
# Rounding to period start
start_of_day: "{created|to_date}"
start_of_week: "{created|to_week}"
formatted_month_start: "{created|to_month|format:date}"
# Boolean fields (distinguish True/False/None)
is_active: "{is_active|equals:True|value:✅ Enabled|fallback:{is_active|equals:False|value:❌ Disabled|fallback:❓ Unknown}}"
is_polling: "{is_polling|equals:True|value:✅ Active|fallback:{is_polling|equals:False|value:❌ Inactive|fallback:❓ Unknown}}"
# String fields (use equals:value)
status: "{user_status|equals:active|value:Active|fallback:Inactive}"
# Simplified version (if None not expected)
is_simple: "{is_active|true|value:✅ Enabled|fallback:❌ Disabled}"
# Check value existence (in conditions)
# In conditions use: {field|exists} == True or {field|exists} == False
# Check for null (in conditions)
# In conditions use: {field|is_null} == True or {field|is_null} == FalseExample using exists in conditions:
step:
- action: "validate"
params:
condition: "{response_value.feedback|exists} == True"
transition:
- action_result: "success"
transition_action: "continue" # Value exists
- action_result: "failed"
transition_action: "jump_to_scenario"
transition_value: "create_feedback" # Value doesn't existImportant: boolean vs string fields
For boolean fields (True/False):
- Recommended:
{is_active|true|value:✅ Enabled|fallback:❌ Disabled} - Doesn't work:
{is_active|equals:true|value:✅ Enabled}(string comparison)
For string fields:
- Use:
{status|equals:active|value:Active|fallback:Inactive} truemodifier not suitable for string values
Practical Examples:
Example 1: Personalized greeting
step:
- action: "send_message"
params:
text: |
👋 Hello, {username|fallback:Guest}!
📊 Your statistics:
• ID: {user_id}
• Chats: {chat_count|fallback:0}
• Last login: {last_login|format:datetime|fallback:Unknown}Example 2: Dynamic menu with conditions
step:
- action: "send_message"
params:
text: |
🎛️ Main Menu
Status: {user_status|equals:active|value:✅ Active|fallback:❌ Inactive}
Balance: {balance|format:currency|fallback:0.00 ₽}
inline:
- [{"📊 Statistics": "stats"}, {"💰 Balance": "balance"}]
- [{"⚙️ Settings": "settings"}]Example 3: Error handling with fallback
step:
- action: "send_message"
params:
text: |
❌ Processing error
Error code: {error_code|fallback:UNKNOWN}
Message: {error_message|fallback:Unknown error}
Time: {timestamp|format:datetime}
inline:
- [{"🔄 Retry": "retry"}, {"🏠 Main Menu": "main"}]Example 4: Mathematical calculations
step:
- action: "send_message"
params:
text: |
💰 Cost calculation
Price: {base_price|format:currency}
Discount: {discount_percent|fallback:0}%
Total: {base_price|*{discount_percent|/100}|*{base_price|-1}|+{base_price}|format:currency}Example 5: Extracting data from text
step:
- action: "send_message"
params:
text: |
📝 Processing order
Order number: "{event_text|regex:order\\s*(\\d+)|fallback:Not found}"
Amount: "{event_text|regex:amount\\s*(\\d+)|format:currency|fallback:0.00 ₽}"
Delivery time: "{event_text|regex:(\\d+)\\s*min|fallback:30}" minutesExample 6: Dynamic keyboard with expand modifier
step:
# Get tenant list
- action: "get_tenants_list"
params: {}
# Build dynamic keyboard from public tenants
- action: "build_keyboard"
params:
items: "{public_tenant_ids}"
keyboard_type: "inline"
text_template: "Tenant $value$"
callback_template: "select_tenant_$value$"
buttons_per_row: 2
# Send message with dynamic keyboard + static buttons
- action: "send_message"
params:
text: "Select tenant:"
inline:
- "{keyboard|expand}" # Expand dynamic keyboard
- [{"🔙 Back": "main_menu"}] # Static buttonHow expand works:
expandmodifier expands array of arrays one level only when used in array- In example above
{keyboard|expand}ininlinearray expands[[{...}, {...}], [{...}]]to[{...}, {...}], [{...}] - This allows combining dynamically generated keyboard rows with static rows
- Important:
expandworks only in array context. In string or dict value remains unchanged
Example without expand (incorrect):
inline:
- "{keyboard}" # ❌ Gets [[[...]], [...]] - 3 nesting levels!
- [{"🔙 Back": "main_menu"}]Example with expand (correct):
inline:
- "{keyboard|expand}" # ✅ Gets [[...], [...]] - 2 nesting levels
- [{"🔙 Back": "main_menu"}]