Variables
Every string a Flow node sends, requests or evaluates can contain {{...}} tokens. At run time the engine builds a template context for the step and replaces each token with the value at that path.
Hi {{contact.first_name}}, your order {{vars.api_result.body.order.id}} ships {{vars.eta}}.
Namespaces
| Namespace | What it holds | Examples |
|---|---|---|
contact | The contact the run belongs to | contact.first_name, contact.email, contact.phone, contact.language, contact.custom.loyalty_tier, contact.custom["plan-tier"], contact.tags, contact.lists, contact.methods[0].value |
message | The contact's most recent inbound message | message.text, message.channel_type, message.created_at, message.attachments[0].url |
conversation | The conversation the run is attached to (if any) | conversation.id, conversation.status, conversation.channel_type, conversation.messages[-1].text |
trigger | The trigger_payload the run was started with | trigger.amount_paid, trigger.order.items[0].sku |
vars | Flow variables written by earlier nodes (Collect Input, KB query, Create Tracked Link, API Call, Custom Code) | vars.captured_email, vars.api_result.status, vars.order_total |
flow | Run metadata | flow.id, flow.run_id, flow.name, flow.entry_at, flow.last_channel_used, flow.stop_url |
secrets | Stored flow secrets — API Call node only | secrets.STRIPE_KEY |
{{flow.stop_url}} renders a signed opt-out link (see Examples). Legacy aliases such as {{contact.custom_fields.x}} and {{vars.stop_flow_url}} keep working.
Path grammar
identifier ( "." identifier | "[" integer "]" | "[" quoted-string "]" )*
- Dots walk into objects:
vars.order.customer.email. - Integer indices walk into arrays and may be negative:
vars.items[0],vars.items[-1]. - Quoted keys handle names with dashes or dots:
contact.custom["plan-tier"]. - A numeric segment against an object tries the string key (
vars.map[0]reads"0"). - Whitespace inside the braces is ignored:
{{ vars.x }}.
A path that does not exist renders as an empty string — never an error. Use a Condition node (or {{vars.api_result.ok}}) to guard against missing data.
How values become text
| Value | Renders as |
|---|---|
| string | as-is |
| number | 42, 3.5 |
| boolean | true / false |
null / missing | empty string |
| object / array | compact JSON ({"a":1}) |
Typed rendering in JSON bodies
The API Call node's body_json is a JSON template. A token placed outside quotes keeps the native type of the value it points to; a token inside quotes is interpolated as text.
{
"order_id": {{vars.api_result.body.order.id}},
"items": {{vars.api_result.body.order.items}},
"customer": "{{contact.first_name}} {{contact.last_name}}",
"paid": {{trigger.paid}}
}
renders (for a paid order 42 with two items) as
{"order_id": 42, "items": [{"sku": "X1", "qty": 2}, {"sku": "Y2", "qty": 1}], "customer": "Ada Lovelace", "paid": true}
The template must be valid JSON once every bare token is treated as a string placeholder — the validator rejects a body it cannot parse at save time, so a stray comma is caught in the editor, not at run time.
Secrets
Store credentials once under Flows → Secrets (or PUT /api/v1/flows/secrets/{NAME}) and reference them as {{secrets.NAME}} inside an API Call node's URL, headers, auth block or body. Names are upper-case identifiers (^[A-Z][A-Z0-9_]{0,63}$, lower-case input is upper-cased for you).
- Values are write-only: list and detail responses carry only a masked preview (
sk_…GH). - Publishing fails if a flow references a secret that does not exist; saving shows it as a warning.
- Secrets are resolved right before the request and are never exposed to Custom Code, Send nodes or webhooks.
- Anywhere a resolved value would appear in the step audit (request headers, URL, response body) it is replaced with
***. Standard credential headers (Authorization,Cookie,X-API-Key, ...) are masked regardless of their value.
Where variables come from
| Node | Writes |
|---|---|
| Collect Input | vars.<output_var_name> — the validated answer |
| KB query | vars.<output_var> — retrieved passages |
| Create Tracked Link | vars.<output_var_name> — the short URL |
| API Call | vars.<output_var> — {ok, status, headers, body, duration_ms, error} |
| Custom Code | one vars.<key> per key of the returned object, plus vars.<error_var> on failure |
Every one of these paths can also be tested directly in a Condition or Branch rule — as the rule's field (vars.api_result.body.count, with or without the {{ }} wrapper) or inside its value ("{{vars.threshold}}") — with an optional value_type cast (number, boolean, date, string). See Condition (Branch). secrets.* is the one namespace Conditions never resolve.
GET /api/v1/flows/variables?flow_id=... returns the same catalogue the editor's variable picker uses — see Triggers and the API.
Test context
The editor's Test request / Run script buttons and Flow test runs build the same context as a real run. Without a contact_id they substitute a sample contact ("Sam Sample") and a sample inbound message so {{contact.*}} and {{message.*}} tokens still render; vars.is_test is true in both cases.