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

# Branching and variables

> How branch_on_reply actually matches a reply, what its fallback really does, and where Liquid can't reach.

Two node types make decisions in a flow. `branch_on_reply` decides based on what the customer just said. `conditional` decides based on what the flow already knows. They are not interchangeable, and knowing which one you need is most of the work.

<Warning>
  **Configuring a `branch_on_reply` node from the canvas is coming soon.** The inspector's Step panel
  renders configuration for `send_message`, `send_amb_primitive`, `wait_for_reply`, `integration`, and
  `assign_to_agent` — not for `branch_on_reply`. The canvas can create the node, and it renders whatever
  match clauses and fallback already exist on it, but there's no UI yet for authoring or editing them.
  Match clauses and fallbacks are API-only today. The one exception is the composite **"Message with
  options"** shortcut, which drops in a `branch_on_reply` with its fallback hardcoded to `loop` and
  `max_retries: 2` — everything below still applies to that node once it exists, you just can't
  hand-author a new one from the canvas yet.
</Warning>

## How a match clause matches

A `branch_on_reply` node holds a list of match clauses, each pointing at a destination node. Three clause types exist:

| Type | Matches when |
| - | - |
| `list_picker_item` | The reply's identifier equals a fixed value — exact, stable, not affected by copy |
| `text_equals` | The reply's full text equals a fixed value, case-insensitive |
| `text_contains` | The reply's text contains a fixed value anywhere, case-insensitive |

The first clause that matches wins. If none do, the fallback runs.

<Tip>
  **Reach for `list_picker_item` whenever the choice matters.** It matches on a stable identifier, not
  displayed text — the same caveat covered in [Channel-native nodes](/chatbot/channel-nodes): quick
  replies on a Simple Message hand back their label text and nothing else, so a `text_equals` branch
  built on one breaks the moment someone edits the copy. A List Picker doesn't have that problem.
</Tip>

## What the fallback actually does

Three outcomes exist for "nothing matched," and only one of them notifies anyone:

<AccordionGroup>
  <Accordion title="loop" icon="repeat">
    Re-poses the same node, up to a configured retry cap. Once the cap is hit, the flow ends.
  </Accordion>

  <Accordion title="agent" icon="bot">
    Hands the turn to the **AI agent** — not to a person — once consecutive unmatched replies reach
    the inbox's threshold (one miss, by default). The agent picks the conversation up from there and
    is then subject to its own [escalation grounds](/agents/guardrails), so a human is reached only
    if the agent cannot finish either.

    If the agent cannot be reached at all, this degrades to a real human handoff rather than failing
    silently. See [AI-to-human handoff](/inbox/handoff) for what a handoff costs.
  </Accordion>

  <Accordion title="Anything else, including no fallback configured" icon="user">
    Ends the flow. Nothing is assigned, and nothing is logged as an escalation — this is functionally
    identical to the flow simply running out of graph.
  </Accordion>
</AccordionGroup>

<Warning>
  **An unset fallback is a silent dead end.** The flow simply stops: nobody is assigned, nothing is
  logged as an escalation, and the customer is left without a reply. Set one deliberately.

  `agent` is usually the one you want — it is what stops a gap in your branch list from costing you
  a human touch. Note that it reaches the **AI agent** first; routing straight to a person is
  `assign_to_agent`, whose name is unhelpful here and does mean a human.
</Warning>

## conditional

`conditional` branches on a variable rather than a reply — a `true_next_node_id` and a `false_next_node_id`, chosen by comparing a variable to a fixed value.

<Note>
  **Today that comparison is a strict equality check only.** No greater-than, less-than, or contains —
  those aren't built yet. And like `set_conversation_attribute`, `add_label`, and `delay`, there's
  currently no library block for `conditional` on the canvas (see [Canvas basics](/chatbot/canvas)). The
  engine evaluates it correctly wherever a graph contains one; the gap is only in adding one through the
  builder itself.
</Note>

## Captured variables and Liquid

`set_conversation_attribute` writes a key and a value onto the **conversation** — a conversation
attribute, not an entry in the execution's variable bag.

**Reading a captured attribute back through `{{ variables.<key> }}` is coming soon.** Liquid's `variables`
scope is populated only by `last_reply`, the trigger, and two internal counters the engine itself writes
— nothing `set_conversation_attribute` writes reaches it. So a later node's `{{ variables.order_status }}`
will not resolve a value you captured with `set_conversation_attribute`, and `conditional` can't read one
either, since it compares against that same `variables` scope. The available context today is narrower
than it might look:

```liquid theme={null}
Hi {{ contact.name }}, your order status is {{ variables.last_reply }}.
```

* `contact` exposes `name`, `email`, and `phone_number` only — no custom contact attributes yet.
* `conversation` exposes `id`.
* `variables` exposes `last_reply`, the trigger, and two internal counters the engine writes — not
  anything written by `set_conversation_attribute`.

A reply's raw text is itself a captured variable — `variables.last_reply` — which is what `branch_on_reply` and `conditional` are actually reading when they evaluate a match.

## Routing a reply back into the flow

One more thing worth knowing if an agent or the AI sends a rich feature manually, outside a running flow: a `send_amb_primitive` node can be marked to route replies globally, so a customer tapping a List Picker that a human sent by hand still lands on the right `branch_on_reply` node in a published flow, rather than falling through as an unhandled message. This starts a run only when no person owns the conversation; while one does, the reply is left to them.

## Next

<CardGroup cols={2}>
  <Card title="Channel-native nodes" icon="sparkles" href="/chatbot/channel-nodes">
    Why List Picker returns a stable identifier and quick replies don't.
  </Card>

  <Card title="Node types" icon="boxes" href="/chatbot/nodes">
    The full node set, including what each field configures.
  </Card>

  <Card title="Testing and simulation" icon="flask-conical" href="/chatbot/testing">
    Where the simulator's own matching diverges from production.
  </Card>

  <Card title="AI agents" icon="bot" href="/agents">
    What happens once the `agent` fallback hands a turn over.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.