Skip to content

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 params structure (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:

yaml
params:
  text: "Hello, {username}!"
  chat_id: "{user_id}"

With modifiers:

yaml
params:
  text: "Hello, {username|fallback:Guest}!"
  price: "{amount|*0.9|format:currency}"

Literal values in quotes:

yaml
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 working

Nested placeholders:

yaml
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:

yaml
# ❌ 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 works

When 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: \\s for space, \\d for digit
  • Example: "{event_text|regex:^([^\\s]+)|lower}"\\s becomes \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:

yaml
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 ID
  • username — username
  • chat_id — chat ID
  • event_text — message text
  • callback_data — callback button data
  • event_date — event time
  • tenant_id — tenant ID
  • message_id — message ID
  • reply_to_message_id — ID of message being replied to
  • forward_from — forwarding data
  • And any other fields — depending on event type

Tenant configuration:

  • _config — dictionary with tenant configuration (attributes from config.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 ID
  • response_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.

SyntaxDescriptionExampleResult
Objects
object.fieldAccess object field{message.text}Hello world
object.field.subfieldNested 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].fieldArray element field{users[1].name}Bob (second user name)
array[index][index]Nested arrays{matrix[0][1]}2 (matrix element)
Combined
object.array[index].fieldMixed access{event.attachment[0].file_id}file_1

Usage Examples:

yaml
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:

yaml
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 fallback modifier for default value
  • Negative indexes work like in Python: -1 = last element
  • Unlimited nesting depth supported

Modifiers:

ModifierDescriptionExampleResult
Arithmetic Operations
+valueAddition{price|+100}1500 (if price=1400)
-valueSubtraction{price|-50}1350 (if price=1400)
*valueMultiplication{price|*0.9}1260 (if price=1400)
/valueDivision{seconds|/3600}2.5 (if seconds=9000)
%valueModulo{number|%7}3 (if number=10)
Case
upperUppercase{name|upper}JOHN (if name="John")
lowerLowercase{name|lower}john (if name="John")
titleTitle case each word{name|title}John Doe (if name="john doe")
capitalizeFirst letter uppercase{name|capitalize}John (if name="john")
case:typeCase conversion{name|case:upper}JOHN (if name="John")
Lists
tagsConvert to tags{users|tags}@user1 @user2
listBulleted list{items|list}• item1\n• item2
commaComma separated{items|comma}item1, item2
expandExpand array of arrays one level (in arrays only){keyboard|expand}[[a, b], [c]][a, b], [c] (in array)
keysExtract keys from object (dict) to array{storage_values|keys}["group1", "group2"] (if storage_values={"group1": {...}, "group2": {...}})
Transformations
codeWrap value in code block{field|code}<code>value</code>
String Formatting
truncate:lengthTruncate text{text|truncate:50}Very long text...
Date & Time Operations
shift:±intervalShift 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")
secondsConvert time strings to seconds (supports format: Xw Yd Zh Km Ms){duration|seconds}9000 (if duration="2h 30m")
to_dateRound date to day start (00:00:00), returns ISO format{created|to_date}2024-12-25 00:00:00
to_hourRound 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_minuteRound 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_secondRound date to second start (microseconds = 0), returns ISO format{created|to_second}2024-12-25 15:30:45
to_weekRound 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_monthRound 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_yearRound 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:timestampConvert to Unix timestamp{date|format:timestamp}1703512200
format:dateDate format (dd.mm.yyyy){timestamp|format:date}25.12.2024
format:timeTime format (HH:MM){timestamp|format:time}14:30
format:time_fullTime format with seconds (HH:MM:SS){timestamp|format:time_full}14:30:45
format:datetimeFull format (dd.mm.yyyy HH:MM){timestamp|format:datetime}25.12.2024 14:30
format:datetime_fullFull format with seconds (dd.mm.yyyy HH:MM:SS){timestamp|format:datetime_full}25.12.2024 14:30:45
format:pg_datePostgreSQL date format (YYYY-MM-DD){timestamp|format:pg_date}2024-12-25
format:pg_datetimePostgreSQL date-time format (YYYY-MM-DD HH:MM:SS){timestamp|format:pg_datetime}2024-12-25 14:30:45
Number Formatting
format:currencyCurrency formatting{amount|format:currency}1000.00 ₽
format:percentPercentage formatting{value|format:percent}25.5%
format:numberNumber formatting{value|format:number}1234.56
Conditional
equals:valueEquality check (string comparison){status|equals:active}true or false
in_list:itemsCheck inclusion in list{role|in_list:admin,moderator}true or false
trueTruth check (recommended for boolean){is_active|true}true or false
existsCheck value existence (not None and not empty string){field|exists}true or false
is_nullCheck for null (None, empty string or string "null"){field|is_null}true or false
value:resultReturn value when true{status|equals:active|value:Active}Active or null
Utility
fallback:valueDefault value{username|fallback:Guest}John or Guest
lengthCount length{text|length}15 (character count)
lengthCount array length{array|length}3 (element count)
regex:patternExtract by regex{text|regex:(\d+)}123 (first number)
Async Actions
readyCheck async action readiness{_async_action.ai_req_1|ready}true if completed, false if executing
not_readyCheck action still executing{_async_action.ai_req_1|not_ready}true if executing, false if ready

Usage Examples:

yaml
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} == False

Example using exists in conditions:

yaml
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 exist

Important: 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}
  • true modifier not suitable for string values

Practical Examples:

Example 1: Personalized greeting

yaml
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

yaml
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

yaml
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

yaml
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

yaml
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}" minutes

Example 6: Dynamic keyboard with expand modifier

yaml
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 button

How expand works:

  • expand modifier expands array of arrays one level only when used in array
  • In example above {keyboard|expand} in inline array expands [[{...}, {...}], [{...}]] to [{...}, {...}], [{...}]
  • This allows combining dynamically generated keyboard rows with static rows
  • Important: expand works only in array context. In string or dict value remains unchanged

Example without expand (incorrect):

yaml
inline:
  - "{keyboard}"                           # ❌ Gets [[[...]], [...]] - 3 nesting levels!
  - [{"🔙 Back": "main_menu"}]

Example with expand (correct):

yaml
inline:
  - "{keyboard|expand}"                     # ✅ Gets [[...], [...]] - 2 nesting levels
  - [{"🔙 Back": "main_menu"}]

Coreness — Create. Automate. Scale.