Template syntax
Reference for the {{ }} expressions workflow steps accept, covering trigger data, earlier step output, secrets and the time, plus paths, defaults and types.
Last updated
Templates put live values into the parameters of a workflow step. You write an expression inside double braces, and Anythink replaces it with the value just before the step runs. This page lists every expression, how paths work, the one filter, how types are kept, and what happens when something doesn't resolve. For the bigger picture, see How workflows work.
{{ $anythink.trigger.data.email }}
The basics
- A template is an expression inside
{{ }}. Spaces around it are ignored, so{{$anythink.trigger.id}}and{{ $anythink.trigger.id }}are the same. - One value can hold several templates and plain text:
Order {{ $anythink.trigger.id }} for {{ $anythink.trigger.data.email }}. - An expression can't contain a
}. - When a whole parameter is a single
$anythinkpath with no spaces, the braces are optional:"loop_over": "$anythink.steps.find_orders.data". - Templates are filled in when the step runs, so they always see the latest step outputs. Trigger filters and Run Script steps don't use templates. See Where templates apply.
Expression roots
Every expression starts with $anythink and one of four roots.
| Expression | Value |
|---|---|
$anythink.trigger.id |
The id the job started with. For a record event it's the record's id. For a manual run or an API call it's the entity-id or id you passed. For a schedule, or when none was passed, it's empty. |
$anythink.trigger.data |
The trigger data: the record for an event, the request body for an API call, the --payload for a manual run. Empty for a schedule. |
$anythink.steps.<key> |
The output of an earlier step, by its key |
$anythink.secrets.<name> |
A project secret, decrypted when the step runs. Always text. |
$anythink.now |
The current time in UTC, in ISO 8601 form such as 2026-10-04T15:08:57.288Z |
Only id and data exist under trigger, and $anythink.now takes no path. Any other name, such as $anythink.trigger.entity_name, doesn't resolve. See Missing values. What trigger.data holds for each trigger is in Triggers and scheduling.
Examples
{{ $anythink.trigger.id }} → 42
{{ $anythink.trigger.data.email }} → ada@example.com
{{ $anythink.steps.find_customer.data[0].name }} → Ada
{{ $anythink.secrets.PARTNER_API_KEY }} → the secret's value
{{ $anythink.now }} → 2026-10-04T15:08:57.288Z
A step's output depends on the step. Read Data and Run Script keep their records or results as a list under data, so the first item is data[0]. A Call An API step keeps the response under data. An Integration step returns the provider's reply at the top level, so a field is $anythink.steps.<key>.<field>. Step types lists each step's output, and View Job History shows what a step actually returned.
Paths
A path walks into an object with dots and into a list with square brackets.
| Path | Meaning |
|---|---|
customer.email |
The email field of the customer object |
items[0].sku |
The sku of the first item in the items list. Lists count from zero. |
nested.deep[1].v |
Dots and brackets combine to any depth |
data[*].sku |
The sku of every item, as a list. Works on $anythink.steps paths only. |
{{ $anythink.trigger.data.customer.email }} → ada@example.com
{{ $anythink.trigger.data.items[0].sku }} → A1
{{ $anythink.steps.find_orders.data[*].id }} → [11, 12, 15]
[*] is how a step runs once per item. Send a Push Notification sends once per row when its audience or text uses data[*], as in Push notifications from a workflow. An index past the end of a list, or a [*] on trigger data, doesn't resolve.
Following relationships from trigger data
On an entity event, a path through a relationship loads the related record. If an order has a customer relationship, {{ $anythink.trigger.data.customer.email }} fetches that customer and reads its email. A field named customer_id is followed the same way. This works on trigger data for entity events, not on step outputs.
The current item
Some steps repeat for each item in a list. Inside them, the item's fields are available with no prefix:
| Step | Repeats for |
|---|---|
| Create Data, Upsert Data | Each item in loop_over |
| Update Data with filters | Each matched record |
| Send a Push Notification | Each item of the step referenced with data[*] |
{
"entity_name": "invoices",
"loop_over": "{{ $anythink.steps.due_orders.data }}",
"payload": "{ \"order_id\": \"{{ id }}\", \"amount\": \"{{ total }}\", \"issued_at\": \"{{ $anythink.now }}\" }"
}
Filters
There is one filter, default. Add it after a pipe to supply a value when the path doesn't exist. Write the fallback in double or single quotes.
{{ $anythink.trigger.data.nickname | default: "there" }} → there
{{ $anythink.trigger.data.nickname | default: 'friend' }} → friend
| Rule | Detail |
|---|---|
| When it applies | Only when the path doesn't resolve. A value that exists is used as it is, including an empty one. |
| Result type | Always text, even in a number-shaped field. |
| Other filters | None. ` |
Format or combine values in a Run Script step instead, and read its output in later steps.
Types
In a JSON parameter such as payload, a template keeps its type when it is the entire JSON value:
{ "total": "{{ $anythink.trigger.data.total }}" } → { "total": 129.5 }
{ "tags": "{{ $anythink.trigger.data.tags }}" } → { "tags": ["gift", "express"] }
{ "who": "{{ $anythink.trigger.data.customer }}" } → { "who": { "email": "ada@example.com", "name": "Ada" } }
{ "paid": "{{ $anythink.trigger.data.paid }}" } → { "paid": true }
So numbers stay numbers, booleans stay booleans, and lists and objects stay structured. A secret or the current time is always text.
Inside longer text, the value is written into the text:
| Value | Written as |
|---|---|
| Text and numbers | As they are: Total 129.5 |
| A boolean | True or False, with a capital |
| A list or an object | Compact JSON: ["gift","express"] |
| Null | Nothing |
{ "summary": "Paid: {{ $anythink.trigger.data.paid }}, total {{ $anythink.trigger.data.total }}" }
→ { "summary": "Paid: True, total 129.5" }
In parameters that aren't JSON, such as a url, a header value, a filter value, an email address or an id, the result is plain text.
Note: Write payloads as valid JSON, with each template in quotes. Values are escaped and keep their types only when the payload is valid JSON before the templates are filled in. An unquoted template such as { "total": {{ total }} } isn't valid JSON, so a value containing a quote or a new line can break it.
Note: In a JSON payload that contains a template, numbers are read as 32-bit integers or double-precision decimals. Send ids above 2^53 as text.
Missing and empty values
| Situation | Result |
|---|---|
| The path, step or list item doesn't exist | The template is left as written, braces included |
The path doesn't exist and has default |
The default |
| The value exists but is null | null when the template is the whole JSON value, nothing inside longer text |
$anythink.secrets.<name> names a secret that doesn't exist |
The step fails with "Failed to retrieve secret with key …" |
Hi {{ $anythink.trigger.data.nope }}! → Hi {{ $anythink.trigger.data.nope }}!
Hi {{ $anythink.trigger.data.nope | default: "there" }}! → Hi there!
x{{ $anythink.trigger.data.nothing }}x → xx (nothing is null)
An unresolved template stays in the output, so a typo shows up in the saved record or the step log instead of quietly producing an empty value. Send an Email and Send a Push Notification refuse to send anything that still contains {{.
A Read Data step that finds no records leaves $anythink.steps.<key>.data[0].<field> unresolved, and an unresolved path compares as text in a Condition. To branch on whether anything was found, return a count from Run Script, then compare the count:
return { count: $anythink.steps.find_orders.data.length };
Where templates apply
| Step | Templated | Not templated |
|---|---|---|
| Read Data | Filter values | Entity, fields, sort, limit, filter fields |
| Create Data, Upsert Data | payload, loop_over, rls_user_group_id |
entity_name, match_on |
| Update Data | payload, ids, filter values, rls_user_group_id |
entity_name, rls_mode |
| Delete Data | Filter values | entity_name, filter fields |
| Condition | field and value |
logical_operator |
| Call An API | url, header values, body |
method, header names, results_path |
| Send an Email | to, from, payload, attachment ids |
template_type |
| Send a Push Notification | Title, body, image, click action, category, data values, audience ids | template_type, buttons |
| File Handler | Entity, record id, field, source URL, file name, folder, existing file id | operation, link_mode |
| Integration | Text values in inputs, connection_id, credential_field_path |
provider, operation |
| Run Script | Not templated. Use the $anythink object in the script. |
Trigger filters don't use templates. A filter compares a field with a fixed value.
In a Run Script step
A script doesn't use {{ }}. It gets an $anythink object and reads values with ordinary JavaScript:
| In the script | Value |
|---|---|
$anythink.trigger.data |
The trigger data |
$anythink.trigger.id |
The trigger id |
$anythink.trigger.entity_name |
The entity the trigger concerns, when there is one |
$anythink.steps.<key> |
The output of an earlier step |
var items = $anythink.trigger.data.items;
return items.map(function (item) {
return { sku: item.sku, count: items.length };
});
The script's result is saved as the step's output, so a later step reads the first row as {{ $anythink.steps.<key>.data[0].count }}. Secrets and now aren't on the script's object. Use new Date() for the time, and pass a secret to a Call An API step instead.
Next steps
- Step types: every step, parameter and output
- Triggers and scheduling: what each trigger puts in
trigger.data - Secrets: store the values
$anythink.secretsreads - Test workflows and read job history: read a job to see what a template resolved to
- Workflow recipes: complete workflows that use these expressions