Skip to content
Get started free
Reference

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:

Reference pattern
{{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.

Node prefixes in expressions
PrefixResolves toNode types
trigger:Trigger outputWebhook, chat, schedule, form, table, manual, workflow, and error triggers
mcp:Integration step outputCatalog API and tool steps
agent:AI step outputAgent, Browser Agent, Guardrail, Classify, Generate
core:Core node outputIf / else, Switch, While, Split, Merge, Transform, Wait, Fork, Code, and the other core nodes
table:Table operation outputFind, get, create, update, and delete rows
interface:Interface dataAction data and user input captured by an interface node
note:always nullNotes (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:

  1. Trim
    Leading and trailing whitespace is removed.
  2. Transliterate accents
    Accented characters become their ASCII base letter.
  3. Lowercase
    The whole string is lowercased.
  4. Replace other characters
    Every character that isn't a-z0-9 becomes an underscore.
  5. Collapse and trim underscores
    Repeated underscores collapse to one; leading and trailing underscores are removed.
Label normalization examples
LabelNormalized key
My Labelmy_label
If / elseif_else
Step-123step_123
Entrée IDsentree_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

Paths
{{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:

Split item aliases
Short formFull formMeaning
{{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}}.

  1. Open the Variables tab
    Go to Settings > Credentials & Variables and select Variables.
  2. Add a variable
    Click 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.
  3. Reference it
    Use Copy reference in the list, or type {{$vars.name}} in any node field.
Variable references
{{$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.

Typed vs text
{{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

Type conversion functions
FunctionBehaviorExample
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

Check and fallback functions
FunctionBehaviorExample
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

Math functions
FunctionBehaviorExample
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

Text functions
FunctionBehaviorExample
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

Date and formatting functions
FunctionBehaviorExample
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

JSON functions
FunctionBehaviorExample
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() examples
{{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.

Collection operators
OperatorNameExampleResult
.?[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
Collection examples
{{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:

Fallback syntax by context
ContextFallback syntaxNotes
Interface template{{name|fallback}}A simple name plus one optional pipe default. No functions, not Handlebars.
Workflow node field or conditiondefault(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

Common expression problems
SymptomCauseFix
A reference resolves to empty, with no errorThe 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 emptyA 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 emptymatches 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 downstreamThe 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 emptyNode 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.

Related pages