Use dynamic variables and templates to create personalized, data-driven conversations.
UniversalChatbot uses Handlebars templating syntax to insert dynamic values into your flow messages, API requests, conditions, and other configurations. This allows you to personalize conversations and work with data collected during the flow.
With templating, you can:
Handlebars expressions are wrapped in double curly braces: {{expression}}
To output the value of a variable, use its name within double curly braces:
Hello, {{name}}! Welcome to our service.
If name is set to "Sarah", this renders as: Hello, Sarah! Welcome to our service.
Access nested properties using dot notation:
Your order total is {{order.total}} with {{order.items.length}} items.
Variables in your templates come from several sources:
Variables created by nodes during flow execution. For example, a Prompt node that stores user input in a variable called user_email:
We'll send confirmation to {{user_email}}
Data returned from API Call nodes is stored in the variable you specify. Access the response body using dot notation:
{{api_response.body.user.name}}
{{api_response.body.account.balance}}
{{api_response.status_code}}
When you reference an array or object directly, it's rendered as JSON:
Your tags: {{user.tags}}
Renders as: Your tags: ["vip","enterprise"]
Array Index Access Not Supported
You cannot access specific array elements by index. Patterns like items.0, items[0], or data.results.0.name will not work. Use #each to iterate over arrays instead.
Variables available in your templates come from different sources:
| Source | Variables | Description |
|---|---|---|
| Flow Data | {{variable_name}} | Variables set by nodes (Prompt, API Call, etc.) |
| Date & Time | {{@today}}, {{@now}} | Current date (YYYY-MM-DD) and timestamp |
| Request Context | {{@request.platform}}, {{@request.user.*}} | Platform identifier and user information |
| Conversation | {{@conversation.*}} | Conversation id, platform, gateway, and transcript (reliable in triggers) |
| Main-Flow Variables | {{@flow.*}} | The main flow's collected variables — lets a trigger read them, e.g. {{@flow.welcome_message.data.body}} |
| Platform Data | {{@intercom.*}} | Platform-specific data (e.g., Intercom contact info and bot admin id) |
| Conversation History | {{@conversation_history}} | Full conversation so far as formatted text; supports {{#each}} for per-message access |
These variables are always available in every flow:
{{@today}}{{@now}}{{@request.platform}}{{@request.user.id}}{{@request.user.name}}{{@request.user.email}}{{@request.user.phone}}{{@conversation_history}}{{#each @conversation_history}}{{this.role}}: {{this.content}}{{/each}}. Use this in AI node prompts to provide conversation context.{{@conversation.id}}@conversation.* namespace is sourced from the conversation record, so it stays available in triggers (e.g. conversation-closed) where the live @request.* context is not present.{{@conversation.platform_conversation_id}}{{@conversation.platform}}http, intercom, telegram, or whatsapp.{{@conversation.gateway}}http_simple, intercom_api, telegram_bot_api, or whatsapp_cloud_api. Useful as a trigger guard, e.g. {{@conversation.gateway}} == "intercom_api".{{@conversation.history}}{{@conversation_history}} (plain text, also supports {{#each}}) — available under the conversation namespace for use in triggers.{{@flow.*}}{{@flow.welcome_message.data.body}}. In the main flow these are also available unprefixed ({{welcome_message.data.body}}); inside a trigger, {{@flow.*}} is the only way to reach them, since the trigger runs with its own separate variables.When deployed to specific platforms, additional variables become available with user and context data from that platform. See the Deployments documentation for platform-specific variable references.
What Works
{{name}}, {{user.email}}{{response.body.data.id}}{{@today}}, {{@user_id}}{{#if}}, {{#unless}}, {{#each}}, {{#with}}{{else}} within conditionals{{#if premium_member}}
Welcome back, valued member!
{{else}}
Consider upgrading to premium for more features.
{{/if}}
{{#unless email_verified}}
Please verify your email to continue.
{{/unless}}
Your items:
{{#each cart.items}}
- {{this.name}}: {{this.price}}
{{/each}}
Special variables available inside #each:
{{@index}} - Current iteration index (0-based){{@first}} - True if first item{{@last}} - True if last itemExample using @last to add commas between items:
{{#each items}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
Renders as: a, b, c
{{#with user.address}}
{{street}}, {{city}} {{zip}}
{{/with}}
UniversalChatbot provides built-in helper functions for common operations. Use them directly or combine with #if for conditional logic.
| Helper | Usage | Output |
|---|---|---|
| upcase | {{upcase name}} | JOHN |
| downcase | {{downcase name}} | john |
| capitalize | {{capitalize name}} | John |
| titleize | {{titleize title}} | The Quick Fox |
| truncate | {{truncate text "20"}} | This is a long te... |
| default | {{default name "Guest"}} | Guest (if empty) |
Use with #if for conditional rendering. Literal values must be quoted.
| Helper | Usage | Description |
|---|---|---|
| eq | {{#if (eq status "active")}} | Equal to |
| ne | {{#if (ne status "banned")}} | Not equal to |
| gt | {{#if (gt count "5")}} | Greater than |
| gte | {{#if (gte count "5")}} | Greater than or equal |
| lt | {{#if (lt count "5")}} | Less than |
| lte | {{#if (lte count "5")}} | Less than or equal |
| Helper | Usage | Description |
|---|---|---|
| and | {{#if (and premium verified)}} | Both must be true |
| or | {{#if (or admin moderator)}} | Either can be true |
| not | {{#if (not banned)}} | Invert the value |
| Helper | Usage | Description |
|---|---|---|
| size | {{size items}} | Returns array length |
| contains | {{#if (contains tags "vip")}} | Check if array has item |
| join | {{join items ", "}} | Join array elements |
| is_empty | {{#if (is_empty items)}} | Check if nil or empty |
| is_present | {{#if (is_present items)}} | Check if has values |
| Helper | Usage | Result |
|---|---|---|
| add | {{add price tax}} | 100 + 10 = 110 |
| subtract | {{subtract total discount}} | 100 - 20 = 80 |
| multiply | {{multiply quantity price}} | 5 * 20 = 100 |
| divide | {{divide total count}} | 100 / 4 = 25 |
Extract parts of a timestamp such as {{@now}} (or a date value from an API response). Both return a number, which you can compare with the comparison helpers or in a Condition node — useful for business-hours logic. The value is read in its own timezone; {{@now}} is UTC.
| Helper | Usage | Output |
|---|---|---|
| hour | {{hour @now}} | Hour of day, 0–23 |
| weekday | {{weekday @now}} | Day of week, 0 (Sun) – 6 (Sat) |
{{json object}} - Outputs value as JSON stringHelpers can be nested using subexpressions (parentheses):
// Uppercase with fallback
Hello {{upcase (default name "guest")}}!
// Check if array has many items
{{#if (gt (size items) "5")}}
You have many items in your cart.
{{/if}}
// Combined conditions
{{#if (and (gt age "18") verified)}}
You're eligible for this offer.
{{/if}}
// Open during business hours (Mon-Fri, 8am-5pm UTC)
{{#if (and (and (gte (weekday @now) "1") (lte (weekday @now) "5")) (and (gte (hour @now) "8") (lt (hour @now) "17")))}}
Our team is online right now.
{{/if}}
Note: Literal values in helper calls must be quoted: use "5" not 5. Variable references don't need quotes.
Limitations
items.0, items[0] - use #each instead{{! comment }} syntax is not supported"5" not 5 in helper calls{{> partialName}} partial includes{{{{raw}}}}...{{{{/raw}}}}{{helper key=value}} named parametersUnlike standard Handlebars, our implementation does not HTML-escape output since we're working with plain text messages, not HTML. This means {{variable}} and {{{variable}}} produce the same result.
Since array index access isn't supported, use these approaches instead:
{{#each api_response.body.items}}
Item: {{this.name}} - {{this.price}}
{{/each}}
Instead of returning nested arrays requiring index access, configure your APIs to return the specific data you need:
response.orders.0.items:// Configure API to return current_order directly
{{current_order.items}}
Within an #each block, you can access the current index using @index:
{{#each products}}
{{@index}}. {{this.name}} - ${{this.price}}
{{/each}}
Hi {{customer_name}}! Thanks for reaching out about your {{inquiry_type}}.
I can see you're contacting us on {{@today}}. Let me help you with that.
https://api.example.com/orders/{{order_id}}/status
{
"customer_id": "{{customer_id}}",
"email": "{{email_address}}",
"requested_at": "{{@now}}",
"platform": "{{@platform}}"
}
{{#if order_status.body.shipped}}
Great news! Your order has shipped and should arrive by {{order_status.body.estimated_delivery}}.
{{else}}
Your order is being prepared and will ship soon.
{{/if}}
Here are the products matching your search:
{{#each search_results.body.products}}
{{@index}}. {{this.name}} - ${{this.price}}
{{/each}}