Skip to content
Learn Agentic AI
Learn Agentic AI12 min read1 views

Building Conversational Flows with OpenAI Agents SDK: Multi-Turn State Management

Design structured conversational flows with the OpenAI Agents SDK including state machines, slot filling, context tracking, and graceful conversation control for multi-turn interactions.

Conversations Are State Machines

Every structured conversation follows a pattern: greet the user, collect information, confirm details, execute an action, and close. This is a state machine. The OpenAI Agents SDK does not force a specific state management approach, which gives you the flexibility to implement exactly the pattern your use case needs.

This guide shows you how to build structured conversational flows with explicit state tracking, slot filling, and flow control.

Defining Conversation State

Start with a clear state model that tracks where the user is in the flow and what data has been collected.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
flowchart LR
    INPUT(["User input"])
    AGENT["Agent<br/>name plus instructions"]
    HAND{"Handoff to<br/>another agent?"}
    SUB["Sub-agent<br/>specialist"]
    GUARD{"Guardrail<br/>passed?"}
    TOOL["Tool call"]
    SDK[("Tracing<br/>OpenAI dashboard")]
    OUT(["Final output"])
    INPUT --> AGENT --> HAND
    HAND -->|Yes| SUB --> GUARD
    HAND -->|No| GUARD
    GUARD -->|Yes| TOOL --> AGENT
    GUARD -->|Block| OUT
    AGENT --> OUT
    AGENT --> SDK
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style SDK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
from pydantic import BaseModel
from enum import Enum
from typing import Any

class FlowState(str, Enum):
    GREETING = "greeting"
    COLLECTING_INFO = "collecting_info"
    CONFIRMING = "confirming"
    EXECUTING = "executing"
    COMPLETED = "completed"
    CANCELLED = "cancelled"

class SlotValue(BaseModel):
    value: Any | None = None
    confirmed: bool = False
    attempts: int = 0

class BookingState(BaseModel):
    flow_state: FlowState = FlowState.GREETING
    slots: dict[str, SlotValue] = {}
    required_slots: list[str] = ["date", "time", "service", "name", "phone"]
    errors: list[str] = []

    def get_missing_slots(self) -> list[str]:
        return [
            slot for slot in self.required_slots
            if slot not in self.slots or self.slots[slot].value is None
        ]

    def all_slots_filled(self) -> bool:
        return len(self.get_missing_slots()) == 0

    def get_slot_summary(self) -> str:
        lines = []
        for slot_name in self.required_slots:
            slot = self.slots.get(slot_name)
            if slot and slot.value:
                status = "confirmed" if slot.confirmed else "pending"
                lines.append(f"- {slot_name}: {slot.value} ({status})")
            else:
                lines.append(f"- {slot_name}: [not provided]")
        return "\n".join(lines)

Building the Slot Filling Agent

Create tools that let the agent update the conversation state as it collects information.

from agents import Agent, Runner, function_tool, RunContextWrapper

@function_tool
async def set_slot(ctx: RunContextWrapper[BookingState], slot_name: str, value: str) -> str:
    """Set a slot value collected from the user."""
    state: BookingState = ctx.context
    if slot_name not in state.required_slots:
        return f"Unknown slot: {slot_name}. Valid slots: {state.required_slots}"

    state.slots[slot_name] = SlotValue(value=value, confirmed=False)
    missing = state.get_missing_slots()
    if missing:
        return f"Slot '{slot_name}' set to '{value}'. Still need: {', '.join(missing)}"
    else:
        state.flow_state = FlowState.CONFIRMING
        return f"Slot '{slot_name}' set to '{value}'. All slots filled. Ask user to confirm."

@function_tool
async def get_state(ctx: RunContextWrapper[BookingState]) -> str:
    """Get current booking state and missing information."""
    state: BookingState = ctx.context
    summary = state.get_slot_summary()
    missing = state.get_missing_slots()
    return f"Current state: {state.flow_state.value}\n{summary}\nMissing: {missing or 'none'}"

@function_tool
async def confirm_booking(ctx: RunContextWrapper[BookingState]) -> str:
    """Confirm the booking after user approval."""
    state: BookingState = ctx.context
    if not state.all_slots_filled():
        return f"Cannot confirm. Missing: {state.get_missing_slots()}"
    for slot in state.slots.values():
        slot.confirmed = True
    state.flow_state = FlowState.EXECUTING
    return "Booking confirmed. Proceeding with execution."

@function_tool
async def cancel_flow(ctx: RunContextWrapper[BookingState]) -> str:
    """Cancel the current booking flow."""
    state: BookingState = ctx.context
    state.flow_state = FlowState.CANCELLED
    return "Booking cancelled."

The Conversational Agent

Wire the tools into an agent with instructions that guide the conversation flow.

