> ## Documentation Index
> Fetch the complete documentation index at: https://helpcenter.magure.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Jump to the target">
    The workflow jumps to the matched branch's `goto` activity. Only one branch is taken; the others are not run.
  </Step>

  <Step title="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](#loops-route-back-to-an-earlier-node)).
  </Step>
</Steps>

<Info>
  Condition nodes enable dynamic, adaptive workflows. The same workflow definition can handle different scenarios by routing through different branches based on runtime data.
</Info>

## 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`:

<Tabs>
  <Tab title="variable">
    Compare a single value from the variable store against a fixed value using an operator. This is the default type.

    ```json theme={null}
    {
      "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](#referencing-data-in-conditions).
  </Tab>

  <Tab title="llm">
    Ask a model to classify free-form data into one of the branch labels. Use it when routing depends on meaning, not an exact value.

    ```json theme={null}
    {
      "condition_type": "llm",
      "llm_prompt": "Classify: {{input.message}}",
      "llm_config_id": "llm-uuid",
      "branches": [
        { "label": "question", "goto": "answer-question" },
        { "label": "complaint", "goto": "handle-complaint" },
        { "label": "other", "goto": "general-response" }
      ],
      "default_branch": "other"
    }
    ```

    The model is instructed to return one of the branch labels; if it responds with something off-label, the node falls through to the default branch (or fails when there is none). `llm_prompt` supports `{{path}}` templates and `llm_config_id` is required.
  </Tab>

  <Tab title="expression">
    Evaluate a safe boolean expression over the workflow's data.

    ```json theme={null}
    {
      "condition_type": "expression",
      "expression": "variables.risk.score > 0.7 and input.amount > 50000",
      "branches": [
        { "label": "true", "goto": "escalate" },
        { "label": "false", "goto": "auto-approve" }
      ]
    }
    ```

    Expressions are parsed with a safe evaluator (no function calls, no imports, no indexing). Supported: comparisons `==` `!=` `<` `>` `<=` `>=`, boolean `and` / `or` / `not` (`&&` / `||` / `!` are also accepted), membership `in` / `not in`, attribute access (`input.field`, `variables.name`), and literals. A blocked or failed expression evaluates to `false`.
  </Tab>
</Tabs>

### 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` / `false` labels (also `yes` / `no` and `1` / `0`). With the `match` operator, 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](/workflows/memory) 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`, not `activity_id.output.field`.
* Trigger and user input live in the `input` scope (`input.field`), execution context in the `system` scope (`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) |

<Note>
  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.
</Note>

## 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 → _end
```

The condition routes `false` back to the first step and `true` to the end:

```json theme={null}
{
  "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.

<Tip>
  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.
</Tip>

### 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 branch `label` to route through once `max_loops` is 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_loops`** exits through its `loop_exit_branch` (or, if that can't be resolved, the terminal END node) once the budget is spent.
* **Any other backward edge** (a raw `next` cycle, or a conditional with no budget) is capped at `MAX_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`.

<Warning>
  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.
</Warning>

## 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.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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](#loops-route-back-to-an-earlier-node).

```json theme={null}
{
  "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-approve
```

### Condition 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-review
```

### Condition 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-agent
```

## Best practices

<AccordionGroup>
  <Accordion title="Keep conditions simple and readable">
    Complex expressions are hard to debug. Break complex logic into a chain of simple Condition nodes rather than one giant expression.
  </Accordion>

  <Accordion title="Check data exists before comparing">
    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.
  </Accordion>

  <Accordion title="Always set a default branch">
    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.
  </Accordion>

  <Accordion title="Bound every loop deliberately">
    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.
  </Accordion>

  <Accordion title="Use descriptive branch labels">
    For multi-way (`match` / `llm`) routing, name branches after what they mean (`passport`, `complaint`) so the workflow reads clearly.
  </Accordion>

  <Accordion title="Test every branch">
    Test with data that triggers each branch, including the default and the loop-exit path. Don't just test the happy path.
  </Accordion>
</AccordionGroup>

<Tip>
  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.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Human task node" icon="user-check" href="/workflows/human-task-node">
    Add human approval in conditional branches
  </Card>

  <Card title="Parallel node" icon="code-branch" href="/workflows/parallel-node">
    Combine conditions with parallel execution
  </Card>

  <Card title="Memory system" icon="database" href="/workflows/memory">
    Understand the variable store conditions read from
  </Card>

  <Card title="Agent node" icon="robot" href="/workflows/agent-node">
    Use agent outputs as condition inputs
  </Card>
</CardGroup>
