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 with
h-update-nodeon the conversation node - The MCP tools directly
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. |
behavior |
transition, detour |
transition = permanent move. detour = push to goal stack, come back when done. |
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.
Edge presets (UI shortcuts)¶
When creating edges in the editor, these presets fill in the properties:
- Next:
timing: on_complete,behavior: transition, no condition. "When this is done, go there." - Prerequisite:
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, no condition. "The user can bail to this conversation at any time." - Fallback:
timing: always,behavior: transition, triggered byonTrack: false. "If the conversation is off track, go here."
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"
}
When onTrack is false, the system knows the current conversation
isn't serving the user well. This triggers re-routing: the system
evaluates edges and goal signals to find a better conversation.
This is continuous — the LLM evaluates onTrack on every turn,
so drift is caught immediately.
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]
When the top goal completes, the system pops it and returns to the previous goal. When the stack is empty, the user's original goal is done.
The goal stack enables detours: "We need your business type before we can find government programs. Let's update your profile first." The system remembers where the user was and brings them back.
How goals get pushed¶
- Automatically: when a
detouredge fires, the system pushes the detour's goal and records the return point. - By the LLM: when it detects an implicit goal ("you mentioned
cash flow — want to work on that next?"), it can push via
h-chat-push-goal. - By the user: explicitly stating a new goal.
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": "..." }
]
Used for: - Debugging: why did the chatbot end up here? - Loop prevention: the server rejects routing suggestions that would send the user back to a conversation they just left. - Analytics: which paths do users take? Where do they drop off? - Resumption: when the user comes back, the system knows where they left off.
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.
MCP tools for routing¶
The chat MCP tools fall into three groups: the core conversation flow
(start / send / process), routing inspection (routing map, route history,
training entries), and the goal stack (push / pop). Inputs marked with ?
are optional.
Note: a richer reference for all Hadron MCP tools (memory ops, sessions, data) is tracked in #11. This page covers only chat / routing tools to keep them in context with the routing model documented above.
Core conversation flow¶
h-chat-start¶
Start a new chat session with an agent. Returns the compiled welcome
prompt and the respond tool schema your app will pass to the LLM.
Input:
agentId: string # Agent the chat belongs to
userId?: string # End-user identifier (provisions a
# per-user memory if absent)
conversationName?: string # If unset, the wizard's setup
# conversation runs first; otherwise
# falls through to the first
# non-setup conversation.
Returns:
chatId: string # e.g. "chats:20260507-abc12345-onboarding"
systemMessage: string # Compiled prompt with Mustache variables
# resolved, partials inlined
tools: object # Tool schema (the `respond` tool)
# built from the stage's extractionSpec
conversationName: string # Which conversation was started
stageName: string # Initial stage name
h-chat-send¶
Send the user's next message and get the updated prompt + history for the next assistant turn.
Input:
chatId: string
userMessage: string
Returns:
systemMessage: string # Re-compiled for the current stage
# (which may have just transitioned)
tools: object # Tool schema for the current stage
messageHistory: Array<{ # Last N messages to feed the LLM
role: "user" | "assistant",
content: string
}>
needsSummarization: boolean # True when the running message count
# crosses the conversation's threshold
h-chat-process¶
Send the LLM's structured response back to Hadron. Saves the assistant message, extracts data into user memory, and decides on stage transitions.
Input:
chatId: string
message: string # The assistant's reply text
data?: object # Extracted fields, e.g.
# { "memory.name": "Alex",
# "memory.location": "Portland" }
next_stage?: string | null # null/omit = stay in stage;
# name = transition to that stage
Returns:
displayMessage: string # What to show the user
stageTransitioned: boolean # True if the stage changed
newStageName: string | null # Set when stageTransitioned is true
Routing inspection¶
h-chat-get-routing-map¶
Download the full routing graph for an agent — useful for debugging or visualization.
Input:
agentId: string
Returns:
topics: Array<TopicNode>
conversations: Array<ConversationNode>
stages: Array<StageNode>
edges: Array<EdgeNode> # Stage edges with conditions, timing,
# behavior, label
trainingEntries: Array<{ # Routing training data
userStatement: string,
matchedConversation: string,
language: string
}>
h-chat-get-route-history¶
Read where a chat has traveled and the current goal stack.
Input:
chatId: string
Returns:
conversationName: string # Current conversation
stageName: string # Current stage
goalStack: Array<Goal> # Active goals, top of stack last
routeHistory: Array<RouteEvent> # Chronological enter/transition log
h-chat-add-training-entry¶
Add a user statement to the agent-wide routing training set so future chats route the same way for similar phrasings.
Input:
agentId: string
userStatement: string # The user's actual words
matchedConversation: string # Which conversation should route here
language?: string # Default "en"
Returns:
count: number # Total training entries for this agent
Goal stack¶
h-chat-push-goal¶
Push a new goal onto a chat's goal stack — used when a detour edge fires or when the assistant detects an implicit new goal.
Input:
chatId: string
description: string # Short goal description
createdBy?: "ASSISTANT" | "USER" # Default "ASSISTANT"
Returns:
description: string
activeStackDepth: number
h-chat-pop-goal¶
Complete the current (top) goal and return to the previous one.
For test-persona tools (h-chat-define-persona, h-chat-list-personas,
h-chat-run-persona), see Test personas.
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, signals, and an escape edge.
- 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