Conversation Routing¶
How topics, conversations, stages, edges, and goals work together to create chatbots that flow naturally between modes.
The hierarchy¶
Agent
└─ Topic (optional) ← groups related conversations
└─ Conversation ← a coherent flow with a goal
└─ Stage ← a focused step with a prompt + extraction
Topics group conversations that share a broad goal. A yoga studio bot might have topics "Class schedule", "Membership", and "General help." Topics are optional — simple agents skip them.
Conversations are the core unit. Each has a goal ("help the user find a mentor"), a sequence of stages, and routing metadata that helps the system match user messages to the right conversation.
Stages are steps within a conversation. Each stage has a prompt (what the chatbot says/asks), an extraction spec (what data to pull from the user's response), and edges (where to go next).
Goals and signals¶
Every conversation (and optionally every topic) carries routing metadata:
{
"goalDescriptions": [
"Help the user find class times and teacher availability",
"Answer questions about the weekly schedule"
],
"goalSignals": [
"When is yoga?",
"Is the Saturday class running?",
"Who teaches on Monday?",
"Is Nina back in town?"
]
}
goalDescriptions — natural-language descriptions of when this
conversation is the right fit. Multiple phrasings help the routing
engine match ambiguous user messages. Write them from the user's
perspective: "The user wants to..."
goalSignals — example user statements that should route here.
This is a living list — it grows over time as you discover new
ways users express the same need. Multi-language signals are
supported.
Where to define them¶
Goals and signals live in the conversation node's data field. You
can set them via:
- The Chatbot Control tab in the portal (read-only display for now)
- The Conversation Editor (portal)
- Claude Code or any MCP host, with
hadron_update_nodeon the conversation node
Edges¶
Edges connect stages to other stages or conversations. They encode routing logic in the graph itself.
Edge properties¶
{
"target": "conversations:profile-building",
"condition": { "missing": ["memory.business_type"] },
"priority": 0,
"timing": "on_complete",
"behavior": "detour",
"label": "Fill profile if business type is missing"
}
| Property | Values | Description |
|---|---|---|
condition |
JSONLogic JSON, or null |
When the edge fires. null = always fires. See Edge conditions for the full operator subset, the five variable scopes (memory, chat, agent, message data, time), and the memory cascade. |
priority |
integer (default 0) | Resolution-order hook for edges sharing a source node; lower fires first. Reserved space for future agent-exception handlers to slot in at the top. |
timing |
on_complete, on_enter, always |
When to evaluate the condition. on_enter is stored but not evaluated today — see What the chat engine acts on today. |
behavior |
transition, detour |
transition = permanent move. detour = move, and push a goal on the stack. Nothing returns the user afterwards — see Goal stack. |
label |
any string | Human-readable description (shown in the editor). |
For everything about condition — operators, variable scopes,
cascade, encryption behavior, the structured builder UI in the portal —
see the dedicated Edge conditions reference.
What the chat engine acts on today¶
The fields above are what an edge can store. The chat engine acts on a narrower set, and the difference matters when you design a flow:
- Edges are checked only when the LLM does not choose a stage. If the
LLM's
respondcall setsnext_stage, the engine moves to that stage and checks no edges that turn. - Only
on_completeandalwaysedges are checked, and they behave the same. Nothing detects that a stage is finished, so anon_completeedge is checked on every turn where the model doesn't choose a stage, exactly likealways. Anon_enteredge is accepted and stored, but never evaluated. - Only an edge that targets a conversation does anything. It hands the
chat off to that conversation's first stage. An edge that targets a
stage is evaluated and then ignored, so moving between stages is
entirely the LLM's
next_stage.
That leaves the Next and Prerequisite patterns below with no effect when they point at a stage. We are checking this against the engine's intended behaviour (hadron-docs#308).
Common edge patterns¶
These are the usual combinations of timing, behavior and condition. The Conversation Editor has no presets for them: you set the fields yourself in Edit routing rule.
- Next:
timing: on_complete,behavior: transition. "When this is done, go there." Give it a condition that says when the stage is done (for example, the facts it collects are present): with no condition it fires on the first turn where the model doesn't setnext_stage. - Prerequisite (not evaluated today — see
What the chat engine acts on today):
timing: on_enter,behavior: detour, with a{ missing: [<field>] }condition. "Before entering this stage, make sure we have this data." - Escape:
timing: always,behavior: transition, with a condition. "The user can bail to this conversation when this holds." Without a condition it fires on every turn where the model doesn't setnext_stage, which hands the chat off almost at once. - Fallback:
timing: always,behavior: transition, meant to fire ononTrack: false. It can't be built today: an edge condition can't seeonTrack(see TheonTrackfield), and the same edge without a condition is the always-firing Escape above.
The onTrack field¶
Every chatbot response includes an onTrack boolean:
{
"message": "I see you want something else...",
"data": { ... },
"next_stage": null,
"onTrack": false,
"offTrackReason": "User is asking about billing, not the schedule"
}
onTrack: false is the model saying the current conversation isn't
serving the user. It was designed to trigger re-routing, but today it is
only recorded: processChatResponse stores it on the chat (with
offTrackReason) and returns it, and nothing reads it back. An edge
condition can't see it either: conditions evaluate against the memory
layers, the chat's message count, conversation and stage, the agent, and
the turn's extracted data. The one re-route that does run is out of the
fallback conversation, on goal signals
(see Fallback conversation).
Goal stack¶
The user's state is a stack, not a single position:
[bottom] Find government programs (original goal)
→ Update business profile (detour: missing data)
→ Confirm industry category (sub-goal) [top]
The goal stack records detours: "We need your business type before we can find government programs. Let's update your profile first."
What is built today¶
- Pushing is automatic, and only automatic. When an edge with
behavior: detourto another conversation fires, the engine pushes a goal (the edge'slabel, orDetour to <conversation>) and hands off to the target conversation's first stage. Nothing else pushes: the LLM'srespondtool has no goal field, and there is no user-facing operation for it. - Nothing pops the stack. A detour does not return the user to where they were when it completes. The stack is recorded on the chat, but no engine step, API operation or portal control completes a goal or returns to the previous one.
The hadron_chatbot_push_goal and hadron_chatbot_pop_goal MCP tools
were the only way to push a goal by hand or pop one. They were removed in
hadron-server#1143.
Route history¶
Every chat records where it has been:
[
{ "action": "ENTER", "conversationUrn": "conversations:onboarding", "nodeUrn": "conversations:onboarding:welcome", "timestamp": "..." },
{ "action": "ENTER", "conversationUrn": "conversations:onboarding", "nodeUrn": "conversations:onboarding:background", "trigger": { "type": "STAGE_TRANSITION" }, "timestamp": "..." },
{ "action": "ENTER", "conversationUrn": "conversations:strategy", "nodeUrn": "conversations:strategy:diagnose", "edgeUrn": "...", "trigger": { "type": "TRANSITION_EDGE" }, "timestamp": "..." }
]
The engine appends an entry when a chat starts, when the LLM's
next_stage moves it to another stage, when an edge hands off to another
conversation, and when it reroutes to the fallback conversation.
No surface reads it back today. Neither the Chat API nor the portal
returns a chat's route history. The hadron_chatbot_get_route_history
MCP tool did, and was removed in
hadron-server#1143.
The engine does not use it for loop prevention either. To see where a
test chat went, run it in the portal Tester's Scripted persona section,
whose report lists each turn's stage and the conversations visited.
Hierarchical extraction¶
Data extraction specs can live at three levels:
| Level | What it extracts | Scope |
|---|---|---|
| Agent | User name, language, account ID | Every conversation |
| Conversation | Order number, problem description | All stages in that conversation |
| Stage | A yes/no confirmation, a rating | Just that step |
Each level inherits from above. A stage sees its own spec + the conversation's spec + the agent's spec. Stage-level fields win on conflict.
Agent-level specs live in a config node in the system memory:
{
"agentExtractionSpec": [
{ "field": "memory.name", "description": "User's full name", "shape": "string" },
{ "field": "memory.language", "description": "Preferred language", "shape": "string" }
]
}
Fallback conversation¶
Every agent should have a fallback conversation — created automatically by the wizard. It handles the case where no conversation matches the user's request:
conversations:fallback
stage: no-match
prompt: "I can't help with that. Here's what I can do: [list].
Or reach a person at [contact]."
The fallback conversation has isFallback: true in its data.
Driving a chat¶
Run a chat through the Chat API — startChat,
sendChatMessage and processChatResponse — or in the portal: an App's
Chats tab for a real chat, and the agent's Tester for test runs.
To view or edit the routing graph, open the agent's Conversation
Editor. There are no MCP tools for chatbot chats: the eleven
hadron_chatbot_* tools were removed in
hadron-server#1143.
Related docs¶
- designing-a-multi-stage-conversation.md — learn-by-doing tutorial that walks through designing a real three-stage conversation (intake → research → summarize) with prompts, extraction specs and signals.
- test-personas.md — automated testing with personas
- chatbot-end-to-end-test.md — manual end-to-end test guide
- building-a-chatbot-agent.md — creating a chatbot from scratch