Set Variable Node
This content is not available in your language yet.
The set_variable node writes one or more variables into the run’s execution context without calling a language model. Use it to copy or rename a value, give a variable a default, turn text into a real number or boolean, compute a value from others, or build a small JSON object from scalar values. It uses no tokens and no credits.
It replaces a common workaround: an llm node whose only job was to copy a value into its own variable, which cost a model call, added latency and could still answer with prose instead of the value.
See Workflow Overview for the node/edge envelope this fits into.
Config fields
Section titled “Config fields”| Field | Type | Required | Description |
|---|---|---|---|
assignments | array | Yes | 1 to 20 assignments, applied in order. Each one writes one context variable (fields below). |
Each entry in assignments:
| Field | Type | Required | Description |
|---|---|---|---|
variable | string | Yes | The context variable to write. Letters, digits and underscores, starting with a letter, at most 64 characters. Must be unique within the node. Reserved names are refused (below). |
kind | "expression" | "template" | "literal" | Yes | How source is turned into a value (see The three kinds). |
source | string | number | boolean | null | Yes | The expression (at most 500 characters), the template (at most 2,000 characters), or the literal value. |
coerce | "string" | "number" | "boolean" | "json" | No | Converts the result before it is stored (see Converting types). |
default | string | number | boolean | null | No | expression only. The value stored when the expression references a variable this run does not hold. Without it, that case fails the node (and the run, unless the node has a failure edge). |
Reserved names: variable cannot be one of the engine’s own context keys (sessionId, tenantId, agentId, interruptGeneration, clientExternalId, clientTurnId, userMessage, conversationHistory, turnCount, agentTimezone, apiKeyAllowedLlmModels, apiKeyId, session, attachment, channel, attachmentUrl, attachmentMime, attachmentFilename, attachments, attachmentImageUrls, turn), and cannot start with _.
Every rule above is checked when you save or publish: an unknown field value, a duplicate or reserved name, an expression or template with a syntax error, or a source over its size limit is refused with a BAD_USER_INPUT error naming the node and the field (for example assignments[1].source is not a valid expression: ...).
The node has plain outgoing edges: no labels, and every edge is followed, like an llm or response node.
The three kinds
Section titled “The three kinds”expression
Section titled “expression”Evaluated with the same sandboxed expression language as the condition node (==, !=, and, or, not, arithmetic, comparisons). The result must be a number, a string or a boolean.
{ "variable": "total", "kind": "expression", "source": "price * qty" }A missing variable fails the run, unlike a condition. A condition that references a variable this run does not hold evaluates false, because a gate has a safe branch to fall back to. An assignment has no such branch, so it fails the node with an error naming the node, the assignment and the missing name (which fails the run unless the node has a failure edge), unless you give a default:
{ "variable": "attempts", "kind": "expression", "source": "previousAttempts + 1", "default": 1}A variable that exists with the value null counts as present: the default is not used, and an expression whose result is null is refused.
Note that in this expression language || joins two strings ("a" || "b" is "ab"); use or for logic.
The expression itself cannot assign anything: the assignment is the node’s variable field. An = inside source (such as x = 5) or a function definition is refused when you save; see No assignment and no function definitions.
template
Section titled “template”A Handlebars template, rendered against the context exactly like a response node’s messageTemplate. The result is text.
{ "variable": "greeting", "kind": "template", "source": "Hi {{customerName}}, your order is {{orderStatus}}."}Templates are rendered without any escaping: an apostrophe stays an apostrophe and <b> stays <b>. That is right for text a person reads, and wrong for text you later put inside JSON. If the value is going to be parsed as JSON, use coerce: "json" (below), not a plain template.
literal
Section titled “literal”source is stored as it is: a string, a number, a boolean or null.
{ "variable": "currency", "kind": "literal", "source": "MYR" }Converting types (coerce)
Section titled “Converting types (coerce)”coerce | Accepts | Stores | Fails on |
|---|---|---|---|
number | a number, numeric text (" 42 "), true/false (1/0) | a real number | non-numeric text, empty text, null, Infinity |
boolean | true/false, 1/0, and the same as text ("TRUE", "0") | a real boolean | anything else ("yes") |
string | anything scalar | text (null becomes "") | nothing |
json | template or literal only | the parsed object, array or value | text that is not valid JSON |
When default is used, coerce applies to it too.
Numbers stay numbers
Section titled “Numbers stay numbers”A condition compares without converting types, so the text "0" is not the number 0, and a "0" “no match” marker walks straight through a count != 0 gate. Store the value with coerce: "number" and the gate behaves:
{ "variable": "count", "kind": "template", "source": "{{countText}}", "coerce": "number"}Then count > 0 in a later condition compares a real number. Without coerce, a template result is always text.
coerce: "json" and the escaping trap
Section titled “coerce: "json" and the escaping trap”With coerce: "json", a template is rendered with JSON-safe escaping (quotes, newlines and backslashes inside variable values are escaped) and the result is parsed. Write the JSON structure yourself, around scalar variables:
{ "variable": "payload", "kind": "template", "source": "{\"name\": \"{{customerName}}\", \"qty\": {{qty}} }", "coerce": "json"}A customer name like Ann "the" Lee (with a line break) still produces valid JSON, because the value is escaped before it lands between your quotes.
Two traps:
- It cannot re-parse a variable that already holds JSON text.
"source": "{{analysis}}"whereanalysisis the text{"severity":"high"}fails: the quotes inside that value are escaped like any other value, so the result is not JSON. Anllmnode stores its answer as text; to route on one field of it, have thellmnode write that field to its own variable (for example withmatchAnyOf). - Put a space before a closing brace that follows a variable.
{{qty}}}is Handlebars’ triple-brace syntax and does not parse;{{qty}} }does. Saving refuses the broken form.
A parse failure fails the node with an error naming the node and the variable (and the run, unless the node has a failure edge). Error messages never include the value itself.
Order, limits and the run trace
Section titled “Order, limits and the run trace”- Assignments run in order, and each one sees the variables written by the ones before it in the same node.
- A single value larger than 64 KB (as JSON) is refused.
- The node’s row in the run trace (
workflowRuns { nodeTrace }) carriesassigned(the variable names, in order) and anoutputValuemap of{ variable: value }. Like every trace value, credential-shaped strings (tokens in URLs and similar) are redacted on the trace; the run context keeps the real value. - A failed assignment fails the run like any other node error: the trace row is
FAILEDwith the error, aNodeFailedEventis published, and the user gets the standard failure reply. To handle it instead, give the nodesuccessandfailureedges (below). - A failed node writes none of its assignments, including the ones before the one that failed.
Success/failure edges
Section titled “Success/failure edges”Added 2026-10-15. A set_variable node supports the same opt-in routing as a tool node: draw one outgoing edge labeled success and one labeled failure. A failed assignment (a referenced variable that is not set and has no default, a failed coerce, a template or coerce: "json" value that does not render or parse) then takes the failure edge instead of failing the run. The trace row is FAILED_HANDLED, the run completes, and the error message is in the _lastNodeError context variable.
This lets one node both normalise a value and send a missing or malformed one to a branch that asks the user again. A failure edge without a success edge is refused at publish, and a node with neither label behaves exactly as before. See Failure routing for the full rules.
Worked example: replace an llm node that only copies a value
Section titled “Worked example: replace an llm node that only copies a value”Before: a classifier, then a second llm node whose prompt was “respond with only the severity”, just to get the value into the variable the router reads. That second node costs a model call and can answer with a sentence.
After: the classifier writes its label (with matchAnyOf, so the stored value is exactly one of the labels), a set_variable node copies it and sets a numeric retry counter, and the router reads the copy.
{ "nodes": [ { "id": "start", "type": "start", "label": "Start", "config": {}, "position": { "x": 0, "y": 0 } }, { "id": "classify", "type": "llm", "label": "Classify severity", "config": { "promptTemplate": "Classify the urgency of this message as high or low. Respond with one word: \"{{userMessage}}\"", "outputVar": "classification", "matchAnyOf": ["high", "low"], "streamOutput": false }, "position": { "x": 0, "y": 100 } }, { "id": "normalise", "type": "set_variable", "label": "Normalise", "config": { "assignments": [ { "variable": "severity", "kind": "template", "source": "{{classification}}" }, { "variable": "retries", "kind": "expression", "source": "retries + 1", "default": 0, "coerce": "number" } ] }, "position": { "x": 0, "y": 200 } }, { "id": "route", "type": "router", "label": "Route on severity", "config": { "variable": "severity", "defaultTarget": "low" }, "position": { "x": 0, "y": 300 } }, { "id": "high", "type": "response", "label": "Escalate", "config": { "messageTemplate": "Connecting you to an agent now." }, "position": { "x": -150, "y": 400 } }, { "id": "low", "type": "response", "label": "Self-serve", "config": { "messageTemplate": "Here is what you can try first." }, "position": { "x": 150, "y": 400 } } ], "edges": [ { "id": "e1", "source": "start", "target": "classify" }, { "id": "e2", "source": "classify", "target": "normalise" }, { "id": "e3", "source": "normalise", "target": "route" }, { "id": "e4", "source": "route", "target": "high", "label": "high" }, { "id": "e5", "source": "route", "target": "low", "label": "low" } ]}retries starts at 0 on the first turn (the default, since nothing has set it yet), and is a real number, so a later condition like retries >= 3 works without surprises. Context variables are not carried from one turn to the next, so this counter is per turn unless the graph pauses with an await_reply node, which keeps the context across the pause.
Next steps
Section titled “Next steps”- Triage a support ticket end to end — See the end-to-end example:
set_variablenormalises a classifier label and coerces an option id to a number before aswitch. - Condition Node — the same expression language, used as a gate.
- Router Node — route on the variable you just set.
- LLM Node — when a value has to be understood from free text, not just copied.
- Workflow Diagnostics — non-blocking checks on a graph.
- Workflows API Reference — creating, updating and publishing workflows.