> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Functions node recipes: normalize, hash, map, compute, and shape payloads in a flow

> Reference and recipes for the Functions (transform) node — the safe glue-logic node that derives flow variables from templates, boolean expressions, and literals, with the sandbox rules, the input/output contract, and five copy-ready assignment sets.

# 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](/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:

```json theme={null}
{
  "id": "derive_flags",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "is_vip", "expression": "tier === 'gold' || tier === 'platinum'" },
      { "target": "greeting", "template": "Hi {{first_name}}, your {{tier}} rewards:" },
      { "target": "source_flow", "value": "loyalty_digest" }
    ]
  }
}
```

Each assignment resolves to a value from exactly one source, in this priority order:

| Assignment field | What it does                                                                                                                                            |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expression`     | A boolean expression evaluated against the run context — same grammar as a Condition node's `data.expression`. The result is always `true` or `false`.  |
| `template`       | A string with `{{variable}}` placeholders interpolated against the context. A placeholder for a missing key renders as an empty string, never an error. |
| `value`          | A literal assigned as-is. Use it to stamp constants a downstream node needs (an id, a mode flag, a fixed label).                                        |

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 no `eval`, no imports, no clock or network access — everything resolves against the run context and the bounded expression grammar:

* **Targets are top-level identifiers.** `target` must 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.total` work 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 `skipped` with 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:

```json theme={null}
{
  "output": {
    "applied": [
      { "target": "greeting", "value": "Hi Ana, your gold rewards:" }
    ],
    "skipped": [
      { "target": "_internal", "reason": "Invalid target name — must be a top-level identifier (letter, then letters/digits/underscore; no leading underscore)" }
    ]
  }
}
```

Two contract details worth building on:

* **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 `skipped` array 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:

```json theme={null}
{
  "id": "normalize_phone",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "phone_digits", "template": "{{phone}}" },
      { "target": "phone_display", "template": "+{{phone}}" }
    ]
  }
}
```

Keep `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:

```json theme={null}
{ "target": "has_phone", "expression": "phone !== null && phone !== ''" }
```

Then draw the Condition edge on `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:

```json theme={null}
{
  "id": "segregate_pii",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "contact_ref", "template": "acct-{{account_id}}-c-{{contact_id}}" },
      { "target": "consent_basis", "value": "contract" }
    ]
  }
}
```

`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:

1. **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.
2. **Record the lawful basis next to the reference.** The `consent_basis` stamp 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:

```json theme={null}
{
  "id": "map_inbound_fields",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "first_name", "template": "{{customer_firstName}}" },
      { "target": "order_id", "template": "{{orderNumber}}" },
      { "target": "order_total_cents", "template": "{{total_cents}}" }
    ]
  }
}
```

Downstream nodes stay source-agnostic (`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:

```json theme={null}
{ "target": "mapped_ok", "expression": "order_id !== '' && order_total_cents !== ''" }
```

Route the `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:

```json theme={null}
{
  "id": "compute_windows",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "within_sla", "expression": "age_ms < 86400000" },
      { "target": "is_stale", "expression": "idle_ms > 2592000000" },
      { "target": "order_shipped", "expression": "order_status.startsWith('ship')" }
    ]
  }
}
```

Three notes that keep this honest:

* **Compute in the caller, compare in the flow.** An expression with a `-` in it is rejected at evaluation and recorded as `skipped`; 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 `>` yields `false`, not a string-order surprise — which is why recipes 1–3 chain into this one: map to numeric keys first, then compare. The `order_shipped` assignment 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 `delay` with `relativeTo` (see the appointment recipe in [Five flow recipes](/guides/flows-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:

```json theme={null}
{
  "id": "shape_payload",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "order_event", "template": "order.{{event_stage}}" },
      { "target": "attempt_summary", "template": "run={{_flow_execution_id}} node=notify_receiver" },
      { "target": "receiver_schema", "value": "v2" }
    ]
  },
  "edges_note": "follow with the HTTP Request node"
}
```

Your receiver then:

* **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_event` is 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.

Keep secrets out of the payload entirely — the node sends the same sanitized context plus your derived keys, and nothing more. If the receiver needs a credential, that belongs in the endpoint verification you already run on your side (see [Webhook Security](/webhooks/security)), not in flow variables.

## 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:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/flows/flow_abc123/validate \
  -H "X-API-Key: dv_live_sk_..."
```

**2. Walk it in Test mode.** Click **Test** in the builder toolbar (or start a manual run against a fixture contact) and read the Functions node's step record. You want `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:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/flows/flow_abc123/simulate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"audience_size": 10000, "default_country": "US"}'
```

If the simulator shows an implausible drop at the gate, the expression — not the audience — is the first suspect.

**Run-book for production.** When a live run misbehaves, the [Executions API](/flows/executions) is the sequence: find the Functions node's step record, read `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](/flows/overview) — node taxonomy and edge semantics the Functions node plugs into
* [Five flow recipes](/guides/flows-recipes) — full flow definitions that these recipes slot into
* [Build your first automation flow](/guides/build-first-flow) — the design walkthrough for recipe-style flows
* [Flow Executions](/flows/executions) — the step-trace reference for reading `applied` / `skipped`
* [Webhook Events](/webhooks/events) — what to subscribe to when a downstream system answers back
