Skip to content

Conditional Node

The Conditional node picks exactly one of its outgoing branches based on a runtime condition, activating that branch’s target and marking every other branch target as skipped.

Use Conditional whenever the path through the workflow depends on runtime data — a count, a score, a classification, a success/error signal. For fixed forks where all branches should run in parallel, use straight-line edges instead.

Put the decision in the Conditional, not in the node before it. A Script node that outputs {"new_items": 3} and a Conditional that branches on new_items ≥ 1 lets a reader see the rule on the canvas. A Script node that decides for itself and outputs {"skip": false} hides the rule inside its source.

FieldDescription
condition_typeHow the condition is evaluated: rules, field, regex or llm. See below.
condition_rulesFor condition_type: rules — an ordered list of { when, label }.
condition_expressionFor every other type — the dot path, pattern or prompt.
branchesMap from label to target node ID. The label produced by evaluation selects which target activates.
default_branch (optional)Target node ID used when evaluation produces no usable label.
  • rules (recommended) — structured comparisons, evaluated top to bottom. The condition appears on the canvas, and every field is checked against the incoming edge’s contract when the workflow is saved.
  • field — a single dot path such as .category or .meta.route. Its value is turned into text and used as the branch label. This is not an expression language: pipes, comparisons and if/then/else are not evaluated and are refused when the workflow is saved.
  • regex — status:\s*(?<label>\w+), matched against the incoming data. The named capture group label is used.
  • llm — a classification prompt. The LLM is instructed to output exactly one of the branch labels. Always pair this with a default_branch.

jq appears in older workflows as the former name of field. It behaves identically — it only ever read dot paths.

Each rule is a list of conditions (when) that must all hold, plus the label to take when they do. The first matching rule wins, and a rule with an empty when always matches — that is how you write the final “otherwise”.

condition_type: rules
condition_rules:
- when:
- { field: .new_items, op: gte, value: 1 }
- { field: .locked, op: eq, value: false }
label: generate
- when: []
label: skip
branches:
generate: write-blog
skip: end

Available operators:

OperatorMeaningValue
eq / neequal / not equalany scalar
gt / gte / lt / lteorderinga number, or text compared alphabetically (ISO dates sort correctly)
inis one ofa list
containsa list holds the value, or text holds the substringa scalar or substring
existsthe field is present and not null—
emptyabsent, null, "", [] or {}—

There is no or: write a separate rule for each alternative. When a decision needs real computation, compute it in a Script node and branch on the result.

A missing field makes a comparison false rather than failing. Comparing values that cannot be ordered against each other — a number against text, say — fails the node instead of quietly taking the other branch.

  1. When cascade reaches the Conditional it claims itself as running and evaluates the condition against its input data.
  2. The resulting label is looked up in branches to find the target node ID; if there is no usable label, default_branch is used.
  3. All outgoing edges other than the chosen one have their targets inserted as skipped (so downstream joins can see they settled).
  4. The chosen target is activated via the normal registry path (so nested fan-outs/conditionals still get type-specific activation).
  5. The Conditional settles as completed, with _chosen_branch and _target_node in its output_data for auditability.

If no target resolves at all, the node is marked failed and the reason is recorded in output_data._condition_error, which fails the execution. A reason is also recorded when default_branch rescued the routing, since the workflow did not branch the way its author intended.

  • condition_type is required.
  • Either branches (non-empty) or default_branch must be set.
  • Every target referenced in branches must be a real node.
  • For every label in branches, an outgoing edge to the target must exist on the graph.
  • Outgoing edges whose condition_label is set must match one of the branches labels.
  • default_branch, if set, must reference an existing node.
  • For rules: every rule’s label must be one of the branches; operators that need a value must have one; in needs a list; and no rule may sit after one with an empty when, which would make it unreachable.
  • For rules, when a single incoming edge carries a contract: every field must be declared in it, and the operator and value must match the declared type.
  • For field: the expression must be a dot path.
  • For regex: the pattern must compile.
flowchart LR
Classify["Skill: classify-ticket"] --> C{{"Conditional"}}
C -->|bug| Bug["Skill: bug-triage"]
C -->|feature| Feat["Skill: feature-intake"]
C -->|other| Other["Skill: fallback"]

With condition_type: llm and a prompt “classify as bug, feature, or other”, the LLM’s label picks the branch. Setting default_branch: other protects against unexpected LLM output.