Skip to content

Switch Node

The switch node branches on a list of tests, checked in order. The first case whose expression is true wins, and the run follows the outgoing edge labelled with that case’s label. When no case is true, the run follows the edge labelled default, which every switch must have. It calls no language model: no tokens, no credits.

Use it instead of a chain of condition nodes when one decision has more than two outcomes (“over 1,000”, “gold customer”, “everything else”). Use a router instead when one variable already holds the name of the branch (for example an llm node that classified the message with matchAnyOf).

See Workflow Overview for the node/edge envelope this fits into.

FieldTypeRequiredDescription
casesarrayYes1 to 10 cases, tested in order. Each one is a label and an expression (fields below).

Each entry in cases:

FieldTypeRequiredDescription
labelstringYesThe name of the case and the label of the outgoing edge it routes to. At most 100 characters, no leading or trailing spaces, no line breaks. Unique within the node, ignoring letter case. Reserved labels are refused (below).
expressionstringYesThe test, in the same expression language as a condition node (at most 500 characters). A true result selects the case.

Reserved labels: a case cannot be called default, true, false, success, failure, loop_exhausted, answered, unanswered, approved, rejected, expired, changes_requested, body or done (in any letter case). These are edge labels the engine gives a meaning to, now or in planned node types.

Every rule above is checked when you save or publish: a missing field, more than 10 cases, a reserved or repeated label, or an expression with a syntax error is refused with a BAD_USER_INPUT error naming the node and the field (for example cases[1].expression is not a valid expression: ...). The node also takes the common maxVisits field, like every node.

A switch’s edges must match its cases exactly. Publishing refuses a switch unless:

  • every case label has exactly one outgoing edge with that label (the edge label must match the case label exactly, letter case included);
  • there is exactly one outgoing edge labelled default;
  • there are no other outgoing edges: no unlabelled edge and no edge with a label that is not a case. The one exception is loop_exhausted, which any node may carry.

Two cases (or a case and default) may point at the same node, as separate edges.

[
{ "id": "e1", "source": "size", "target": "large_order", "label": "large" },
{ "id": "e2", "source": "size", "target": "vip", "label": "gold" },
{ "id": "e3", "source": "size", "target": "standard", "label": "default" }
]

Unlike a router’s defaultTarget (a node id in the config), a switch’s default is a real edge in edges.

  • Cases are tested from first to last, and testing stops at the first true one. Put the most specific test first: with amount > 1000 before amount > 100, an order of 5,000 takes the first case.
  • “True” means any truthy result, the same as a condition: true, a non-zero number, non-empty text.
  • A missing variable makes that case false, it does not fail the run. If an expression names a variable this run does not hold, the case is skipped and testing continues with the next one, so a switch whose every case names missing variables takes default. This is the same rule as a condition, and the opposite of a set_variable expression, which fails without a default. A variable that exists with the value null counts as present.
  • Types are not converted. The text "0" is not the number 0, so count == 0 is false when count holds "0". Store numbers as numbers first, for example with a set_variable node and coerce: "number".
  • Any other evaluation error (for example calling something that is not a function) fails the run with an error naming the node, the case number and its label.
  • An expression can only read variables. Assignment (x = 5) and function definitions are refused when you save; see No assignment and no function definitions.
  • Prefer flat variable names, as in a condition. A dotted name such as order.total reads into a variable that holds an object; it cannot read into text, and an llm node stores text unless it has an outputSchema. For a structured llm node, set flatten and test the flat names (request_intent == "cancel_order").

Publishing makes these impossible, but a draft test can run a graph that has not been published. If the edge for the chosen branch is missing (no default edge when nothing matched, or no edge for the case that won), the run fails at the switch: the switch’s own trace row is FAILED: it still shows what the switch chose (matchedCase), carries failurePhase: "routing", and its error names the node, the branch and the edge labels it has, a NodeFailedEvent is published, and the user gets the standard failure reply. A missing case edge never falls back to default, and the run never ends silently.

The switch’s row in the run trace (workflowRuns { nodeTrace }) carries:

FieldWhenMeaning
matchedCasealwaysThe label of the case that won, or default when none did.
matchedCaseIndexwhen a case wonIts position in cases, counting from 0. Absent for default.
switchUnresolvedwhen a case named a missing variable[{ index, label, variables }] for each case skipped because a variable was not set.

Cases after the winner are never tested, so they never appear in switchUnresolved. Which case won is recorded on the trace only; it is not left in the run context for later nodes. If a later node needs it, put a set_variable node on each branch.

UseWhenBranches on
conditionOne yes/no question.One expression; edges true and false.
switchMore than two outcomes, each decided by a test on values (amount > 1000, tier == "gold").Up to 10 expressions in order; edge default required.
routerOne variable already holds the branch name, usually an llm classification with matchAnyOf.The text value of one variable; defaultTarget recommended.

Worked example: route an order by size and customer tier

Section titled “Worked example: route an order by size and customer tier”

Large orders go to a manager, gold customers get a priority reply, everyone else gets the standard reply. The amount arrives as text, so a set_variable node turns it into a number first.

{
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "normalise",
"type": "set_variable",
"label": "Amount as a number",
"config": {
"assignments": [
{
"variable": "amount",
"kind": "template",
"source": "{{amountText}}",
"coerce": "number"
}
]
},
"position": { "x": 0, "y": 100 }
},
{
"id": "size",
"type": "switch",
"label": "Order size and tier",
"config": {
"cases": [
{ "label": "large", "expression": "amount > 1000" },
{ "label": "gold", "expression": "tier == \"gold\"" }
]
},
"position": { "x": 0, "y": 200 }
},
{
"id": "large_order",
"type": "response",
"label": "Manager review",
"config": {
"messageTemplate": "A manager will review this order today."
},
"position": { "x": -200, "y": 300 }
},
{
"id": "vip",
"type": "response",
"label": "Priority reply",
"config": {
"messageTemplate": "Thanks for being a gold customer. Your order is on its way."
},
"position": { "x": 0, "y": 300 }
},
{
"id": "standard",
"type": "response",
"label": "Standard reply",
"config": { "messageTemplate": "Your order is confirmed." },
"position": { "x": 200, "y": 300 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "normalise" },
{ "id": "e2", "source": "normalise", "target": "size" },
{ "id": "e3", "source": "size", "target": "large_order", "label": "large" },
{ "id": "e4", "source": "size", "target": "vip", "label": "gold" },
{ "id": "e5", "source": "size", "target": "standard", "label": "default" }
]
}

A gold customer ordering 5,000 takes large (the first true case). A gold customer ordering 50 takes gold. A turn with no tier at all skips the gold case (it names a missing variable) and, under 1,000, takes default.