Scenario Creation Guide
📖 Complete guide to creating and configuring scenarios for Telegram bots with support for placeholders, transitions, and dynamic logic.
📑 Table of Contents
- Scenario Structure
- Triggers (trigger)
- Scheduled Scenarios (Cron Execution)
- Actions (step)
- Transitions (transition)
- Placeholders
- Async Actions
📋 Scenario Structure
Scenario Organization
File Organization — scenarios can be organized in folders and subfolders for convenience. All YAML files from scenarios/ folder (including subfolders) are automatically loaded.
Important Principles:
- Name Uniqueness: Scenario name must be unique within the tenant
- Flexible File Structure: Can create files with any names and organize them in subfolders — all files will be loaded
- Recursive Loading: All YAML files from
scenarios/and all subfolders are automatically parsed - Global Transitions: Scenarios can transition to any other tenant scenarios through
jump_to_scenario
Example Structure:
tenant_101/
scenarios/
commands.yaml # Scenarios: start, menu, help
settings.yaml # Scenarios: settings, profile
admin/ # Subfolder for organization
panel.yaml # Scenarios: admin_panel, logs
moderation.yaml # Scenarios: ban, kick
users/ # Another subfolder
profile.yaml # Scenarios: user_profile, edit_profileIn this example:
- All files from
scenarios/and subfolders are loaded automatically - Scenarios can transition to each other regardless of folder location
- Subfolders are used only for logical file organization
YAML File Structure:
# Scenario name
scenario_name:
description: "Scenario description (optional)"
# Scenario launch conditions (optional for scheduled scenarios)
trigger:
- event_type: "message"
event_text: "/start"
- event_type: "callback"
callback_data: "start"
# Launch schedule (optional, cron expression)
schedule: "0 9 * * *" # Every day at 9:00
# Scenario actions
step:
- action: "send_message"
params:
text: "Hello!"
inline:
- [{"Button": "callback_data"}]Scenario Parameters:
description— scenario description (optional)trigger— scenario launch conditions (optional for scheduled scenarios)schedule— cron expression for scheduled launch (optional)step— action sequence
Practical Example:
user_registration:
description: "User registration process with validation and notifications"
# TRIGGERS: Each works independently - any of them will launch scenario
trigger:
- event_type: "message" # Command /register
event_text: "/register"
- event_type: "callback" # Button "Start Registration"
callback_data: "start_registration"
# STEPS: Executed sequentially one after another
step:
# Step 1: Send greeting
- action: "send_message"
params:
text: |
👋 Welcome, {username|fallback:Guest}!
Let's register you in the system.
inline:
- [{"✅ Continue": "continue_registration"}]
# Step 2: Check user permissions with transitions
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_welcome" # Transition to admin welcome
- action_result: "error"
transition_action: "continue" # Normal flow for users
# Step 3: Request name
- action: "send_message"
params:
text: |
📝 Step 2 of 3
Please enter your name:
inline:
- [{"❌ Cancel": "cancel_registration"}]
# Step 4: Send confirmation with attachments
- action: "send_message"
params:
text: |
✅ Registration started!
We sent you instructions via email.
📎 Also attaching useful materials:
attachment:
- file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
type: "photo"
- file_id: "AgACAgIAAxkBAAIUQWjyOH65wQYR4onkrrqM4J65yUD7AAM8-zEb4vGQS1mwvckFcZHlAQADAgADdwADNgQ"
type: "photo"
inline:
- [{"🏠 Main Menu": "main_menu"}]
# Step 5: Delete previous message
- action: "delete_message"
params:
delete_message_id: "{last_message_id}" # Use previous message ID
admin_welcome:
description: "Admin welcome (transition scenario)"
# NO TRIGGERS - called only programmatically
step:
- action: "send_message"
params:
text: |
🔧 Welcome, administrator!
You have additional permissions.
inline:
- [{"⚙️ Control Panel": "admin_panel"}]Key Points:
- Triggers — OR logic: if at least one triggers, scenario launches
- Steps — executed strictly in order of appearance
- Transitions — control execution flow through
transition:continue— continue executing next actionsbreak— interrupt only current scenario executionabort— interrupt entire current scenario chain (including nested)stop— interrupt all event processing (all scenarios)move_steps— move specified number of steps (positive = forward, negative = backward)jump_to_step— jump to specific step by index (steps numbered from 0)jump_to_scenario— transition to another scenario (to any tenant scenario)
- Transition scenarios — scenarios without triggers, called programmatically from other scenarios
- Scheduled scenarios — scenarios with
schedulefield (cron expression), launched automatically on schedule - Hybrid scenarios — can have both
triggerandschedulesimultaneously (work on events AND on schedule) - Event data — available in action parameters (user_id, chat_id, timestamp, etc.)
- Attachments — can forward files through
attachment[0].file_idandattachment[0].type
🎯 Triggers (trigger)
Purpose: Conditions under which scenario is launched.
Trigger Mechanics:
Simple form (attributes):
trigger:
- event_type: "message"
event_text: "/start"Converts to condition:
$event_type == "message" and $event_text == "/start"Complex form (with condition):
trigger:
- event_type: "message"
condition: |
$event_text == "test"
and $user_id > 100
or $username in ('@admin', '@moderator')Converts to condition:
$event_type == "message" and ($event_text == "test" and $user_id > 100 or $username in ('@admin', '@moderator'))Trigger Logic:
- Between triggers — OR logic: if at least one triggers, scenario launches
- Within trigger — AND logic: all conditions must be met
- In condition — full freedom:
and,or,not, parentheses, operators
⚠️ Important: Placeholders in condition
Problem: When using placeholders in trigger condition for string comparison, you need to wrap placeholder in quotes. Otherwise placeholder will "expand" as attribute to search for data in condition_parser, not as string value.
Solution: Always wrap placeholders in quotes when comparing strings in conditions.
Examples:
# ❌ ERROR - placeholder expands to random_user without quotes
# condition_parser tries to find variable random_user in data
trigger:
- event_type: "message"
condition: "{_cache.detected_intent} == 'random_user'"
# After placeholder processing: random_user == 'random_user'
# condition_parser looks for variable random_user in data, not comparing strings!
# ✅ CORRECT - placeholder wrapped in quotes
trigger:
- event_type: "message"
condition: "'{_cache.detected_intent}' == 'random_user'"
# After placeholder processing: 'random_user' == 'random_user'
# condition_parser correctly compares strings
# ✅ CORRECT - number comparison (quotes not needed)
trigger:
- event_type: "message"
condition: "{user_id} > 100"
# After placeholder processing: 12345 > 100
# Numeric comparison works correctly
# ✅ CORRECT - comparison with modifiers (quotes not needed for boolean)
trigger:
- event_type: "message"
condition: "{response_value.feedback|exists} == True"
# exists modifier returns boolean, comparison works correctlyRule:
- For string comparison — always wrap placeholder in quotes:
'{_cache.field}' == 'value' - For number comparison — quotes not needed:
{user_id} > 100 - For boolean values — quotes not needed:
{field|exists} == True
Available Fields for Conditions:
⚠️ Important: All fields in conditions must have $name marker (e.g., $event_type, $user_id, $event_text).
Event data:
$event_text— message text$event_type— event type (message, callback)$user_id— user ID$username— username$chat_id— chat ID$callback_data— callback button data$event_date— event time$tenant_id— tenant ID$message_id— message ID$chat_type— chat type$is_group— whether chat is group- And other event fields (see EVENT_GUIDE.md)
Previous action data:
$last_message_id— last sent message ID$response_data— any data returned by previous actions- Other fields — depending on actions in scenario
Nested fields:
$message.text— access nested fields via dot$user.profile.name— multi-level nesting
Operators in Conditions:
==— equals!=— not equals>,<,>=,<=— number comparisonin,not in— inclusion in list~— contains substring!~— doesn't contain substringregex— regular expressionis_null— field is empty (None or empty string)not is_null— field is not empty (exists and has value)
⏰ Scheduled Scenarios (Cron Execution)
Purpose: Automatic scenario launch on schedule through cron expressions.
Scheduled Scenario Structure:
daily_report:
description: "Daily report at 9:00"
# Cron expression for launch every day at 9:00
schedule: "0 9 * * *"
# Triggers optional - scenario will work on schedule
# Can add triggers for hybrid mode (on events AND on schedule)
step:
- action: "send_message"
params:
target_chat_id: 123456789
text: |
📊 Daily Report
Launch time: {scheduled_at|format:datetime}
Scenario: {scheduled_scenario_name}Cron Expressions:
Format: minute hour day month day_of_week
| Example | Description |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour at start |
0 9 * * * | Every day at 9:00 |
0 9 * * 1-5 | Every weekday at 9:00 |
0 0 1 * * | First day of month at 00:00 |
ℹ️ Important: Cron expressions interpreted in platform local time.
Available Fields in Scheduled Scenarios:
Scheduled scenarios have special event fields available. For detailed description of all fields see Scheduled Scenario Fields section in EVENT_GUIDE.md.
Usage Example:
step:
- action: "send_message"
params:
text: |
⏰ Scheduled scenario launched
Time: {scheduled_at|format:datetime}
Name: {scheduled_scenario_name}
ID: {scheduled_scenario_id}Hybrid Mode (trigger + schedule):
Scenario can work on events and schedule simultaneously:
hybrid_scenario:
description: "Works on events AND on schedule"
# Launch on events
trigger:
- event_type: "message"
event_text: "/report"
# Launch on schedule (every day at 9:00)
schedule: "0 9 * * *"
step:
- action: "send_message"
params:
text: |
📊 Report
{scheduled_at|fallback:Launched manually|format:datetime}⚠️ Important Limitations and Recommendations:
1. Scenario Synchronization:
- During scenario synchronization (deletion and recreation), last run information is lost (
last_scheduled_run) - After synchronization, scheduled scenarios recalculate next run time from current moment
- Recommendation: Synchronize scenarios during low scheduled task activity periods
2. Missed Launch:
- If synchronization happens at moment when scheduled scenario should launch, launch may be missed
- Recommendation: Avoid scenario synchronization few seconds before planned launch time
3. Error Behavior:
- Scheduled scenarios update next run time even on execution error
- This ensures predictable behavior and prevents repeated launches of problematic scenarios
4. Execution Context:
- Scheduled scenarios execute with system context (automatically get
bot_id,tenant_id) - Common event fields unavailable: Most common fields (
user_id,chat_id,event_text,message_id, etc.) unavailable in scheduled scenarios, as event is not associated with user or chat - Passing parameters: Must pass most parameters manually in actions or extract data from tenant or user storage (from database)
- Use
target_chat_idin action parameters to specify concrete chat
⚡ Actions (step)
Purpose: Sequence of actions executed in scenario.
Important:
- Step execution order determined by their position in
steparray (top to bottom). Steps executed sequentially in order specified in YAML. Step numbering starts from 0. - Sequential Execution: By default scenario goes sequentially through steps regardless of action result. If step has no
transitionor no suitable transition for action result, execution automatically proceeds to next step. - Flow Changes: Transitions (
transition) allow changing execution sequence (jump to another step, scenario, interrupt execution, etc.).
Sending Message:
Inline buttons:
step:
- action: "send_message"
params:
text: |
👋 Hello!
📋 Choose action:
inline:
- [{"📋 Menu": "main_menu"}, {"📚 Help": "help"}]
- [{"🌐 Website": "https://example.com"}, {"📞 Support": "tg://resolve?domain=support"}]
- [{"🔙 Back": "start"}]Reply keyboard:
step:
- action: "send_message"
params:
text: |
👋 Hello!
📋 Choose action:
reply:
- ["📋 Menu", "📚 Help"]
- ["🔙 Back"]Remove keyboard:
step:
- action: "send_message"
params:
text: "Keyboard removed"
reply: [] # Empty array removes keyboardMessage Parameters:
text— message text (supports multiline with|)inline— button array:[{"Button text": "callback_data"}]— one button per row[{"Button 1": "data1"}, {"Button 2": "data2"}]— multiple buttons per row[{"Link": "https://example.com"}]— button with link
reply— reply keyboard:[["Button 1", "Button 2"]]— one row of buttons[["Button 1"], ["Button 2"]]— each button in separate row[]— remove keyboard
attachment— attachment array (photos, documents, videos, audio):[{"file_id": "AgACAgI...", "type": "photo"}]— forward file by file_id[{"file_id": "{event_attachment[0].file_id}", "type": "{event_attachment[0].type}"}]— dynamic forwarding from event[{...}, {...}]— multiple files at once
Attaching Files:
📎 Attaching files to messages:
Can send photos, documents, videos, audio. Files can be taken from events or use saved file_id.
Example: Forwarding file from event:
step:
- action: "send_message"
params:
text: "Forwarding your file back"
attachment:
- file_id: "{event_attachment[0].file_id}"
type: "{event_attachment[0].type}"Example: Sending file by known file_id:
step:
- action: "send_message"
params:
text: "Instructions"
attachment:
- file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
type: "photo"Example: Multiple files:
step:
- action: "send_message"
params:
text: "Useful materials"
attachment:
- file_id: "{event_attachment[0].file_id}"
type: "{event_attachment[0].type}"
- file_id: "BQACAgIAAxkBAAI..."
type: "document"📋 How to get file_id and type: see scenario example below 👇
Tenant Configuration in Actions:
Purpose: Automatic use of tenant configuration attributes in actions.
Some actions automatically use parameters from tenant configuration (_config). Such parameters don't need to be passed explicitly in params — they are automatically taken from _config.
Usage Example:
step:
- action: "completion"
params:
prompt: "Hello, how are you?"
# ai_token automatically taken from _config.ai_token
# No need to specify ai_token explicitly!Access through placeholders:
Tenant configuration also directly accessible through placeholders:
step:
- action: "send_message"
params:
text: |
Tenant configuration:
Token: {_config.ai_token|fallback:Not set}Important:
_configavailable in all tenant scenarios automatically- If attribute not set in tenant configuration, value will be
None - Use
fallbackin placeholders to handle missing values - If parameter passed explicitly in
params, explicit value has priority
📖 More about tenant configuration: See config.yaml section in TENANT_CONFIG_GUIDE.md
Data Caching in Scenarios:
Purpose: Automatic saving of action results to _cache for use in subsequent scenario steps.
⚠️ Important: Cache Scope:
- Cache
_cachesaved only within one scenario execution chain (one chain processing one event) - Data available between scenarios within one execution chain (e.g., when transitioning via
jump_to_scenarioorexecute_scenario) - Cache NOT saved between different execution chains (even if triggered by one event — each scenario has its isolated cache)
- Cache NOT saved between different events (different messages, different callbacks)
- For long-term data storage use
set_storage/set_user_storageinstead ofset_cache - See more in Saving Context Within Event Processing section
General Mechanics:
- Each action can return
response_datawith any fields - Data automatically saved to
_cacheand available in subsequent actions through placeholders - Caching system supports two system fields for data structure management:
_namespace— for overwrite control and organizing data into nested structures_response_key— for substituting replaceable field key before saving to_cache
- All mechanisms work at scenario engine level and don't require service changes
Flat Caching (default):
By default flat caching is used — data merged directly into _cache without nesting. Data accessed through {_cache.field}:
step:
- action: "generate_int"
params:
min: 1
max: 100
# response_data: {"random_value": 42, "random_seed": 123}
# Goes to: _cache.random_value = 42, _cache.random_seed = 123
- action: "send_message"
params:
text: "Number: {_cache.random_value}" # ✅ CORRECTExample with multiple actions:
step:
- action: "generate_int"
params:
min: 1
max: 100
# _cache.random_value = 42, _cache.random_seed = 123
- action: "generate_array"
params:
min: 1
max: 10
count: 5
# _cache.random_list = [3, 7, 2, 9, 1], _cache.random_seed = 456
- action: "send_message"
params:
text: |
Number: {_cache.random_value}
Array: {_cache.random_list|comma}⚠️ Flat Caching Problems:
1. Name conflicts between different actions:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
# _cache.response_value = "key1"
- action: "get_user_storage_value"
params:
key: "api_secret"
# _cache.response_value = "secret1" # ⚠️ Overwritten!
- action: "send_message"
params:
text: "Key: {_cache.response_value}" # Shows only "secret1"2. Overwrite on repeated calls of same action:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
# _cache.response_value = "key1"
- action: "get_storage_value" # Same action!
params:
group_key: "settings"
key: "api_secret"
# _cache.response_value = "secret1" # ⚠️ First result overwritten!System Field _namespace (overwrite control):
Purpose: Saving data to nested structure _cache[_namespace] for overwrite control and avoiding name conflicts.
Mechanics:
- If
_namespacespecified, data saved to_cache[_namespace]instead of flat_cache - Data access:
{_cache[_namespace].field} - Allows saving results of repeated calls of same action
- Allows avoiding name conflicts between different actions
Usage Example:
step:
- action: "generate_int"
params:
min: 1
max: 100
_namespace: "my_random"
# Goes to: _cache.my_random = {random_value: 42, random_seed: 123}
- action: "send_message"
params:
text: "Number: {_cache.my_random.random_value}" # ✅ CORRECTSolving conflicts with _namespace:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
_namespace: "first"
# _cache.first.response_value = "key1"
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_secret"
_namespace: "second"
# _cache.second.response_value = "secret1"
# ✅ Both results saved!
- action: "send_message"
params:
text: |
First: {_cache.first.response_value}
Second: {_cache.second.response_value}System Field _response_key (renaming main field):
Purpose: Allows setting custom name for action result main field for convenient _cache data access.
Mechanics:
- Some actions return main value under specific key (e.g.,
storage_values,user_storage_values,formatted_text) - By default data saved to
_cachewith original key from action result - If
_response_keyspecified, engine automatically renames main field before saving to_cache - Works only for actions that support renaming main field
Usage Example:
step:
# Get storage value (by default will be in _cache.storage_values)
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_response_key: "api_key" # Rename storage_values to api_key
# response_data: {"storage_values": "secret_key_123"}
# Goes to: _cache.api_key = "secret_key_123" (instead of _cache.storage_values)
# Use renamed value
- action: "send_message"
params:
text: "API key: {_cache.api_key}" # ✅ CORRECT - use custom keyExample with multiple calls of same action:
step:
# First call - save to api_key
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_response_key: "api_key"
# _cache.api_key = "secret_key_123"
# Second call - save to api_secret
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_secret"
_response_key: "api_secret"
# _cache.api_secret = "secret_secret_456"
# ✅ Both values saved under convenient keys!
- action: "send_message"
params:
text: |
API Key: {_cache.api_key}
API Secret: {_cache.api_secret}Example with text formatting:
step:
# Format data and save under custom key
- action: "format_data_to_text"
params:
format_type: "list"
title: "Available intents:"
item_template: '- "$id" - $description'
input_data: "{storage_values.ai_router.intents}"
_response_key: "formatted_intents"
# response_data: {"formatted_text": "• intent1\n• intent2"}
# Goes to: _cache.formatted_intents = "• intent1\n• intent2"
# (instead of _cache.format_data_to_text.formatted_text)
- action: "send_message"
params:
text: "{_cache.formatted_intents}" # ✅ Use convenient keyCombining _namespace and _response_key:
step:
# Use both _namespace and _response_key simultaneously
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_namespace: "first"
_response_key: "api_key"
# _cache.first.api_key = "secret_key_123"
# (instead of _cache.first.storage_values)
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_secret"
_namespace: "second"
_response_key: "api_secret"
# _cache.second.api_secret = "secret_secret_456"
- action: "send_message"
params:
text: |
First: {_cache.first.api_key}
Second: {_cache.second.api_secret}Usage Recommendations:
General Recommendations:
- ✅ Flat caching by default — less nesting, simpler access
- ✅ Use
_namespacefor overwrite control on repeated calls of same action - ✅ Use
_namespaceto avoid name conflicts between different actions - ✅ Use
_response_keyfor convenient access to action main values - ✅ Combine
_namespaceand_response_keyfor full control over_cachedata structure - ✅ Use meaningful names for
_namespaceand_response_key - ✅ Use
set_cacheto extract data from nesting to flat when needed
⚠️ Important:
_response_keyworks only for actions that support substituting main value key- If action doesn't support key substitution,
_response_keyignored (without error) - Substitution happens automatically before saving to
_cache
Saving Context Within Event Processing:
⚠️ Important: Cache _cache saved only within one scenario execution chain. This means data available between scenarios within one chain (when one scenario transitions to another via jump_to_scenario or execute_scenario), but NOT saved between different execution chains (even if triggered by one event) and between different events.
What is "one execution chain":
- One chain = sequence of scenarios linked by transitions (
jump_to_scenario,execute_scenario) - If scenario A transitions to scenario B via
jump_to_scenario— this is one chain, cache saved - If scenario A calls scenario B via
execute_scenario— this is one chain, cache saved - If event triggers multiple independent scenarios — these are different chains, each has its isolated cache
What is "one event":
- One event = one message from user, one callback from button, one scheduled launch, etc.
- If user sent message
/start— this is one event - If user clicked button with
callback_data: "menu"— this is another, separate event - Each event can trigger multiple scenarios, but each scenario executes independently with its cache
What is saved within one event:
- All event data (event data:
user_id,chat_id,event_text, etc.) - All action results (data from
response_dataautomatically goes to flat_cacheor_cache[_namespace]if_namespacespecified, considering key substitution via_response_key) - Temporary data from
_cache(saved viaset_cacheaction or other actions) - Any other data accumulated in event processing context
When cache saved (within one execution chain):
✅ When transitioning via jump_to_scenario:
# Scenario 1: main_scenario
step:
- action: "set_cache"
params:
cache:
selected_user: "@username"
- action: "send_message"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "next_scenario"
# Scenario 2: next_scenario
# ✅ All data from main_scenario available (one execution chain):
step:
- action: "send_message"
params:
text: "User: {_cache.set_cache.selected_user}" # ✅ AvailableWhen cache NOT saved:
❌ Between different scenarios triggered by one event:
# Event: message "/start"
# Scenario 1: welcome_scenario (triggered by "/start")
step:
- action: "set_cache"
params:
cache:
welcome_sent: true
# Scenario 2: analytics_scenario (also triggered by "/start", but executes independently!)
step:
- action: "send_message"
params:
text: "Welcome sent: {_cache.set_cache.welcome_sent}" # ❌ NOT available! Different execution chains❌ Between different events:
# Event 1: User sent "/start"
step:
- action: "set_cache"
params:
cache:
step: 1
# Event 2: User sent "/menu" (different event!)
step:
- action: "send_message"
params:
text: "Step: {_cache.set_cache.step}" # ❌ NOT available! Different events🔄 Transitions (transition)
Purpose: Managing scenario execution flow.
step:
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_panel" # Transition to admin scenario
- action_result: "error"
transition_action: "continue" # Normal flow for users
- action_result: "timeout"
transition_action: "break" # Interrupt only current scenario on timeout
- action_result: "error"
transition_action: "abort" # Interrupt entire chain on critical error
- action_result: "system_failure"
transition_action: "stop" # Interrupt all event processing on system errorTransition Types:
continue— continue executing next actionsbreak— interrupt only current scenario executionabort— interrupt entire current scenario chain (including nested)stop— interrupt all event processing (all scenarios)move_steps— move specified number of steps (positive number = forward, negative = backward). E.g.,move_step: 1jumps to next step,move_step: 2skips 1 step and jumps to nextjump_to_step— jump to specific step by index (steps numbered from 0, e.g.,jump_to_step: 5jumps to step with index 5)jump_to_scenario— transition to another scenario (similar to break + jump, current scenario doesn't continue executing)any— universal transition processed first regardless of action result
Transition Features:
transition_value— required formove_steps(number of steps),jump_to_step(step index, starting from 0) andjump_to_scenario(scenario name)- Scenario search — system looks for scenario in same tenant
- Transition priority —
anytransition processed first, regardless of action result - Single transition — always only one transition executed from all possible
- Blocking other transitions — if
anytransition exists, other transitions (success,error,timeout, etc.) won't be processed - Difference between transitions:
break— interrupts only current scenario, other scenarios (if any) continue event processingabort— interrupts entire current scenario chain (including all nested scenarios), but allows other scenarios (from other triggers) to continue workingstop— completely stops event processing in all scenarios, returns control to systemjump_to_scenario— interrupts current scenario execution and transitions to another (similar to break + jump)
🔧 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> |
| Formatting | |||
truncate:length | Truncate text | {text|truncate:50} | Very long text... |
format:date | Date format | {timestamp|format:date} | 25.12.2024 |
format:time | Time format | {timestamp|format:time} | 14:30 |
format:time_full | Time format with seconds | {timestamp|format:time_full} | 14:30:45 |
format:datetime | Full format | {timestamp|format:datetime} | 25.12.2024 14:30 |
format:datetime_full | Full format with seconds | {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 |
seconds | Convert time strings to seconds (supports format: Xw Yd Zh Km Ms) | {duration|seconds} | 9000 (if duration="2h 30m") |
shift:±interval | Shift date by interval (PostgreSQL style: +1 day, -2 hours, +1 year 2 months). Supports all date formats, correct month/year handling | {created|shift:+1 day} | 2024-12-26 (if created="2024-12-25") |
| Rounding to Period Start | |||
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") |
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 |
format:timestamp | Convert to timestamp | {date|format:timestamp} | 1703512200 |
| 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: Transition to scenario (interrupt current)
step:
- action: "check_user_role"
params:
required_role: "admin"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_panel" # Transition to admin panel, current scenario interrupts
- action_result: "error"
transition_action: "continue" # Continue executing current scenario
# This step won't execute if jump_to_scenario triggered
- action: "send_message"
params:
text: "Regular menu for users"Example 7: Moving through steps
step:
- action: "check_permissions"
params:
required_role: "admin"
transition:
- action_result: "success"
transition_action: "move_steps"
transition_value: 2 # Move 2 steps forward (skip 1 step, go to next)
- action_result: "error"
transition_action: "continue" # Execute all stepsExample 7.1: Jump to specific step
step:
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_step"
transition_value: 5 # Jump to step with index 5 (steps numbered from 0)
- action_result: "failed"
transition_action: "continue" # Continue executionExample 8: Universal transition "any"
step:
- action: "send_message"
params:
text: "Message sent"
transition:
- action_result: "any" # Executes regardless of result
transition_action: "abort" # Always interrupt execution
- action_result: "success"
transition_action: "continue" # ❌ This transition won't execute due to "any"
- action_result: "error"
transition_action: "continue" # ❌ This transition won't execute due to "any"⚠️ Important: any transition has highest priority and blocks execution of all other transitions. System always executes only one transition from all possible.
Example 9: 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"}]⚡ Async Actions
Purpose: Launch long actions in background with ability to continue scenario execution and check result readiness.
Mechanics:
Async actions allow launching long operations (e.g., LLM requests, large data processing) in background without blocking scenario execution. Scenario can continue executing other actions, check result readiness and wait for completion when necessary.
Async Action Structure:
step:
- step_id: 1
action_name: "ai_request"
async: true # Async execution flag
action_id: "ai_req_1" # Unique ID for tracking (required)
params:
prompt: "Hello!"Parameters:
async: true— flag that action should execute asynchronouslyaction_id— unique action identifier (required for async actions)action_name— action name to executeparams— action parameters
How Async Action Mechanism Works:
What happens when launching async action:
Launch action in background:
- When launching async action it starts executing in background without blocking scenario execution
- Scenario continues working and can execute other actions while async action executes
- System automatically tracks action execution state
Save action reference in
_async_action:- System saves reference to executing action in special field
_async_action[action_id] _async_action— system dictionary storing all launched async actions- Dictionary key —
action_id(which you specified), value — reference to executing action - Important:
_async_actionstored separately from_cacheto be available for readiness checks between steps
- System saves reference to executing action in special field
Automatic state tracking:
- When action completes (successfully or with error), system automatically updates its state
- Update happens automatically in background, regardless of scenario execution
- Can check readiness via placeholder
{_async_action.action_id|ready}at any time
Why syntax {_async_action.action_id|ready} is used:
_async_action— system field automatically created when launching async actionsaction_id— ID you specified when launching async action (e.g.,"ai_req_1")ready— modifier checking if action completed (returnsTrueif ready,Falseif still executing)- This allows using readiness check in validation conditions and creating complex scenarios with waits
Example: After launching several async actions system automatically creates structure:
_async_action["ai_req_1"]— reference to first AI request_async_action["data_proc_1"]— reference to data processing_async_action["ai_req_2"]— reference to second AI request
You can check readiness of any via placeholder: {_async_action.ai_req_1|ready}
Checking Readiness:
To check async action readiness use ready and not_ready modifiers in placeholders. This allows creating complex scenarios with waits via validation and transitions.
Modifiers:
{_async_action.action_id|ready}— returnsTrueif action completed,Falseif still executing{_async_action.action_id|not_ready}— returnsTrueif action still executing,Falseif ready
How it works:
- Placeholder
{_async_action.action_id|ready}accesses system field_async_actionand checks state of action with specifiedaction_id readymodifier checks if action completed — returnsTrueif action ready,Falseif still executing- Result can be used in validation conditions to create wait loops and complex logic
Usage example in validation:
step:
- action: "validate"
params:
condition: "{_async_action.ai_req_1|ready} == True" # Check readiness
transition:
- action_result: "failed"
transition_action: "move_steps"
transition_value: -2 # Go back if not ready yetWaiting for Result:
To get async action result use wait_for_action action:
step:
- step_id: 3
action_name: "wait_for_action"
params:
action_id: "ai_req_1"
timeout: 60 # Optional timeout in secondsParameters:
action_id— async action ID to wait for (required)timeout— wait timeout in seconds (optional)
Result:
wait_for_actionreturns main action result AS IS (as if it executed directly)- If main action completed successfully → returns its result with
response_data, which automatically goes to flat_cache(or_cache[_namespace]if_namespacespecified, considering key substitution via_response_key) - If main action completed with error → returns its error
- If wait error (timeout, Future not found) → returns
wait_for_actionerror
Practical Example:
ai_processing:
description: "Processing AI request with loading indicator"
trigger:
- event_type: "message"
event_text: "/ai"
step:
# Step 1: Launch AI request asynchronously
- action: "completion"
async: true
action_id: "ai_req_1"
params:
prompt: "{event_text|regex:/ai\\s+(.+)}"
# Step 2: Send loading message
- action: "send_message"
params:
text: "⏳ Processing request..."
# Step 3: Delay before check
- action: "sleep"
params:
seconds: 1.0 # 1 second delay between checks
# Step 4: Check readiness via validation
# Use placeholder {_async_action.ai_req_1|ready} to check action state
- action: "validate"
params:
condition: "{_async_action.ai_req_1|ready} == True"
transition:
- action_result: "failed"
transition_action: "move_steps"
transition_value: -1 # Go back to delay step (step 3) if not ready yet
# Step 5: Delete loading message (executes only when action ready)
- action: "delete_message"
params:
delete_message_id: "{last_message_id}"
# Step 6: Wait for AI result (get data into context)
- action: "wait_for_action"
params:
action_id: "ai_req_1"
timeout: 60 # 60 second timeout
# response_data goes to flat _cache = {response_completion: "AI answer", prompt_tokens: 50, completion_tokens: 100}
# Step 7: Send AI result
- action: "send_message"
params:
text: "{_cache.response_completion|fallback:Processing error}"What happens in this example:
- Async action launched — system saves reference to it in
_async_action["ai_req_1"] - Loading message sent
- 1 second delay made (prevents 100% CPU)
- Readiness checked via validation — placeholder
{_async_action.ai_req_1|ready}checks if action completed - If not ready — go back to delay step (loop: delay → check → delay → check...)
- If ready — delete loading message and get result
This example shows:
- How to use readiness check via validation to create wait loops
- How to properly organize loop with delay to prevent 100% CPU
- Placeholder
{_async_action.action_id|ready}mechanics in validation conditions
Important Features:
1. Result goes to context only via wait_for_action:
- Main action result goes to scenario
_cacheonly after callingwait_for_action wait_for_actiongets result from completed action and returns it as regular action result- Result automatically saved to flat
_cache(or_cache[_namespace]if_namespacespecified, considering key substitution via_response_key) - Without
wait_for_actionresult remains in system and not available in scenario via placeholders
2. Multiple async actions:
- Can launch multiple async actions simultaneously with different
action_id - Each action tracked independently via its
action_idin_async_action - Can check each action readiness separately:
{_async_action.action_id_1|ready},{_async_action.action_id_2|ready} - Can create complex conditions:
{_async_action.ai_1|ready} and {_async_action.data_1|ready}
3. Timeouts:
- Can specify
timeoutinwait_for_actionto limit wait time - On timeout
wait_for_actionreturnstimeouterror, but scenario execution continues - Main action continues executing in background even after timeout — action reference remains in
_async_actionand can check readiness later
Example with Multiple Async Actions:
parallel_processing:
description: "Parallel processing of multiple requests"
trigger:
- event_type: "message"
event_text: "/process"
step:
# Launch multiple actions in parallel
- action: "completion"
async: true
action_id: "ai_1"
params:
prompt: "Question 1"
# response_data: {"response_completion": "Answer 1", "prompt_tokens": 50, "completion_tokens": 100}
- action: "process_data"
async: true
action_id: "data_proc_1"
params:
data: "{event_text}"
# response_data: {"processed_data": "Processed data", "status": "success"}
# Wait for AI result (saved to _cache)
# IMPORTANT: Don't use readiness check loop without delay - leads to 100% CPU!
# Just wait for result directly via wait_for_action with timeout
- action: "wait_for_action"
params:
action_id: "ai_1"
timeout: 60 # 60 second timeout
# wait_for_action returns main action result AS IS
# response_data from result goes to flat _cache = {response_completion: "Answer 1", prompt_tokens: 50, completion_tokens: 100}
# Wait for data processing result (saved to _cache)
- action: "wait_for_action"
params:
action_id: "data_proc_1"
timeout: 30 # 30 second timeout
_namespace: "data_processing" # Use custom key to save both results
# response_data goes to _cache.data_processing = {processed_data: "Processed data", status: "success"}
# Use results (all data available via _cache)
- action: "send_message"
params:
text: |
AI answer: {_cache.response_completion|fallback:Not received}
Tokens: {_cache.completion_tokens|fallback:0}
Processed data: {_cache.data_processing.processed_data|fallback:Not processed}
Status: {_cache.data_processing.status|fallback:Unknown}ℹ️ Important:
wait_for_actionreturns main action result AS IS (as if it executed directly)- Result processed at
scenario_enginelevel as regularresponse_data - Data goes to flat
_cache(or_cache[_namespace]if_namespacespecified, considering key substitution via_response_key) - If
wait_for_actioncalled twice without_namespace, second result overwrites first (data merged via deep merge) - Solution: Use
_namespaceto save different async action results to different keys - Additionally: Use
_response_keyfor convenient substitution of replaceable field key in async action results
Recommendations:
- Always specify
action_idfor async actions — this is only way to track their status - Add timeouts to
wait_for_actionfor long operations — prevents infinite wait - Handle errors — add
transitionto handleerrorandtimeoutresults inwait_for_action - Use readiness check loops with delay — if need readiness check loop, must add
sleepbefore returning to check - For multiple async actions use
wait_for_actiondirectly — don't create readiness check loops for multiple actions, just wait each viawait_for_actionwith timeout - Use
_namespaceto save different async action results to different_cachekeys