Condition node
Route workflows to different branches based on conditional logic and data evaluation
Purpose
The Condition node evaluates a condition and routes the workflow to one of several branches. Each branch names a target activity to jump to, so a condition can send execution forward to different paths, or back to an earlier node to retry a step. Only one branch is chosen per evaluation.
Condition nodes are the decision points in your workflows. They enable dynamic routing based on runtime data, creating adaptive workflows that respond to different scenarios intelligently.
How conditions work
When execution reaches a Condition node:
Evaluate the condition
The condition is evaluated against the variable store and produces a result, a boolean, a label, or a raw value depending on the condition type.
Match a branch
The result is matched against the node's branches. Each branch has a label and a goto (the id of the activity to jump to). If nothing matches and a default_branch is set, that branch is used.
Jump to the target
The workflow jumps to the matched branch's goto activity. Only one branch is taken; the others are not run.
Continue from the target
Execution continues from the target activity. If the target sits earlier in the workflow, the jump forms a loop (see Loops).
Condition nodes enable dynamic, adaptive workflows. The same workflow definition can handle different scenarios by routing through different branches based on runtime data.
Configuration
A Condition node is configured with a condition_type, the fields that type needs, and a list of branches.
Condition types
There are three condition types, set by condition_type:
Compare a single value from the variable store against a fixed value using an operator. This is the default type.
{
"condition_type": "variable",
"variable_path": "classify.is_urgent",
"operator": "equals",
"compare_value": true,
"branches": [
{ "label": "true", "goto": "urgent-handler" },
{ "label": "false", "goto": "normal-handler" }
]
}variable_path is a plain dot-path (not a {{...}} template). See Referencing data.
Branches and goto
Every condition lists one or more branches. Each branch has:
label— the value this branch matches.goto— the id of the activity to jump to when this branch is chosen.value(optional) — an explicit value to match instead of the label.
How the result matches a label depends on the type:
- variable conditions with a boolean result match the
true/falselabels (alsoyes/noand1/0). With thematchoperator, the raw value is compared directly against the labels for multi-way routing. - llm conditions match the label the model returned.
- expression conditions match
true/false.
Default branch
default_branch names the label to fall back to when nothing matches. Without a default and with no match, the node fails, so set a default whenever a condition might not match cleanly (missing data, an unexpected LLM label).
Referencing data in conditions
Conditions read from the variable store using dot-paths, not {{...}} templates.
For a variable condition, variable_path starts with a scope:
input.customer_id // execution input
system.user_id // execution context
classify.intent // output of the activity with id "classify"
approve-email.answers.decision // nested field of an activity output- Activity outputs are stored flat under the activity's id. Reference a field as
activity_id.field, notactivity_id.output.field. - Trigger and user input live in the
inputscope (input.field), execution context in thesystemscope (system.field). - Descend into nested objects with dots, and into list elements by numeric index:
plan.steps.0.action.
For an expression condition, names resolve against input and variables, so an activity output is reached as variables.activity_id.field and execution input as input.field.
For an llm condition, llm_prompt uses {{path}} templates (for example {{input.message}}), which follow the same scope names.
Operator reference
For condition_type: "variable", operator selects how the resolved variable_path value is compared with compare_value:
| Operator | Meaning |
|---|---|
equals / not_equals | Equality, with numeric and boolean coercion |
greater_than / less_than | Numeric comparison |
greater_than_or_equal / less_than_or_equal | Numeric comparison |
contains / not_contains | Substring check (string form) |
starts_with / ends_with | String prefix / suffix |
in_list / not_in_list | Membership in compare_value (a list) |
exists / not_exists | Whether the value is present (non-null) |
match | Return the raw value and route it directly to the branch whose label equals it (multi-way routing) |
There is no regex operator. match routes on the raw value; it does not match a pattern. Equality and numeric operators coerce types where sensible (for example 10 == "10", "5" > 3), but greater_than / less_than on non-numeric values fail the node.
Loops (route back to an earlier node)
A branch whose goto points at an earlier activity forms a loop. Use it to retry a step, refine a search, or keep iterating until a result is good enough, then exit.
Building a retry loop
The pattern is: do work, verify it, then a condition either exits or loops back.
plan → gather → verify → check (condition)
↑ │
└────────── false ──────────────┘ (loop back to plan)
true → _endThe condition routes false back to the first step and true to the end:
{
"id": "check",
"type": "conditional",
"config": {
"condition_type": "variable",
"variable_path": "verify.satisfied",
"operator": "equals",
"compare_value": true,
"branches": [
{ "label": "true", "goto": "_end" },
{ "label": "false", "goto": "plan" }
],
"default_branch": "true",
"max_loops": 2,
"loop_exit_branch": "true"
}
}Each pass can carry state forward. In the canonical example the planner reads the verifier's feedback (verify.gap), which is empty on the first pass and refined on later passes, so each retry searches differently.
See the full runnable example at docs/examples/workflows/07-verify-retry-loop.json in the platform repository: a plan → gather → verify → check loop that retries until the verifier is satisfied.
The bounded-exit branch
Loops are bounded so they can't run forever. Two fields make the exit explicit and graceful:
max_loops— the maximum number of times this condition may take a backward edge. After the budget is spent, the runtime stops looping.loop_exit_branch— the branchlabelto route through oncemax_loopsis reached. This lets a budget-stopped loop finish through a real branch (typically the one that leads to an END node) with normal output formatting, instead of erroring.
loop_exit_branch must be one of the node's branch labels, and setting max_loops requires loop_exit_branch. Both are validated at save time and re-checked at runtime.
The safety cap
Even without max_loops, every backward edge is bounded so a stuck loop can't drain the workflow:
- A conditional with
max_loopsexits through itsloop_exit_branch(or, if that can't be resolved, the terminal END node) once the budget is spent. - Any other backward edge (a raw
nextcycle, or a conditional with no budget) is capped atMAX_LOOPBACKS_PER_EDGE(default 25, env-configurable) traversals of the same edge, then routed through END. - As a final catch-all, the whole workflow is capped at
MAX_WORKFLOW_ITERATIONS(1000) activity steps.
The cap is counted per edge (source activity, goto target). If no END node is reachable when a loop is stopped, the run finishes softly with a success result flagged loop_budget_exceeded.
Loop budgets are counted per edge across the whole run and never reset. Avoid nesting or overlapping loop regions: an inner loop shares the same run-global counters, so its budget can be undercounted.
Examples
Let's look at practical Condition nodes in real workflows.
Example 1: Confidence-based routing
Scenario: Route documents to auto-processing or human review based on a confidence score.
{
"condition_type": "variable",
"variable_path": "document_agent.confidence_score",
"operator": "greater_than",
"compare_value": 0.85,
"branches": [
{ "label": "true", "goto": "auto-process" },
{ "label": "false", "goto": "human-review" }
]
}High-confidence documents route to auto-process; low-confidence documents route to human-review, balancing speed with accuracy.
Example 2: Document type routing
Scenario: Route to different processors based on document type, using match for multi-way routing.
{
"condition_type": "variable",
"variable_path": "document_classifier.document_type",
"operator": "match",
"branches": [
{ "label": "passport", "goto": "passport-flow" },
{ "label": "emirates_id", "goto": "emirates-id-flow" },
{ "label": "invoice", "goto": "invoice-flow" }
],
"default_branch": "invoice"
}The raw document_type value is matched directly against the branch labels, so each document type gets its own path.
Example 3: Intent routing with an LLM
Scenario: Send a message to the right handler based on what it's about.
{
"condition_type": "llm",
"llm_prompt": "Analyze this request and choose a department:\n\n{{input.request}}\n\nOne of: sales, support, billing, general",
"llm_config_id": "llm-uuid",
"branches": [
{ "label": "sales", "goto": "sales-handler" },
{ "label": "support", "goto": "support-handler" },
{ "label": "billing", "goto": "billing-handler" },
{ "label": "general", "goto": "general-handler" }
],
"default_branch": "general"
}Example 4: Multi-factor escalation
Scenario: Escalate only when several factors line up, using an expression.
{
"condition_type": "expression",
"expression": "input.amount > 100000 and variables.transaction.department == 'finance'",
"branches": [
{ "label": "true", "goto": "cfo-approval" },
{ "label": "false", "goto": "standard-processing" }
]
}Example 5: Retry until satisfied
Scenario: Keep refining until a verifier is happy, then exit. See Loops.
{
"condition_type": "variable",
"variable_path": "verify.satisfied",
"operator": "equals",
"compare_value": true,
"branches": [
{ "label": "true", "goto": "_end" },
{ "label": "false", "goto": "plan" }
],
"default_branch": "true",
"max_loops": 3,
"loop_exit_branch": "true"
}Advanced patterns
If-else-if chains
Chain Condition nodes so a false branch leads to the next condition:
check-high (variable: risk.score greater_than 0.9)
├─ true → goto reject
└─ false → goto check-medium
check-medium (variable: risk.score greater_than 0.7)
├─ true → goto human-review
└─ false → goto auto-approveCondition with a fallback
Verify a step succeeded before continuing, and route failures to a human:
after-analysis (variable: analysis.execution_successful equals true)
├─ true → goto continue
└─ false → goto manual-reviewCondition with parallel branches
Route into a Parallel node when multi-agent analysis is needed:
needs-multi-agent (variable: route.requires_multi_agent equals true)
├─ true → goto parallel-analysis
└─ false → goto single-agentBest practices
Complex expressions are hard to debug. Break complex logic into a chain of simple Condition nodes rather than one giant expression.
A missing value can make a comparison fail the node. Use the exists operator (or gate on a value the upstream activity is guaranteed to set) before comparing.
Set default_branch whenever a condition might not match cleanly, especially for llm conditions where the model could return an unexpected label. Without a default, an unmatched result fails the node.
When a branch routes backward, set max_loops and a loop_exit_branch that reaches an END node, so the loop exits gracefully with a real result rather than hitting the global safety cap.
For multi-way (match / llm) routing, name branches after what they mean (passport, complaint) so the workflow reads clearly.
Test with data that triggers each branch, including the default and the loop-exit path. Don't just test the happy path.
Combine Condition nodes with Human Task nodes for approval workflows. Route high-risk items to human review while auto-processing low-risk items. This balances automation with human oversight.