Condition Node
This content is not available in your language yet.
The condition node branches execution based on a boolean expression evaluated against the current execution context. It is the only node type in the workflow engine that evaluates an expression directly — everything else routes on a plain value match (see Router Node).
See Workflow Overview for the node/edge envelope this fits into.
Config fields
Section titled “Config fields”| Field | Type | Required | Description |
|---|---|---|---|
expression | string | Yes | The boolean expression to evaluate against the current execution context. Not JavaScript — see the callout below for the real syntax. |
That’s the entire config shape — one field. All of the complexity in this node type is in what expression is allowed to contain.
Critical: expression is expr-eval syntax, not JavaScript
Section titled “Critical: expression is expr-eval syntax, not JavaScript”This is the single most common mistake when authoring a condition node. The expression is parsed and evaluated by expr-eval, a sandboxed expression evaluator — it is not eval(), not new Function(), and it does not understand real JavaScript syntax. It has no access to Node.js globals, require, the filesystem, or any runtime API — only the variables in the execution context are in scope.
Concretely, this means:
- Use
==and!=, not===/!==.expr-evalhas no strict-equality operators at all —===is a syntax error. - Use the words
and,or,not— not&&,||,!. The symbolic logical operators do not exist in this grammar. - Variable names must be bare and flat —
userAge,intent,orderCount. There is no property access:context.userAgeorctx.user.ageare not valid, even though the underlying execution context is technically a nested object at runtime. Every value you want to branch on must already be a flat key in the context — typically something an earlierllmnode wrote via itsoutputVar.
| JavaScript (wrong) | expr-eval (correct) |
|---|---|
userAge === 18 | userAge == 18 |
status !== 'active' | status != 'active' |
isVip && orderCount > 3 | isVip and orderCount > 3 |
!isBlocked | not isBlocked |
context.userAge > 18 | userAge > 18 |
If you write real JavaScript syntax here, you’ll typically get a parse error at evaluation time (for ===, &&, etc.) or a silently-wrong result (property access simply won’t resolve the value you meant). Either way, the fix is the same: flatten the value into its own context variable first (with an llm node, or by referencing whatever earlier node already wrote it as a flat outputVar), then write the expression using only ==/!=/and/or/not against bare names.
No assignment and no function definitions
Section titled “No assignment and no function definitions”Live in production since 2026-10-21. An expression can only read the run’s variables; it can never change them. The single = operator (assignment, as in x = 5) and function definitions (f(a) = a + 1) are not part of the language any more, in a condition, a switch case or a set_variable expression. Such an expression is a parse error (Unknown character "="). A condition is also evaluated on a copy of the run’s variables, so nothing it does can write into them.
==,!=,<=and>=are separate operators and are not affected.- Built-in functions still work, including passing one to
map,filterorfold:max(a, b)andmap(sqrt, [1, 4, 9])are fine. A callback you define inline, such asmap(f(x) = x * 2, items), is refused. - A
switchorset_variableexpression with=is refused when you save the workflow (BAD_USER_INPUT). Aconditionexpression is checked when it runs, so aconditionthat still uses=fails the run at that node, like any other syntax error.
Text helpers
Section titled “Text helpers”In production since 2026-11-05. Expressions can check text without an LLM node. The same helpers work in condition, switch and set_variable expressions.
| Helper | Returns | Example |
|---|---|---|
contains(text, needle) | true if needle appears in text (case-insensitive; an empty needle is false) | contains(message, "refund") |
startsWith(text, prefix) | true if text starts with prefix (case-insensitive) | startsWith(code, "my-") |
endsWith(text, suffix) | true if text ends with suffix (case-insensitive) | endsWith(email, "@example.com") |
lower(text), upper(text) | the text in lower or upper case | lower(city) == "kuala lumpur" |
trim(text) | the text without leading and trailing spaces | trim(answer) == "yes" |
len(text) | the number of characters | len(message) > 280 |
substring(text, start, end) | part of the text; end is optional | substring(code, 0, 3) == "INV" |
Rules that keep them safe and predictable:
- A
nullvalue, a number or an object never causes an error: a number is read as its digits,nulland objects as empty text. - Text longer than 10,000 characters is cut at 10,000 before the helper looks at it.
contains,startsWithandendsWithignore case. For an exact, case-sensitive comparison use==on the value itself.- There is no regular expression helper. Patterns written by an author and run over customer text can be made to run very slowly, so the engine does not offer them. Combine the helpers above, or ask a
llmnode with a structured output for anything fuzzier. - A name that the run does not hold still makes the node evaluate
false(see below); a helper does not change that.
Example condition: contains(message, "refund") and len(message) > 5.
A missing variable evaluates false, not an error
Section titled “A missing variable evaluates false, not an error”Changed 2026-09-21. If an expression references a name the current execution context does not hold — because that turn simply didn’t carry the fact, or a caller omitted it — the node now evaluates false and takes the false edge. Previously this threw, which failed the entire run and gave the end user a generic “Sorry, I ran into a problem” reply for what is almost always an ordinary missing-value case.
false rather than true on purpose: every gate should be written so the guarded, outbound-calling branch is the true one, so an unanswerable condition takes the branch that does nothing.
The run’s context records which names were unresolved, under _conditionUnresolvedVariables, so “why did this take the false branch?” is answerable from workflowRuns(sessionId) { context } without digging through logs:
{ "_conditionResult": false, "_conditionUnresolvedVariables": ["jobPostingId"] }This is deliberately narrow. Only an unresolved variable reference maps to false. A syntax error, a type error, or any other evaluation failure still throws — those are authoring bugs, and a silent false would bury them.
It also means you can write a gate against a value that may or may not be supplied — jobPostingId > 0 covers “absent”, 0, "0", "" and prose in one expression — which is the recommended shape for anything feeding an outbound write, including values arriving via runWorkflowTask’s variables. Note expr-eval has no defined() or ?? operator, so this is the only way to express “only proceed if I actually got this.”
No type coercion: a string '0' is not the number 0
Section titled “No type coercion: a string '0' is not the number 0”expr-eval compares values without coercing their types, so '0' != 0 is true. A “no match” sentinel stored as the string '0' therefore walks straight through a != 0 gate. Store sentinels as numbers (the extractFirstInteger option on an llm node does) and prefer > 0, which also fails closed if the value turns out to be prose.
For a value that arrives from a channel attachment, use the flat keys (attachmentUrl, attachmentMime) rather than a member access such as attachment.url: the flat keys are always present (an empty string when there is no attachment), while the object may be absent.
Edge labels: exactly 'true' and 'false'
Section titled “Edge labels: exactly 'true' and 'false'”A condition node’s outgoing edges are matched against the expression’s evaluated boolean result, converted to the lowercase strings 'true' and 'false'. Both outgoing edges are required, and their label must be exactly 'true' or 'false' — not 'True', not 'yes', not omitted. If the evaluated result has no matching edge, the workflow now throws a clear error at execution time (an earlier engine version stopped advancing silently instead — that failure mode no longer exists, but a mislabeled edge will still stop your workflow at that node).
Worked example
Section titled “Worked example”A generic support-ticket workflow decides whether an issue can be auto-resolved based on a priority score an earlier llm node extracted into priorityScore (a flat numeric context variable):
{ "nodes": [ { "id": "score_priority", "type": "llm", "label": "Score Ticket Priority", "config": { "outputVar": "priorityScore", "promptTemplate": "Rate the urgency of this support message from 1-10, respond with ONLY the number: \"{{userMessage}}\"", "extractFirstInteger": true, "streamOutput": false }, "position": { "x": 0, "y": 0 } }, { "id": "check_urgent", "type": "condition", "label": "Is This Urgent?", "config": { "expression": "priorityScore >= 7" }, "position": { "x": 0, "y": 100 } }, { "id": "escalate", "type": "response", "label": "Escalate to Human", "config": { "messageTemplate": "This looks urgent — I'm connecting you with a support agent right away.", "mood": "concerned" }, "position": { "x": -150, "y": 200 } }, { "id": "auto_handle", "type": "response", "label": "Continue with Bot", "config": { "messageTemplate": "Thanks for the details — let's see what I can do to help.", "mood": "helpful" }, "position": { "x": 150, "y": 200 } } ], "edges": [ { "id": "e1", "source": "score_priority", "target": "check_urgent" }, { "id": "e2", "source": "check_urgent", "target": "escalate", "label": "true" }, { "id": "e3", "source": "check_urgent", "target": "auto_handle", "label": "false" } ]}priorityScore >= 7 is valid expr-eval syntax — a bare variable name, a comparison operator, a number literal. Note >= itself is fine (comparison operators are shared with JavaScript); it’s equality, logical combinators, and property access where the two languages diverge.
A slightly more complex expression combining two flat context variables:
{ "expression": "priorityScore >= 7 and isRepeatCustomer == 1" }(isRepeatCustomer here would need to have been written earlier as a flat 0/1 — or any other expr-eval-comparable scalar — by another node, not as a nested object property.)
Next steps
Section titled “Next steps”- Triage a support ticket end to end — See the end-to-end example: the same expression language in a
switch, on values stored as real numbers. - Switch Node — more than two outcomes: up to 10 expressions tested in order, with a required
defaultedge. - Router Node — the multi-way alternative to
condition, useful once you have more than two outcomes. - LLM Node — the usual source of the flat context variable a
conditionnode evaluates. - Response Node — often sits on both branches of a
condition. - Workflow Overview — full node/edge reference.
- Best Practices — more templating and reliability gotchas.
- Examples — complete end-to-end workflows.
- Workflows API Reference — mutations for creating and publishing workflows.