Skip to content

Expressions

Flow

An expression is a value in double braces, such as {{input.email}}, that Flow fills in while the automation runs. Expressions are rendered with the Jinjava template engine, so they follow Jinja syntax, not JavaScript.

An expression must start with one of the names in the table below. All five are singular: input, var, step, config and loop. The editor flags any other name under Problems and suggests the right one. When the automation runs, an expression that points at nothing is empty.

A value is never an expression: if data that a step reads contains {{ … }} or {% … %} (an email with merge tags, a scraped page, a webhook payload), it is passed on as that text.

Expression What it gives you Where it comes from
{{input.x}} A value the run was started with The trigger, or the body of the API call that started the run.
{{var.x}} A variable A Variable step earlier in the run.
{{step.STEP_ID.output}} An earlier step’s output The step’s name on the canvas, such as FUNCTION_1. Add .field to reach inside it: {{step.FUNCTION_1.output.total}}.
{{config.NAME}} A configuration value for the current environment Looked up by the configuration’s unique name. If the value is an object, add .field.
{{loop.current}} The item the loop is on Only inside a Loop.
{{loop.index}} The position of that item, counting from 0 Only inside a Loop.
{{loop.size}} How many items the loop has Only inside a Loop.

Step names are the type and a number, given when you add the step: FUNCTION_1, CONDITION_1, SUCCESS_2. Use the exact name shown on your canvas.

Inside nested loops, loop always means the innermost loop.

An automation can’t read secrets: {{secret.x}} fails the step with “has no value”. Don’t put API keys or passwords in expressions or variables. Keep credentials in a connection, and let a connector step use it: the credential is applied to the request and never appears in the run.

Change a value with a filter after a | character. Filters can be chained.

{{ input.name | upper }}
{{ input.nickname | default('friend') }}
{{ step.FUNCTION_1.output.items | join(', ') }}
{{ step.FUNCTION_1.output.items | length }}
{{ step.FUNCTION_1.output | tojson }}
Filter What it does
default('text') Uses the text when the value is missing or empty. A name used with default, is defined or x if x else … may be missing: it is never listed as a problem.
join(', ') Joins a list into one piece of text.
length Counts the items in a list or the characters in text.
upper, lower Changes the case of text.
tojson Writes the value as JSON text. Use it to pass a whole object or list into a text field.
jsonpath('$.path') Reads a JSON path from an object, or from JSON text: {{ step.X.output | jsonpath('$.items[0].id') }}.

A filter name that doesn’t exist fails the step with “there is no filter called ‘uppr’”, instead of an empty value.

For most values, dots are simpler: {{step.X.output.customer.id}}.

Expressions aren’t JavaScript. Use the Jinja form instead. One JavaScript habit does work: {{ x || 'none' }} gives x, or 'none' when x is empty, 0 or false. Use {{ x | default('none') }} when only a missing value should be replaced.

Instead of Write
{{ input.items.join(', ') }} {{ input.items | join(', ') }}
{{ input.user?.name }} {{ input.user.name }}. A missing part is already empty.
{{ items.map(i => i.id) }} Do it in a Function step and read the result.

A template that can’t be rendered fails the step, with the reason: a syntax error, a value of the wrong kind ('a' * 3), or a limit. It is never sent empty or cut short. The limits are there to stop runaway templates, and real data stays well inside them:

  • 2 seconds and 200,000 template steps per field (each loop pass counts).
  • 256 MB allocated per field. This counts all the memory the field’s rendering asks for, including memory freed again, so repeating heavy filters over large data in a loop can reach it.
  • 512 KB of output per field. tojson stops at 512 KB of JSON too. For example, 12,000 small rows such as {"id": 123} fit.
  • 1 MB for any one piece of text a filter makes. center, format, replace, indent, wordwrap and join work out the length first. Every other filter that makes text fails when its result is over 1 MB, and so does a filter that would turn a list or object of more than about 1 MB into text.
  • 1 MB of repeated text in a list or dict the template builds. This covers a literal such as [a, a], +, append, extend, insert, update and put. A list that holds the same 1 MB value thousands of times takes little memory but gigabytes once it is printed, so it fails while it is being built. Lists of different values, such as 12,000 rows, aren’t affected.
  • 10,000 items per row from batch.
  • 1,000 numbers from range(). More fails instead of being cut short. Numbers such as 5000.0 or '1e9' count as the numbers they are.
  • 1,000,000 items in a list or object, and brackets or tags nested at most 64 deep.

Templates can’t call Java methods on values, such as 'x'.repeat(5) or input.name.getBytes(). Use a filter instead. A few safe reads still work, such as input.m.get('k') or input.name.startsWith('a'). Lists and dicts the template makes itself can be changed, as in {% set l = [] %}{% do l.append(x) %}. The run’s own data can’t be changed: {% do input.m.put(...) %} fails the step. Java properties such as input.name.bytes read as empty, and so does a Loop step’s position (step.<loop>.cursor), so a template can’t move a loop on. Use {{ loop.current }} for the item. A for-each Loop step keeps a copy of the list it goes through in its step data (loopItems), so {{ step.<loop> }} and the run’s debug view show that list too.

Results from Data Store and Database steps are plain data that you can read with dots. Examples are {{ var.saved.id }}, {{ var.found.total }}, {% for r in var.found.records %} and {{ step.FIND_1.output.found.records[0].id }}. A Postgres json or jsonb column is its parsed value, an array column is a list, and a MongoDB id is its hex text.

Do heavy work, such as building a large file, in a Function step.

Most fields render the whole value as text, filters included: Condition expressions and values, Success Response and Error Response text, connector request bodies, connector path and address values, and Create Task, Data Store and Human Task fields.

Some fields work differently:

  • Plain paths only. These fields read a path such as {{step.X.output.items}} directly, and filters don’t apply: a Loop’s List to loop over, Variable values of type list or object, and Success Response values of type List or Object.

A rendered value is text. An object turns into text that isn’t JSON, so use | tojson when another system needs JSON.

Two places keep the real value when the field is exactly one expression and nothing else: Data Store fields, and AI Agent fields, which receive it as compact JSON.

In connector request bodies, a field that is one whole expression and renders to JSON text is sent as JSON.

Function steps run JavaScript, so you don’t use braces there. The same data is available as plain names:

In an expression In a Function step In a Smart Function step
{{input.x}} input.x sdk.getInput('x')
{{var.x}} vars.x (with an s, because var is a reserved word in JavaScript) sdk.getContext().getVariable('x')
{{step.FUNCTION_1.output.total}} step.FUNCTION_1.output.total sdk.getContext().getStepOutputField('FUNCTION_1', 'total')
{{config.NAME}} config.NAME sdk.getContext().getConfig('NAME')
{{loop.current}} loop.current —

vars exists only in a Function step. A Smart Function step’s code is async function execute(sdk) { … }, and it reads everything through sdk.

In the Function panel, the Insert pill for a variable adds vars.x to your code.

Function steps covers the rest of the code contract.

Step panels that take expressions, such as Function and Condition, show a row of Insert pills for Input, Variables, Step Outputs and Config. In the Function panel, selecting a pill inserts the name into your code. In other fields, type the expression yourself.

To check what an expression produced, run a test and open the Output tab in the test panel.