Designing a multi-stage conversation¶
A learn-by-doing walkthrough of designing a real conversation from scratch. You will build a meeting-prep assistant with three stages — intake → research → summarize — and end with a runnable design you can chat with.
This tutorial focuses on the design decisions: what each stage is for, what data it extracts, how it routes to the next, and how to handle a user who veers off-topic. For the test mechanics (running chats, verifying results), see Chatbot end-to-end test.
What you'll build¶
A conversation called meeting-prep with:
- Stage 1 —
intake: collect who, what, and why for the meeting. - Stage 2 —
research: pull together talking points and questions. - Stage 3 —
summarize: produce a one-page brief and confirm. - Routing signals: phrases that tell Hadron when this conversation should run.
- An escape edge: a way out if the user changes the subject mid-prep.
By the end, a user can say "Help me prep for my 1:1 with Sam tomorrow" and the assistant will guide them through a structured prep session.
Prerequisites¶
- A Hadron agent with a chatbot system memory. If you don't have one yet, follow Getting started or Building a chatbot agent first.
- Claude Code with the Hadron MCP server connected, so you can author nodes through conversation. The same instructions can be used through the portal node editor — adapt the wording to "create node X with data Y".
- 30–45 minutes for the design pass; another 15 to test.
Set the active memory to your agent's system memory before you start:
Set the active memory to
<your-system-memory-name>.
Step 1 — Plan the conversation before creating nodes¶
The single best discipline in conversation design is to write the flow on paper (or in a scratchpad) before you create any nodes. Three questions per stage:
- What does this stage need from the user? That answer becomes the
extractionSpec. - What's the prompt asking the LLM to do? That answer becomes the prompt content.
- When does this stage hand off to the next? That answer becomes the
transition condition or the LLM's
next_stageinstruction.
For meeting-prep, the answers are:
| Stage | Needs from user | LLM does | Hands off when |
|---|---|---|---|
intake |
who's attending, what's the meeting about, what's the goal | Asks 2–3 short questions; doesn't try to dig deep yet | Topic + goal are captured |
research |
which talking points matter, what questions to raise | Suggests points based on the topic; lets user accept / edit / add | At least one talking point is locked in |
summarize |
a confirmation that the brief looks right | Renders a tidy brief; offers tweaks | User says it's good (or LLM detects acceptance) |
Notice the pattern: each stage is a focused conversation, not a multi-purpose agent. The LLM stays on rails because the prompt has one job at a time.
Step 2 — Create the conversation node¶
Conversations and stages are system nodes in your agent's system memory.
Create the parent first, with routing metadata so the agent knows when to
pick it.
Create node
conversations:meeting-prep(nodeType: system) with:{ "isSetup": false, "stageOrder": ["intake", "research", "summarize"], "goalDescriptions": [ "Help the user prepare for an upcoming meeting", "Build a structured brief: attendees, goal, talking points, questions" ], "goalSignals": [ "Help me prep for my meeting", "I have a 1:1 tomorrow with Sam — what should I bring up?", "Can you help me get ready for the standup?", "I'm meeting with the board next week, need to think through it" ] }
stageOrder declares the default linear flow: intake → research →
summarize.
goalDescriptions and goalSignals are how the routing engine matches
user messages to this conversation. Be generous with signals — multiple
phrasings of the same intent help the engine match real-world wording. See
Conversation routing
for the deep model.
Step 3 — The intake stage¶
intake is the cheapest stage to design well: it just collects three
things and moves on. Don't over-script it; the LLM is good at small-talk
chained with structured asks.
Create node
conversations:meeting-prep:intake(nodeType: system) with:{ "promptRef": "prompts:meeting-prep:intake", "extractionSpec": [ { "field": "{chat}.meeting_topic", "description": "One-line topic of the meeting (e.g. 'quarterly OKRs review')", "shape": "string" }, { "field": "{chat}.meeting_goal", "description": "What the user wants to achieve in the meeting", "shape": "string" }, { "field": "{chat}.attendees", "description": "Attendee names or roles (comma-separated)", "shape": "string" } ] }
Note {chat} as the field prefix — this writes the extracted data into
the current chat's data, not the user's persistent memory. Use
{chat}.* for chat-scoped facts (this meeting) and memory.* for
durable user facts (the user's name, role, team). Mixing the two is the
most common extraction-spec mistake.
Now the prompt. Keep it short and explicit about the tool call:
Create node
prompts:meeting-prep:intake(nodeType: system) with content:The user wants to prep for a meeting. Ask three short questions, one at a time: 1. What's the meeting about? (one line) 2. Who's attending? 3. What does the user want to walk out with? Don't dig into specifics yet — that's the next stage. Once the user has given you a topic AND a goal, set `next_stage` to "research". Use the `respond` tool. Always include any new info in the `data` field; never store it in free-text only.
The intake stage hands off when both meeting_topic and meeting_goal
exist. The cleanest way to enforce this is to instruct the LLM in the
prompt (above) and let it set next_stage itself. For an extra belt
on the suspenders, you could add an edge that holds the stage if the
fields are missing — but for a simple flow, the prompt is enough.
Step 4 — The research stage¶
Research is where the conversation earns its keep. The prompt should ask the LLM to propose talking points based on the topic captured in intake, and let the user accept, edit, or add.
Create node
conversations:meeting-prep:research(nodeType: system) with:{ "promptRef": "prompts:meeting-prep:research", "extractionSpec": [ { "field": "{chat}.talking_points", "description": "Semicolon-separated list of points the user wants to make", "shape": "string" }, { "field": "{chat}.questions_to_ask", "description": "Semicolon-separated list of questions for the meeting", "shape": "string" } ] }Create node
prompts:meeting-prep:research(nodeType: system) with content:The user is preparing for a meeting: - Topic: {{ {chat}.meeting_topic }} - Goal: {{ {chat}.meeting_goal }} - Attendees: {{ {chat}.attendees }} Suggest 3 candidate talking points and 2–3 questions worth asking. Frame them as "Here are some I'd suggest — keep, edit, or replace." The user might already have ideas in mind; capture those as you go. When you have at least one talking point the user has confirmed, set `next_stage` to "summarize". Use the `respond` tool. Talking points and questions go in `data` as semicolon-separated strings.
Notice the prompt uses Mustache variables ({{ {chat}.meeting_topic }})
to inject the data captured in intake. The runtime resolves these when
the prompt is compiled — see
Mustache template syntax for the rules.
This is the moment where intake's discipline pays off: the research stage has clean inputs to work with.
Step 5 — The summarize stage¶
The final stage produces the artifact: a one-page brief. The LLM does the formatting; the user just confirms.
Create node
conversations:meeting-prep:summarize(nodeType: system) with:{ "promptRef": "prompts:meeting-prep:summarize", "extractionSpec": [ { "field": "{chat}.brief_confirmed", "description": "Whether the user accepted the brief (true/false)", "shape": "boolean" } ] }Create node
prompts:meeting-prep:summarize(nodeType: system) with content:Render a tight meeting brief in this exact shape: ## Meeting: {{ {chat}.meeting_topic }} **Goal:** {{ {chat}.meeting_goal }} **Attendees:** {{ {chat}.attendees }} ### Talking points {{ {chat}.talking_points }} ### Questions to ask {{ {chat}.questions_to_ask }} Then ask: "Does this look right, or want me to tweak anything?" If the user accepts, set `data.{chat}.brief_confirmed` to true and `next_stage` to null (end of conversation). If they want changes, apply the edit, render the brief again, ask again. Use the `respond` tool.
Setting next_stage: null ends the conversation cleanly. The chat stays
on the user's record — they can re-enter it later or start fresh.
Step 6 — Add an escape edge for off-topic detours¶
Real users don't stay on rails. Half-way through prep, they might say "Wait — actually, can you help me draft an email to Sam first?" The chatbot should gracefully bail to whichever conversation handles email drafting (or a fallback) rather than soldiering through prep.
The mechanism is an escape edge: an edge that fires whenever the
chatbot's onTrack field comes back false. Add it to the conversation
node, not a specific stage, so it covers all three stages.
Update node
conversations:meeting-prep's data to addedges:
timing: always evaluates the edge every turn; behavior: transition
means the routing engine moves to the target conversation rather than
pushing a goal and coming back. The fallback conversation is created by
the wizard — every agent has one.
For the routing-engine details (when onTrack flips, how behavior:
detour differs, the goal stack), see
Conversation routing.
Step 7 — Test the design¶
The fastest feedback loop is a Claude Code dry run. In Claude Code:
Start a chat with
<your-agent-id>for userme, conversationmeeting-prep. Useh-chat-start.
Then play the user. A short test script:
| Turn | You say | What to watch for |
|---|---|---|
| 1 | "Help me prep for my 1:1 with Sam tomorrow." | Welcome from intake. Topic should land in {chat}.meeting_topic. |
| 2 | "It's a quarterly review of my team's OKRs. I want to walk out with agreement on which two to deprioritize." | Goal extracted. Stage transition to research. |
| 3 | "I'd like to talk about the Q2 sales-enablement OKR — it's not landing. And the ML platform OKR, which is the one I'd keep." | Talking points captured. Stage transition to summarize. |
| 4 | "Looks great. Yes." | brief_confirmed: true, next_stage: null. Chat ends. |
Watch for:
- Stage transitions at the right turn (intake → research → summarize).
- Extracted data in the chat node's
datafield after each turn. - The compiled brief in turn 4 includes the values you gave.
If a transition didn't fire when expected, the most common causes are
(in order): the prompt didn't tell the LLM to set next_stage; the
extractionSpec field shape doesn't match what the LLM emitted; the
stageOrder array is out of sync with the stage node names.
For a more thorough validation pass, build a test persona for this conversation and re-run it whenever you tweak a prompt. See Test personas.
What to do when a stage gets crowded¶
A common failure mode: the design works for the happy path but turns brittle when the user gives surprising answers. Two patterns help.
Split a stage into sub-stages¶
If the prompt for research starts feeling overloaded — "ask about
talking points AND questions AND any prior context AND…" — that's a
signal to split. A research-points stage feeding a research-questions
stage gives each prompt one focused job and makes extraction cleaner.
Add a prerequisite edge¶
If the user lands on research without having actually given a goal
(possible if they routed in mid-flow from another conversation), the
prompt's Mustache variables will be empty and the LLM will improvise
poorly. A prerequisite edge on research with timing: on_enter
and a missing condition on {chat}.meeting_goal will detour to
intake first, then return automatically.
The mechanics are in Conversation routing → Edge presets.
Where to go next¶
- Test personas — automate this happy-path test so it runs on every change.
- Conversation routing — the full routing model (topics, edges, the goal stack, hierarchical extraction).
- Building a chatbot agent — agent setup if you skipped that step.
- Chatbot end-to-end test — manual end-to-end test with both Path A (Portal Chat) and Path B (Claude Code + MCP).