Expressions & pipes

Anywhere a node takes a template — an email body, a SQL statement, a Sheets cell, an HTTP header — you can drop {{payload.field}} and it is replaced with the value from the payload the node received. Pipes transform that value on the way out.

Referencing a value

A reference is a path into the incoming payload. Dots walk into nested objects, and a key with spaces in it works as-is — no quoting needed.

{{payload.email}}
{{payload.customer.plan}}
{{payload.row.Talla de camiseta}}

To read the output of a different node instead of the one right before you, name it: {{$("Fetch orders").rows}}. It finds the nodes of this run in this workspace. A flow reached through a flow link from another workspace sees its own nodes, not the caller's — only the payload crosses.

Transforming with pipes

Chain transformations with |. Arguments go in parentheses, and each pipe receives the result of the one before it.

{{payload.size | substring(0,4)}}                    → S-20
{{payload.size | replaceAll('-','')}}                → S2026XL
{{payload.size | substring(0,4) | replaceAll('-','')}} → S20
{{payload.name | trim | upper}}                      → ANA
{{payload.items | unique | sort | join('-')}}        → a-b-c
{{payload.items | join(', ')}}                       → a, b, c
⚠Chained methods are not the syntax. {{payload.size.substring(0,4)}} looks like JavaScript, but it is read as a path — there is no field called substring(0,4), so the expression resolves to an empty value. Use the pipe form above. Colon-separated arguments (| substring: 0, 4) are not the syntax either.

What you can pipe through

Text — upper, lower, trim, replace, replaceAll, substring, charAt, padStart, padEnd, startsWith, endsWith, includes, length.

Lists — join, first, last, at, count, reverse, sort, unique, flatten.

Dates — formatDate, formatTime, formatDateTime, toISO, toEpoch, diffDays, diffHours, addDays, addHours.

Numbers and shape — round, currency, percent, add, subtract, multiply, divide, number, string, boolean, json, toJSONString, preview.

json and toJSONString both turn a value into JSON text. json indents it, for reading in a message; toJSONString keeps it on one line, for putting an object inside another JSON body or a query parameter. A string comes out in quotes, because that is its JSON; a field that isn't there comes out empty.

{{payload.order | toJSONString}}   → {"id":7,"items":["a","b"]}
{{payload.order | json}}           → {
                                       "id": 7,
                                       "items": ["a", "b"]
                                     }
{{payload.name | toJSONString}}    → "Ana"
{{payload.ratio | percent}}      → 73%      (payload.ratio is 0.732)
{{payload.ratio | percent(2)}}   → 73.20%
{{payload.total | round(2)}}     → 1500.00
{{payload.total | currency}}     → $1,500.00
ℹpercent takes a fraction and scales it by 100, the same way Angular's PercentPipe does: 0.732 becomes 73%. There are no decimals by default and the result is rounded rather than truncated, so 0.7351 becomes 74%. Ask for decimals with an argument, exactly as you would with round: percent(2) gives 73.20%.
⚠This changed in template engine 2.3.0, and it changes what your existing templates render. percent used to append the symbol without scaling, so a field already stored as a whole percentage — 73.2 — rendered as 73.2%. It now renders as 7320%, with no error and no warning, in every email, cell and message that template feeds. If your value is already a percentage, either divide it by 100 at the source or drop the pipe and write the symbol yourself: {{payload.rate | round(1)}}%.

Doing arithmetic

add, subtract, multiply and divide take one argument — a number, or a path to another field — and return the result. Writing the operator outside the braces does not work: {{payload.in_stock}} - 1 is text, so it renders as 10 - 1, unevaluated.

{{payload.in_stock | subtract(1)}}                       → 9
{{payload.in_stock | subtract(payload.ordered)}}         → 7
{{payload.price | multiply(payload.qty)}}                → 59.97
{{payload.subtotal | add(payload.shipping) | currency}}  → $262.50
{{payload.ordered | divide(payload.in_stock) | percent}} → 30%
{{payload.in_stock | divide(3) | round(2)}}              → 3.33

(in_stock is 10, ordered 3, price 19.99, qty 3, subtotal 250, shipping 12.5)

The field-reference form is the one you usually want: subtract(payload.ordered) is “stock minus what was ordered”, which is a different job from subtracting a constant. A literal is tried first, so subtract(1) never goes looking for a field called 1.

ℹFloating point noise is trimmed for you. In plain JavaScript 0.1 + 0.2 is 0.30000000000000004 and 19.99 - 0.99 is 19.000000000000004. Through these pipes they come out as 0.3 and 19. What is not trimmed is real precision: {{payload.in_stock | divide(3)}} gives 3.33333333333333, so add | round(2) when you are printing the number for a person.
⚠Dividing by zero gives an empty value, not Infinity. {{payload.in_stock | divide(0)}} renders as nothing at all. That is deliberate: the word Infinity printed into a spreadsheet cell breaks every SUM down that column, and in an email it reads as a broken system. An argument that points at a field which does not exist behaves the same way — {{payload.in_stock | subtract(payload.typo)}} is empty rather than an untouched 10, so a calculation that did not happen never looks like one that did. A value that is not a number is empty too, exactly like round and currency.

In HTML emails: raw

In the HTML body of an Email, Gmail, Approval or Outlook node, every expression is escaped: its value comes out as text, never as HTML. To insert HTML you trust, end the expression with | raw.

<p>Hi {{payload.name}},</p>          → the name, as text
<div>{{$("AI").aiResponse | raw}}</div>    → the AI Node's HTML, as HTML
⚠Use raw only for HTML your own flow produced. A value that came from a webhook, a form or an email can carry tags and links that end up in what you send.

When an expression does not resolve

A reference that points at nothing — a misspelled field, a path that stops halfway, a pipe that does not exist — resolves to an empty value. The node still runs. That is deliberate: a missing optional field should not take a whole delivery down.

The cost is that an empty value can look like a working one. A SQL template with an unresolved reference binds '' and the query returns no rows; an email subject renders with a gap. So the engine reports it instead of staying quiet:

Template {{payload.talla | noExiste(1)}} came out empty — unknown filter "noExiste"

That line lands in Monitoring → Logs, tied to the run it came from, and it also shows while you are testing a single node with Run. Warnings are deduplicated per run and capped, so a loop over 500 rows does not write 500 identical lines — when the cap is reached, a final line says so rather than silently dropping the rest.

✦If a value comes out empty and you cannot see why, open Monitoring → Logs and filter by template_issue. The message names the expression and what went wrong with it.