powered by

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.

text
{{ $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 $anythink path 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

text
{{ $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.
text
{{ $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[*]
json
{
  "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.

text
{{ $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:

json
{ "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
json
{ "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 …"
text
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:

javascript
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
javascript
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