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.
How a match clause matches
Abranch_on_reply node holds a list of match clauses, each pointing at a destination node. Three clause types exist:
The first clause that matches wins. If none do, the fallback runs.
What the fallback actually does
Three outcomes exist for “nothing matched,” and only one of them notifies anyone:loop
loop
Re-poses the same node, up to a configured retry cap. Once the cap is hit, the flow ends.
agent
agent
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, 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 for what a handoff costs.
Anything else, including no fallback configured
Anything else, including no fallback configured
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.
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.
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). The
engine evaluates it correctly wherever a graph contains one; the gap is only in adding one through the
builder itself.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:
contactexposesname,email, andphone_numberonly — no custom contact attributes yet.conversationexposesid.variablesexposeslast_reply, the trigger, and two internal counters the engine writes — not anything written byset_conversation_attribute.
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: asend_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
Channel-native nodes
Why List Picker returns a stable identifier and quick replies don’t.
Node types
The full node set, including what each field configures.
Testing and simulation
Where the simulator’s own matching diverges from production.
AI agents
What happens once the
agent fallback hands a turn over.