Conversation Flow Design

View as MarkdownOpen in Claude

Real conversations are not linear. Users change topics, ask clarifying questions, and skip ahead. Good flow design handles these patterns gracefully.

Linear vs Branching Flows

Linear flow: A to B to C. Simple but rigid.

Branching flow: A leads to B or C depending on user input. Flexible but complex.

Most production agents need branching.

Designing Flows

Start by mapping the conversation:

State Machine Approach

Track conversation state explicitly:

1from enum import Enum
2
3
4class ConversationState(Enum):
5 GREETING = "greeting"
6 COLLECTING_ORDER_ID = "collecting_order_id"
7 SHOWING_STATUS = "showing_status"
8 HANDLING_CANCELLATION = "handling_cancellation"
9 CONFIRMING = "confirming"
10 DONE = "done"
11
12
13class OrderAgent(OutputAgentNode):
14 def __init__(self):
15 super().__init__(name="order-agent")
16 self.state = ConversationState.GREETING
17 self.order_id = None
18 self.pending_action = None
19
20 async def generate_response(self):
21 # Get last user message
22 user_msgs = [m for m in self.context.messages if m["role"] == "user"]
23 user_message = user_msgs[-1]["content"] if user_msgs else ""
24
25 if self.state == ConversationState.GREETING:
26 yield "Hello! I can help you check an order or cancel one."
27 yield " What would you like to do?"
28 self.state = ConversationState.COLLECTING_ORDER_ID
29
30 elif self.state == ConversationState.COLLECTING_ORDER_ID:
31 # Extract order ID
32 self.order_id = self._extract_order_id(user_message)
33
34 if self.order_id:
35 yield f"Got it, order {self.order_id}. "
36 yield "Do you want to check the status or cancel it?"
37 self.state = ConversationState.CONFIRMING
38 else:
39 yield "I need your order ID. It starts with ORD-."
40
41 elif self.state == ConversationState.CONFIRMING:
42 if "status" in user_message.lower():
43 self.state = ConversationState.SHOWING_STATUS
44 async for chunk in self._show_status():
45 yield chunk
46 elif "cancel" in user_message.lower():
47 self.state = ConversationState.HANDLING_CANCELLATION
48 async for chunk in self._handle_cancel():
49 yield chunk

Conditional Branching

Branch based on user input or data:

1async def generate_response(self):
2 user_intent = self._classify_intent()
3
4 if user_intent == "order_status":
5 async for chunk in self._handle_order_status():
6 yield chunk
7
8 elif user_intent == "cancel_order":
9 # Check if order is cancellable first
10 order = await self._get_order()
11
12 if order.status == "shipped":
13 yield "This order has already shipped. "
14 yield "Would you like to start a return instead?"
15 self.pending_action = "return"
16 else:
17 yield "I can cancel this order. "
18 yield "This action cannot be undone. Should I proceed?"
19 self.pending_action = "cancel"
20
21 elif user_intent == "confirm":
22 if self.pending_action == "cancel":
23 await self._cancel_order()
24 yield "Done. Your order has been cancelled."
25 elif self.pending_action == "return":
26 async for chunk in self._start_return():
27 yield chunk

Collecting Information

Gather required data step by step:

1class IntakeAgent(OutputAgentNode):
2 def __init__(self):
3 super().__init__(name="intake-agent")
4 self.required_fields = ["name", "email", "issue"]
5 self.collected = {}
6 self.current_field = 0
7
8 async def generate_response(self):
9 # Get last user message
10 user_msgs = [m for m in self.context.messages if m["role"] == "user"]
11 user_message = user_msgs[-1]["content"] if user_msgs else ""
12
13 # Store the answer to the previous question
14 if self.current_field > 0:
15 prev_field = self.required_fields[self.current_field - 1]
16 self.collected[prev_field] = user_message
17
18 # Check if we have everything
19 if self.current_field >= len(self.required_fields):
20 yield "Thanks! I have everything I need. "
21 yield f"Name: {self.collected['name']}, "
22 yield f"Email: {self.collected['email']}. "
23 yield "Someone will reach out about your issue shortly."
24 return
25
26 # Ask for the next field
27 field = self.required_fields[self.current_field]
28 self.current_field += 1
29
30 prompts = {
31 "name": "What is your name?",
32 "email": "What is your email address?",
33 "issue": "Please describe your issue briefly."
34 }
35
36 yield prompts[field]

Handling Topic Changes

Users switch topics. Detect and handle it:

1async def generate_response(self):
2 # Get last user message
3 user_msgs = [m for m in self.context.messages if m["role"] == "user"]
4 user_message = user_msgs[-1]["content"] if user_msgs else ""
5
6 # Detect topic change
7 new_topic = self._detect_topic(user_message)
8
9 if new_topic and new_topic != self.current_topic:
10 # Acknowledge the switch
11 yield f"Switching to {new_topic}. "
12
13 # Clean up previous topic state
14 self._reset_topic_state()
15
16 self.current_topic = new_topic
17
18 # Handle current topic
19 if self.current_topic == "orders":
20 async for chunk in self._handle_orders():
21 yield chunk
22 elif self.current_topic == "billing":
23 async for chunk in self._handle_billing():
24 yield chunk

Confirmation Loops

For important actions, confirm before proceeding:

1async def generate_response(self):
2 # Get last user message
3 user_msgs = [m for m in self.context.messages if m["role"] == "user"]
4 user_message = user_msgs[-1]["content"] if user_msgs else ""
5
6 if self.awaiting_confirmation:
7 if self._is_affirmative(user_message):
8 await self._execute_action()
9 yield "Done."
10 self.awaiting_confirmation = False
11 elif self._is_negative(user_message):
12 yield "Okay, I won't do that. Is there anything else?"
13 self.awaiting_confirmation = False
14 else:
15 yield "Please say yes or no."
16 return
17
18 # Normal flow...
19 if self._wants_to_delete():
20 yield "This will permanently delete your data. Are you sure?"
21 self.awaiting_confirmation = True
22 self.pending_action = "delete"

Flow Recovery

Help users who get lost:

1async def generate_response(self):
2 # Get last user message
3 user_msgs = [m for m in self.context.messages if m["role"] == "user"]
4 user_message = user_msgs[-1]["content"] if user_msgs else ""
5
6 # Detect if user is confused
7 confusion_signals = ["what", "huh", "confused", "start over", "help"]
8
9 if any(sig in user_message.lower() for sig in confusion_signals):
10 yield "No problem! Let me help. "
11 yield "You can ask me to: "
12 yield "Check an order, cancel an order, or talk to someone. "
13 yield "What would you like?"
14
15 # Reset to known state
16 self.state = ConversationState.GREETING
17 return
18
19 # Normal flow...

Tips

Explicit states prevent bugs. You always know exactly where in the flow you are.

Detect words like “help”, “confused”, “start over” and offer a reset.

Always ask “are you sure?” before deleting, cancelling, or changing important data.