Router Node
This content is not available in your language yet.
The router node is a multi-way branch driven by a context variable’s value, rather than an evaluated expression. It’s the standard way to dispatch on a classified intent produced by an earlier llm node. For a strictly two-way boolean branch, see Condition Node instead — the two node types use completely different routing mechanisms and are not interchangeable. When the branch depends on tests over values (amount > 1000, tier == "gold") rather than on one variable that already holds the branch name, use a Switch Node: up to 10 expressions in order, with a required default edge.
See Workflow Overview for the node/edge envelope this fits into.
Config fields
Section titled “Config fields”| Field | Type | Required | Description |
|---|---|---|---|
variable | string | Yes | The context variable to inspect: a flat name (intent) or a dotted path into nested object values (analysis.severity). Its current value (converted to a string) is matched against outgoing edge labels. A text value cannot be indexed into: see the section below. |
defaultTarget | string | No (recommended) | The node ID to route to when no outgoing edge’s label matches the variable’s value (or the variable has no value). If omitted and nothing matches, the run fails at this node: see No match and no default below. |
circuitBreaker | { maxConsecutiveDefaultRoutes: number, target: string } | No | A safety net for an incomplete classification taxonomy — see the callout below. Both sub-fields are required if circuitBreaker is present at all: target must be a non-empty node ID, and maxConsecutiveDefaultRoutes must be a positive integer, or the workflow fails validation at save time. |
How routing actually works: the logic lives on edges, not the node
Section titled “How routing actually works: the logic lives on edges, not the node”Unlike condition, a router node does not itself decide where to go — it just reads the value variable points to, converts it to a string, and hands that value off. The workflow runner then looks at the router’s outgoing edges and follows whichever one has a label that string-equals that value. If no edge matches, it falls back to defaultTarget (if set); with no defaultTarget the run fails (next section).
{ "id": "e_billing", "source": "dispatch", "target": "handle_billing", "label": "billing"}This edge is only ever followed when ctx[variable] (whatever router.config.variable points to) equals the string "billing" — plain string equality, no expression evaluation at all.
No match and no default: the run fails
Section titled “No match and no default: the run fails”If the value matches no outgoing edge label, the router has no defaultTarget, and its circuit breaker (if any) has not tripped, the router has nowhere to go. The run then fails at the router node:
-
the router’s own trace row becomes
FAILED(it still carriesroutedToDefault: true, plusfailurePhase: "routing"), and itserrornames the workflow, the node, thevariable, the value it resolved to (shortened to 80 characters) and the edge labels it could have matched, for example:Workflow 12: ROUTER node ‘dispatch’ on variable ‘intent’ resolved to ‘billing’, which matches no outgoing edge label (available: ‘return_request’, ‘exchange_request’, ‘general’), and the node has no defaultTarget — set a defaultTarget or add an edge for this value
-
a live
NodeFailedEventis published for the node, and -
the user gets the platform’s standard “Sorry, I ran into a problem…” reply.
Earlier, this case ended the run quietly with no reply and no error, so the user heard nothing. Saving and publishing still accept a router without a defaultTarget, so set one on every router: it is the branch for anything your labels do not cover. A tripped circuitBreaker.target counts as a destination, so it routes even without a defaultTarget. The success/failure edge labels that tool, action, webhook, llm, sub_agent and set_variable nodes use to catch their own errors do not apply to a router: on a router, success and failure are just route values to match.
Dotted paths read objects, not text
Section titled “Dotted paths read objects, not text”variable can reach into a nested object value with a dot: analysis.severity reads severity inside an object stored in analysis. If the context already has a key whose whole name is analysis.severity, that flat key wins, so older workflows behave exactly as before. A missing or null segment resolves to nothing, like a missing variable.
An llm node always stores its reply as text, even when the text looks like JSON ({"severity": "high"}). A dotted path cannot read inside text, so a router on analysis.severity never matches when analysis came from an llm node, and the run falls through to defaultTarget. Nothing parses the text for you. When this happens the router’s entry in the run trace carries a routerNote that names the problem, for example:
variable
analysis.severity:analysisis text, not an object, soseveritycannot be read from it; add a node that writesseverityas its own variable
The fix is to have a node write the value you route on as its own variable: an llm node with "outputVar": "severity" (ideally with matchAnyOf set to your edge labels), and the router on "variable": "severity". Always set defaultTarget too.
Gotcha: the circuit breaker exists because classification taxonomies are never complete
Section titled “Gotcha: the circuit breaker exists because classification taxonomies are never complete”A classifier llm node is prompted with a fixed list of categories (“respond with ONLY one word: billing, technical, or general”), but real user messages don’t reliably stay inside that list — a model can drift, or a genuinely new category of request can show up that the prompt author never anticipated. Every one of those falls through to defaultTarget. Without a circuit breaker, a session stuck in this state loops through the same default branch turn after turn with no natural signal that anything is wrong — it looks like normal traffic in every dashboard.
circuitBreaker closes that gap. It counts this specific router node’s own history of default-routing outcomes for the current session (persisted per turn, so it works across multiple separate messages, not just within one execution). If the router falls through to defaultTarget maxConsecutiveDefaultRoutes times in a row, the next fall-through is force-routed to circuitBreaker.target instead of defaultTarget — breaking the loop by sending the session somewhere explicit (typically a human-handoff or a “let me connect you with support” branch) rather than letting it default forever.
A few things worth knowing about the mechanism:
- It’s generic — it works for any taxonomy or variable, not a hardcoded category string. Any
routernode can opt in. - The streak resets the moment a real (non-default) route fires — only consecutive default-routes count.
- It’s visible in the execution trace: a
NodeTraceEntryfor a router node withcircuitBreakerconfigured carries aroutedToDefault: true/falseflag on every run. Seeing that flagtrueseveral turns running for the same node, right before the circuit breaker fires, is a reliable sign your classification prompt’s category list needs expanding — the circuit breaker is a safety net, not a fix for the underlying prompt gap.
Worked example
Section titled “Worked example”A returns-and-exchanges assistant classifies intent into three known categories, with a circuit breaker in case the classifier ever drifts into unrecognized territory three turns in a row:
{ "nodes": [ { "id": "classify", "type": "llm", "label": "Classify Intent", "config": { "outputVar": "intent", "promptTemplate": "Classify this message: \"{{userMessage}}\". Respond with ONLY one word: return_request, exchange_request, or general.", "streamOutput": false }, "position": { "x": 0, "y": 0 } }, { "id": "dispatch", "type": "router", "label": "Intent Dispatcher", "config": { "variable": "intent", "defaultTarget": "respond_general", "circuitBreaker": { "maxConsecutiveDefaultRoutes": 3, "target": "handoff_to_human" } }, "position": { "x": 0, "y": 100 } }, { "id": "handle_return", "type": "response", "label": "Handle Return", "config": { "messageTemplate": "I can start a return for you. What's the order number?" }, "position": { "x": -200, "y": 200 } }, { "id": "handle_exchange", "type": "response", "label": "Handle Exchange", "config": { "messageTemplate": "Happy to set up an exchange. What's the order number?" }, "position": { "x": 0, "y": 200 } }, { "id": "respond_general", "type": "response", "label": "Respond to General Query", "config": { "messageTemplate": "I can help with returns and exchanges — what would you like to do?" }, "position": { "x": 200, "y": 200 } }, { "id": "handoff_to_human", "type": "response", "label": "Circuit Breaker Handoff", "config": { "messageTemplate": "I want to make sure you get the right help — connecting you with a support agent now.", "mood": "concerned" } } ], "edges": [ { "id": "e1", "source": "classify", "target": "dispatch" }, { "id": "e2", "source": "dispatch", "target": "handle_return", "label": "return_request" }, { "id": "e3", "source": "dispatch", "target": "handle_exchange", "label": "exchange_request" }, { "id": "e4", "source": "dispatch", "target": "respond_general", "label": "general" } ]}Note there is no edge with label matching handoff_to_human directly off dispatch — the circuit breaker doesn’t need one. It’s a separate force-route mechanism that reads circuitBreaker.target as a node ID directly, bypassing edge-label matching entirely once tripped.
If a user’s message classifies as anything other than return_request, exchange_request, or general three turns in a row (for example, the classifier keeps outputting an unanticipated word), the fourth default fall-through routes straight to handoff_to_human instead of respond_general.
Next steps
Section titled “Next steps”- Triage a support ticket end to end — See the end-to-end example, including when a
switchfits better than arouter. - Condition Node — the two-way, expression-based alternative to
router. - Switch Node — the multi-way, expression-based alternative: tests in order, a required
defaultedge. See Switch, router or condition?. - LLM Node — typically the node that produces the classified value a
routerreads. - Response Node — a common target on every branch of a router.
- Sub-Agent Node — a common target on a classified branch when the branch itself should hand off to a specialist agent, rather than answer inline.
- Workflow Overview — full node/edge reference and edge-label semantics.
- Best Practices — classification-prompt reliability patterns.
- Examples — complete end-to-end workflows.
- Workflows API Reference — mutations for creating and publishing workflows.