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, so a trigger can 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.
| 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 |
A calculated number arrives as a decimal, so a budget worked out by multiply renders as 31500.0 on its own. That is fine inside a condition, but wrong in a sentence someone reads. These helpers write the number the way you would write it. Results from the math helpers above and values from a Table column both need this; a plain number straight out of an API response does not.
| Helper | Usage | Result |
|---|---|---|
| money | {{money budget}} | 31,500.00 |
| number | {{number budget}} | 31,500 |
| round | {{round budget}} | 31500 |
| ceil | {{ceil (divide items 3)}} | 2 |
| floor | {{floor (divide items 3)}} | 1 |
money gives two decimal places, number and round give whole numbers, and only number and money add separators. Pick the symbol yourself, since the helpers never add one: ${{money total}} or GHS {{money total}}.
Pass a number to choose the decimal places: {{round price 2}} gives 1234.57, and {{money budget 0}} gives 31,500. Every helper here takes one.
Reach for ceil whenever the answer is counted in whole things. Four items packed three to a box is 1.33 boxes, and round makes that one box with an item left over. ceil makes it two. floor is the other direction, for how many whole units fit.
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, which is 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}}
Limitations
items.0, items[0] - use #each instead{{! comment }} syntax is not supported{{> partialName}} partial includes{{{{raw}}}}...{{{{/raw}}}}{{helper key=value}} named parameterslength, size, count, first, last, empty?, keys and values work on a list; other Ruby methods return nothing.Unlike 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}}