booking_agent = Agent(
    name="booking_assistant",
    instructions="""You are a booking assistant. Follow this flow:

1. GREETING: Welcome the user and ask what service they need.
2. COLLECTING_INFO: Ask for missing information one field at a time.
   Use set_slot to record each piece of information.
   Required: date, time, service, name, phone.
3. CONFIRMING: Summarize the booking and ask the user to confirm.
4. EXECUTING: Tell the user the booking is confirmed.

Rules:
- Ask for ONE piece of information at a time.
- If the user provides multiple details in one message, set all of them.
- Always use get_state to check what is still missing.
- If the user wants to cancel, use cancel_flow.
- Be conversational and helpful, not robotic.""",
    tools=[set_slot, get_state, confirm_booking, cancel_flow],
)

Running Multi-Turn Conversations

The key to multi-turn flows is preserving conversation history and state across calls.

import asyncio
from agents.items import TResponseInputItem

async def run_booking_flow():
    state = BookingState()
    history: list[TResponseInputItem] = []

    print("Booking Assistant: Welcome! How can I help you today?")

    while state.flow_state not in (FlowState.COMPLETED, FlowState.CANCELLED):
        user_input = input("You: ")
        if not user_input.strip():
            continue

        history.append({"role": "user", "content": user_input})

        result = await Runner.run(
            booking_agent,
            input=history,
            context=state,
        )

        # Update history with full turn
        history = result.to_input_list()

        print(f"Assistant: {result.final_output}")

        if state.flow_state == FlowState.EXECUTING:
            state.flow_state = FlowState.COMPLETED
            print("\n--- Booking Complete ---")
            print(state.get_slot_summary())

asyncio.run(run_booking_flow())

Handling Edge Cases in Flows

Real conversations are messy. Users change their mind, provide partial information, or go off-topic.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

@function_tool
async def update_slot(ctx: RunContextWrapper[BookingState], slot_name: str, new_value: str) -> str:
    """Update a previously set slot value (user changed their mind)."""
    state: BookingState = ctx.context
    if slot_name not in state.slots:
        return f"Slot '{slot_name}' has not been set yet. Use set_slot instead."

    old_value = state.slots[slot_name].value
    state.slots[slot_name] = SlotValue(value=new_value, confirmed=False)
    # Reset to collecting state if we were in confirming
    if state.flow_state == FlowState.CONFIRMING:
        state.flow_state = FlowState.COLLECTING_INFO
    return f"Updated '{slot_name}' from '{old_value}' to '{new_value}'."

FAQ

How do I handle conversation timeouts?

Track a last_active timestamp in your state object. Before processing each turn, check if the elapsed time exceeds your timeout threshold. If it does, reset the state and start fresh with a greeting that acknowledges the gap — something like "It has been a while since we spoke. Would you like to continue where we left off?"

Can I mix free-form conversation with structured slot filling?

Yes. Design your agent instructions to handle both modes. When the user asks a question unrelated to the booking flow, the agent can answer it normally without calling any slot-filling tools. The state persists unchanged until the user returns to the flow. Include a get_state call periodically to remind the agent what information is still needed.

How do I validate slot values (e.g., date format, phone number)?

Add validation logic inside the set_slot tool. Before storing the value, parse and validate it. Return a clear error message if validation fails, and increment the attempts counter on the slot. If attempts exceed a threshold, offer the user an alternative format or skip that slot with a default.


#OpenAIAgentsSDK #ConversationalAI #StateManagement #SlotFilling #MultiTurn #Python #AgenticAI #LearnAI #AIEngineering

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Agentic AI

Parallel Tool Calling in the OpenAI Agents SDK: When It Helps, When It Hurts (2026)

OpenAI's parallel function calling can cut latency in half — or burn money on dependent calls. The architecture, code, and an eval that proves the win.

Agentic AI

Building OpenAI Realtime Voice Agents with an Eval Pipeline (2026)

Build a working voice agent with the OpenAI Realtime API + Agents SDK, then bolt on an eval pipeline that catches barge-in failures, hallucinated grounding, and latency regressions.

Agentic AI

Voice Agent Quality Metrics in 2026: WER, Latency, Grounding, and the Ones Most Teams Miss

The full metric set for evaluating production voice agents — STT word error rate, end-to-end latency budgets, RAG grounding, prosody, and the metrics that actually correlate with retention.

Agentic AI

Building Your First Agent with the OpenAI Agents SDK in 2026: A Hands-On Walkthrough

Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.

Agentic AI

OpenAI Agents SDK vs Assistants API in 2026: Migration Guide with Eval Parity

Honest principal-engineer comparison of the OpenAI Agents SDK and the legacy Assistants API, with a migration checklist and eval-parity strategy so you don't ship regressions.

Agentic AI

Tool Selection Accuracy: The Eval Most Teams Skip — and Should Not (2026)

Your agent picked the wrong tool 12% of the time and the final answer was still right. That's a latent bug. Here's the eval pipeline that surfaces it.