Functions node recipes
The Functions node (transform / function) is the flow’s glue layer: it derives new variables from the run context so downstream nodes can consume them — assembling a message line, computing a boolean gate, or shaping the context before an HTTP Request node’s webhook fires. It runs no customer-hosted code; every assignment is one of three safe inputs: a {{...}} template, a boolean expression on the same grammar Condition nodes use, or a literal.
The Flows overview covers the node in the Data-nodes table; this page is the working reference for writing Functions nodes — the sandbox rules, what flows in and out, five recipes, and how to verify. It is API-only today: define it in definition.nodes and the runtime executes it identically.
1. Node semantics and the sandbox
A Functions node carries a list of assignments. Each assignment computes one value and writes it to one context key:
Two legacy shorthand forms exist on the node itself:
type: "function" is an alias for transform, and a single assignment may be written flat as data.target + data.template/data.expression/data.value instead of the assignments array. Prefer the assignments array — one node computes everything a downstream step needs, which keeps the graph readable.
Sandbox rules
The Functions node never executes arbitrary code. There is noeval, no imports, no clock or network access — everything resolves against the run context and the bounded expression grammar:
- Targets are top-level identifiers.
targetmust start with a letter and contain only letters, digits, and underscores. Leading underscores are rejected so an assignment can never overwrite the runtime’s internal bookkeeping keys, and dot-paths are rejected so assignments write flat keys only. - Reserved identifiers are rejected. Code-like names (
eval,process,require,Function,global,constructor,prototype, and similar) can never be a target or appear in an expression — the assignment is skipped with a reason. - Expressions are the Condition grammar. Identifiers resolve against the context (dot-path reads like
order.totalwork inside expressions), plus string literals, numbers, booleans,null, comparison operators (===,!==,>,<,>=,<=), boolean combinators (&&,||), negation (!), parentheses, and three string methods:.includes(...),.startsWith(...),.endsWith(...). Anything else is rejected. - Bounds. A node runs at most 50 assignments; extra entries are ignored and recorded. Expressions longer than 10,000 characters are rejected.
- Failure is soft. A bad assignment — invalid target, rejected expression, or all three value sources missing — is recorded as
skippedwith a reason in the step trace. The node itself never fails the run, so the rest of the flow keeps routing to the node’s default edge.
2. Inputs and outputs: the contract with adjacent nodes
In. A Functions node reads the whole run context: the trigger payload (posted webhook keys, the contact record, event fields), plus every variable an upstream node wrote. Nothing is scoped away — if a key exists in the context, a{{...}} template or expression can read it.
Out. Each applied assignment sets a flat context key, exactly as if the trigger had supplied it. Downstream nodes consume those keys the normal way: {{target}} placeholders in message bodies and URLs, data.field in Condition nodes, and webhook payloads.
Routing is deliberately simple: the node has no branching handles, so it always takes its default unlabeled edge. The node’s step record shows the per-assignment outcome:
- A skipped assignment writes nothing. Downstream references to its target render as an empty string (templates) or evaluate against an absent key (expressions). Check the
skippedarray in Test mode before publishing. - Assignments compose left to right, across nodes. One Functions node per logical step is clearer than one monolith: derive raw values early, gate mid-flow, and shape the final payload immediately before the HTTP Request node.
3. Recipe 1 — normalize a phone field
Goal: accept a number in any format the caller posted, and give every downstream node one canonical spelling. Templates concatenate placeholders, so strip the source format at the origin (your webhook caller posts digits only) and assemble the display format inside the flow:phone_digits as the identity key you pass to lookup and HTTP nodes; reserve phone_display for message bodies. Add a gate when the source can arrive empty:
has_phone — or use the expression’s boolean directly in a Condition node with data.expression — so a blank number exits instead of sending To: .
4. Recipe 2 — hash or segregate PII before it leaves the flow
Goal: give an external system a stable reference without handing it the contact’s raw identifiers. The webhook node never transmits the usual PII context keys (phone, email, message, transcript, and their aliases — they’re stripped to [REDACTED] before the HTTP Request node sends), but flows often need a tenant-side join key that is not the phone number itself. Derive opaque references in a Functions node and pass those:
contact_ref survives the PII strip — it is a key you own, not a phone or email — so your endpoint reconciles it against your own mapping table. Two rules keep this tenant-controlled:
- Derive, don’t copy. Never write
{{phone}}or{{email}}into a new key for the purpose of shipping it externally. A custom key you mint is not on the strip list, so an alias to a PII value re-exposes exactly what the sanitizer protects. - Record the lawful basis next to the reference. The
consent_basisstamp above is yours to set per your compliance posture; it travels with the payload so your endpoint can enforce it. Keep any hashing or pseudonymization in your own backend — the flow hands over the opaque reference, and lookups against raw identifiers stay in systems you control.
5. Recipe 3 — map external field names to internal ones
Goal: let your storefront or CRM keep its own schema while flow nodes read stable names. A Webhook-triggered flow receives the posted JSON keys verbatim. One Functions node at the head of the flow translates them:Hi {{first_name}}, order {{order_id}}…), and when the upstream system renames a field, you edit one node. Add a presence gate for keys the rest of the flow assumes:
no handle of that Condition to an updateContact or an HTTP Request to your error collector so a malformed POST is visible rather than silently empty.
6. Recipe 4 — date math and windows without code
Goal: express “within 24 hours”, “older than 30 days”, and similar windows as boolean flags. Expression identifiers resolve dot-paths, and comparison operators work on numbers — so timestamp comparisons go through a numeric representation. The expression grammar has no arithmetic operators (no+/-/*), so do the subtraction where the payload originates: have your caller post a precomputed age in milliseconds (or an epoch you compare against a posted now_ms), then compare inside the flow:
- Compute in the caller, compare in the flow. An expression with a
-in it is rejected at evaluation and recorded asskipped; the flow compares keys that are already numeric. If you cannot change the caller, do the arithmetic in a tiny webhook endpoint of your own and POST the derived keys — recipe 3 maps them in. - Comparisons against non-numeric values are always false. A template-derived string like
"86400000"compared with>yieldsfalse, not a string-order surprise — which is why recipes 1–3 chain into this one: map to numeric keys first, then compare. Theorder_shippedassignment above shows the third grammar member: the whitelisted string methods (startsWith,endsWith,includes). - Calendar windows belong to Delay nodes. “Reminder 24h before the appointment” is
delaywithrelativeTo(see the appointment recipe in Five flow recipes); the Functions node answers “is it inside the window?”, the Delay node answers “wait until the window”.
7. Recipe 5 — shape a retry-friendly webhook payload
Goal: make the HTTP Request node’s outbound call idempotent and legible to your receiver. The node POSTs{ "context": <sanitized context> } — so the way to shape the payload is to shape the context. Webhook calls retry automatically on network errors and 5xx responses (4xx does not retry), which means your receiver must dedupe safely. Stamp the keys that make that easy:
- Dedupes on the run identity. The context carries the flow-run id; combined with your endpoint’s idempotency table, a retried POST is a no-op instead of a duplicate ticket.
- Reads one discriminating field.
order_eventis a single flat string your switch/case routes on — no nested parsing of the posted trigger keys. - Versions explicitly.
receiver_schema: "v2"lets you change payload shape without a flag day on the receiver.
8. Verify, then run
1. Validate the definition. Structural validation catches wiring mistakes — an assignment list is checked as node config, so a malformed target shows up here before any run:applied to list every assignment you wrote and skipped to be empty. Any entry in skipped names the target and the reason — fix the assignment, not the downstream node.
3. Trace downstream consumption. In the same run, confirm the consumer nodes see the derived keys: the Condition that gates on within_sla should show result: true/false matching your fixture, and the send node’s rendered body should contain the normalized values. A missing key renders as an empty string, and condition comparisons against absent or non-numeric values yield false — both fail safe, but both are silent unless you look.
4. Simulate before production traffic. Dry-run the flow at real audience size so a gate computed from derived flags shows its funnel impact before any message goes out:
applied vs skipped, then read the consumer node’s record. Three signatures cover nearly every field case: a skipped entry with an invalid-target reason (rename the key), an empty applied value (the upstream key never arrived — check the trigger payload or recipe 3’s mapping), and a result: false everywhere (the comparison operands aren’t numeric — apply recipe 6’s numeric-key discipline).
Next steps
- Flows overview — node taxonomy and edge semantics the Functions node plugs into
- Five flow recipes — full flow definitions that these recipes slot into
- Build your first automation flow — the design walkthrough for recipe-style flows
- Flow Executions — the step-trace reference for reading
applied/skipped - Webhook Events — what to subscribe to when a downstream system answers back