Expressions & variables
Workflows and interfaces share one template syntax: {{ ... }}. Inside the braces you reference another node's output, navigate nested paths and array indexes, call built-in functions, use operators, and read workspace variables.
The basic pattern
Every node's output is reachable with the same shape:
{{prefix:label.output.field}}prefix identifies the kind of node, label is the node's name (normalized, see below), and .output. leads into what the node produced. A reference without .output. (for example {{trigger:start.items}}) still resolves for backward compatibility, but .output. is the form to write.
Node prefixes
A node key carries one of seven prefixes. Six resolve to data; note: always resolves to null because a note produces no output.
| Prefix | Resolves to | Node types |
|---|---|---|
trigger: | Trigger output | Webhook, chat, schedule, form, table, manual, workflow, and error triggers |
mcp: | Integration step output | Catalog API and tool steps |
agent: | AI step output | Agent, Browser Agent, Guardrail, Classify, Generate |
core: | Core node output | If / else, Switch, While, Split, Merge, Transform, Wait, Fork, Code, and the other core nodes |
table: | Table operation output | Find, get, create, update, and delete rows |
interface: | Interface data | Action data and user input captured by an interface node |
note: | always null | Notes (documentation only, never executed) |
See the Node reference for what each node type puts under .output..
Label normalization
A node's label is turned into the key used in {{prefix:label...}} by one rule, applied in order:
- TrimLeading and trailing whitespace is removed.
- Transliterate accentsAccented characters become their ASCII base letter.
- LowercaseThe whole string is lowercased.
- Replace other charactersEvery character that isn't
a-z0-9becomes an underscore. - Collapse and trim underscoresRepeated underscores collapse to one; leading and trailing underscores are removed.
| Label | Normalized key |
|---|---|
| My Label | my_label |
| If / else | if_else |
| Step-123 | step_123 |
| Entrée IDs | entree_ids |
So a Core node labelled “Check Status” is core:check_status, and an integration step labelled “API Call” is mcp:api_call. A reference written with the human-readable label, such as {{mcp:Fetch Profile.output.data}}, is rewritten to {{mcp:fetch_profile.output.data}} before evaluation.
Nested paths and array indexing
{{mcp:fetch.output.user.name}}
{{mcp:fetch.output.items[0]}}
{{core:split.output.edges[0].node.text}}Dotted paths walk nested objects; key[N] indexes into a list. An out-of-range index, or indexing something that isn't a list, resolves to null. .length works as a property on lists and objects, and on a list of objects list.field returns the list of that field's values.
Split item aliases
Inside a Split body, short forms give you the current item and its position:
| Short form | Full form | Meaning |
|---|---|---|
{{item}} | {{core:split.output.current_item}} | The current item |
{{item.field}} | {{core:split.output.current_item.field}} | A field of the current item |
{{index}} | {{core:split.output.current_index}} | 0-based position in the list |
| none | {{core:split.output.items}} | The full list being split (also readable after the split) |
item is an alias of current_item, and index is an alias of current_index: {{current_item.field}} and {{item.field}} resolve identically. These values exist only inside the split body. A While node exposes its 0-based pass counter as {{core:loop.output.iteration}}.
Workspace variables ($vars)
Variables are reusable values (URLs, IDs, settings) shared by every workflow of a workspace. You manage them in Settings > Credentials & Variables, on the Variables tab, and reference them with {{$vars.name}} or the alias {{vars:name}}.
- Open the Variables tabGo to Settings > Credentials & Variables and select Variables.
- Add a variableClick Add variable. Enter a Name (letters, digits, and underscores, not starting with a digit, at most 64 characters), pick a Type (Text, Number, Boolean, or JSON), and enter the Value.
- Reference itUse Copy reference in the list, or type
{{$vars.name}}in any node field.
{{$vars.apiUrl}}
{{vars:config}}
{{$vars.config.api.url}}Variables belong to the workspace you are working in (your personal space or an organization), so every workflow of that workspace can read them. A JSON variable can be navigated like any other object. Values are stored encrypted and fetched once when a run starts; a run paused for more than 10 minutes may see updated values when it resumes. Deleting a variable makes references to it resolve as empty. Your plan may limit how many variables you can create.
Whole value vs text
When a field's whole value is exactly one {{ ... }} block, the result keeps its type: a number stays a number, a list stays a list, an object stays an object. This is the only way to pass a real object or list into a field that expects one.
When the expression is embedded in text, or there are several blocks, each block is resolved and the result is a string. In that case a resolved object or list is written as JSON ({"a":1}), and a block that resolves to null becomes an empty string. In a condition, a null result counts as false.
{{mcp:fetch.output.items}} → a list
Found {{size(mcp:fetch.output.items)}} items → a string, "Found 3 items"Built-in functions
46 function names are available as plain calls inside {{ }}, for example {{uppercase(mcp:fetch.output.name)}}. Two are aliases (len of size, fromjson of json). Names are case-insensitive (formatDate and formatdate are the same function). matches is not among them: it is an operator (see below).
Type conversion
| Function | Behavior | Example |
|---|---|---|
int(value) | Converts to a whole number; a decimal string is truncated. 0 for null or unparseable input. | int('42.9') → 42 |
long(value) | Like int, for large whole numbers. | long('9000000000') → 9000000000 |
double(value) | Converts to a decimal number. 0.0 for null or unparseable input. | double('3.5') → 3.5 |
float(value) | Like double, lower precision. | float('3.5') → 3.5 |
string(value) | Converts to text. Empty string for null. | string(42) → '42' |
bool(value) | true for true, a non-zero number, or the text true, 1, yes, on (any case). false for null. | bool('yes') → true |
Checks and fallbacks
| Function | Behavior | Example |
|---|---|---|
size(value) | Length of a text, list, object, or array. 0 for null and for anything else (numbers, booleans). | size('abc') → 3 |
len(value) | Alias of size. | len(mcp:fetch.output.items) |
length(value) | Like size, but a number or boolean is measured as text. | length(12345) → 5 |
typeof(value) | One of null, string, int, double, bool, list, map, array. | typeof(3.5) → 'double' |
default(value, fallback) | fallback when value is null, an empty string, an empty list, or an empty object. | default(trigger:form.output.form_data.city, 'Paris') |
ifempty(value, fallback) | fallback only when value is null or an empty string (an empty list or object is kept). | ifempty(x, 'n/a') |
coalesce(a, b, ...) | Any number of arguments. Returns the first one that is not null and not an empty string (an empty list or object counts as a value). | coalesce(core:poll_2.output.data.url, core:poll_1.output.data.url) |
isnull(value) | true when value is null. | isnull(x) |
isempty(value) | true when value is null, an empty string, list, object, or array. | isempty(mcp:fetch.output.items) |
coalesce is the tool for polling: read the value from every attempt, newest first, and you get the latest one that is not empty.
Math
| Function | Behavior | Example |
|---|---|---|
abs(value) | Absolute value, as a decimal number. | abs(-3) → 3.0 |
round(value, decimals) | Rounds to decimals places; decimals 0 or less returns a whole number. | round(19.956, 2) → 19.96 |
floor(value) | Rounds down to a whole number. | floor(2.7) → 2 |
ceil(value) | Rounds up to a whole number. | ceil(2.1) → 3 |
min(a, b) | Smaller of two values, as a decimal number. | min(1, 2) → 1.0 |
max(a, b) | Larger of two values, as a decimal number. | max(1, 2) → 2.0 |
pow(base, exponent) | base raised to exponent, as a decimal number. | pow(2, 10) → 1024.0 |
sqrt(value) | Square root, as a decimal number. | sqrt(9) → 3.0 |
Text
| Function | Behavior | Example |
|---|---|---|
uppercase(value) | Upper-cases the text. Empty string for null. | uppercase('ab') → 'AB' |
lowercase(value) | Lower-cases the text. Empty string for null. | lowercase('AB') → 'ab' |
capitalize(value) | Upper-cases the first character and lower-cases all the rest. | capitalize('hELLO wORLD') → 'Hello world' |
trim(value) | Removes leading and trailing whitespace. | trim(' a ') → 'a' |
truncate(value, max, suffix) | Cuts the text to max characters, suffix included; suffix is ... when null. | truncate('Hello world', 8, null) → 'Hello...' |
padleft(value, length, pad) | Pads on the left to length characters; pad is a space when null. | padleft(7, 3, '0') → '007' |
padright(value, length, pad) | Pads on the right to length characters; pad is a space when null. | padright('ab', 4, '.') → 'ab..' |
replace(value, search, replacement) | Replaces every occurrence of search (plain text, not a pattern). | replace('a-b-c', '-', '/') → 'a/b/c' |
substring(value, start, end) | Characters from start (inclusive) to end (exclusive); out-of-range indexes are clamped; end null means the end of the text. | substring('abcdef', 1, 3) → 'bc' |
split(value, delimiter) | Splits into a list on a plain-text delimiter; , when null. | split('a,b,c', ',') → [a, b, c] |
join(list, delimiter) | Joins a list into text; , when null. | join(mcp:fetch.output.tags, ', ') |
startswith(value, prefix) | true when the text starts with prefix. | startswith('invoice-12', 'invoice') → true |
endswith(value, suffix) | true when the text ends with suffix. | endswith('report.pdf', '.pdf') → true |
contains(value, search) | On a list: true when it holds that exact element. Otherwise: true when the text contains search. | contains('hello', 'ell') → true |
Dates and formatting
| Function | Behavior | Example |
|---|---|---|
now() | Current UTC date and time as ISO text, to the second, without a time zone suffix. When the seconds are zero they are left out (2026-09-24T14:05). | now() → '2026-09-24T14:05:09' |
today() | Current UTC date as ISO text. | today() → '2026-09-24' |
formatdate(value, pattern) | Formats a date with a Java date pattern (yyyy-MM-dd when null). See below for accepted values. | formatdate(now(), 'dd/MM/yyyy HH:mm') |
formatnumber(value, decimals) | Formats with exactly decimals decimal places (2 when null), with grouping separators. 0 for null. | formatnumber(1234.5, 2) |
formatcurrency(value, code) | Formats as an amount in the ISO currency code (EUR when null). An unknown code gives the number followed by the code. | formatcurrency(19.9, 'USD') |
formatdate accepts a number of epoch milliseconds (formatted in UTC), an ISO date (2026-09-24), or an ISO date and time without a time zone (2026-09-24T14:05:09). Any other value, including an ISO timestamp that ends in Z or an offset such as +02:00, is returned unchanged, without an error. In the pattern, DD, YYYY, and YY are read as dd, yyyy, and yy. The separators used by formatnumber and formatcurrency follow the server's settings, not your language.
JSON
| Function | Behavior | Example |
|---|---|---|
json(value) | Parses JSON text into a typed object, list, number, boolean, or string. An already-typed value is returned unchanged; blank text gives null. | json(mcp:fetch.output.body) |
fromjson(value) | Alias of json. | fromjson('[1,2]') |
tojson(value) | Writes a value as compact JSON text; null gives the text null. | tojson(mcp:fetch.output.user) |
A typical use of json() is turning a text field into typed data for a parameter that expects an object. Parsing is limited to 256 KB per string value, 64 levels of nesting, and 2 MB per document. Unlike every other error, malformed JSON is reported in the inspector instead of resolving to empty.
{{json(mcp:fetch.output.body)}}
{{json('{"responseModalities":["IMAGE"]}')}}Operators and collection filters
Expressions use the Spring Expression Language (SpEL) operators: arithmetic + - * / %, comparison == != < > <= >=, logical && || ! (or and, or, not), the ternary cond ? a : b, and matches for regular expressions.
| Operator | Name | Example | Result |
|---|---|---|---|
.?[predicate] | Selection (filter) | {{users.?[age >= 18]}} | A filtered list |
.![expr] | Projection (map) | {{users.![name]}} | A mapped list |
.^[predicate] | First match | {{headers.^[name == 'From'].value}} | A single element |
.$[predicate] | Last match | {{items.$[type == 'a'].id}} | A single element |
{{nums.?[#this > 10]}}
{{users.?[age >= 18 and active == true]}}
{{users.?[age >= 18].![name]}}
{{users.?[age >= 18].size()}}Filters and projections chain, and a filter with no match returns an empty list, never null. Only a safe set of methods can be called on values: on text length, isEmpty, isBlank, trim, strip, toLowerCase, toUpperCase, contains, startsWith, endsWith, substring, replace, replaceAll, replaceFirst, matches, split, indexOf, lastIndexOf, charAt, toString; on lists size, isEmpty, contains, get; on objects get, getOrDefault, containsKey, containsValue, size, isEmpty. Type references, constructors, and other method calls are blocked.
Two different defaults: interface pipe vs default()
Interfaces and workflow fields use the same {{ }} braces but two different fallback mechanisms:
| Context | Fallback syntax | Notes |
|---|---|---|
| Interface template | {{name|fallback}} | A simple name plus one optional pipe default. No functions, not Handlebars. |
| Workflow node field or condition | default(var, 'x') | There is no pipe operator: name|fallback here resolves to empty. |
A file resolved into an interface template is rendered as its storage path, so <img src="{{photo}}"> gets a usable address directly. See Interfaces & apps for the full templating model.
Common pitfalls
The Code node's extra result level
A Code node's returned value is exposed downstream as {{core:<label>.output.result.<field>}}: the engine wraps what you return under an extra result key. A mapping written as {"result":"{{core:normalize.output}}"} produces a double result.result and reads as empty. Map past the wrapper: {"result":"{{core:normalize.output.result}}"}.
Sub-workflow outputs use the bare child key
A Sub-Workflow node returns the child's node outputs keyed by the child node's key without its prefix: {{core:call_child.output.result.step_result.output.transformed.url}}, not result.core:step_result. The prefixed form resolves to empty without an error.
Legacy forms
For backward compatibility the resolver still recognizes older dotted forms (steps., triggers., data., current_item., mcps.). Write the prefixed form.
The expression editor
Fields that accept expressions open an editor with categorized autocomplete for node outputs and functions, and a syntax guide with examples. The interface mapping inspector has its own function-help popover for the functions most useful when wiring data into a page.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A reference resolves to empty, with no error | The path does not exist at that point: a misspelled field, an index past the end of a list, or a node that has not run before this step on the same path (a parallel branch before the merge). | Pick the field from the expression editor autocomplete, or copy the exact path from the source node output of a past run. Read parallel branches only after they merge. |
| A function call comes out empty | A function error (wrong argument type, bad date, incompatible comparison) resolves to null silently. Only json() reports malformed input. | Test the function on a literal value first, then swap the reference back in. Convert types explicitly with int(), double(), or string(). |
A number comparison is wrong ('100' > '9' is false) | Values that arrive as text (form fields, CSV columns, many API fields) are compared as text, character by character. | Convert both sides first: {{int(trigger:form.output.form_data.amount) > 9}}. |
matches(value, pattern) resolves to empty | matches is an operator, not a function, so the function form does not parse. | Write value matches 'pattern'. The pattern must match the whole text. |
| A Code node output reads as empty downstream | The value the Code node returns is wrapped under an extra result key. | Read {{core:<label>.output.result.<field>}}, and map output.result, not output. |
| A reference to a node with spaces or accents in its label resolves to empty | Node keys are stored normalized, so core:Check Status does not match the stored key. | Write the normalized label, for example core:check_status. See Label normalization above. |