A good support bot is not one giant prompt. It is a small team: a front desk that reads the message, a specialist for order status, another for refunds, one for FAQs, and a clean path to a human when things get hard. Build it that way and each part stays simple and testable.
In this tutorial you build exactly that — a customer support AI agent in Python, end to end, on the OpenAI Agents SDK. You wire an order-lookup tool, three specialist agents, a triage router that hands off between them, a human-escalation path, and memory so the chat remembers context. Every snippet runs as-is. By the end you will have a working support agent and a clear map of where to plug in your real systems.
- New to the SDK? Do the OpenAI Agents SDK tutorial first
- Handoffs are the core here — the handoffs deep-dive covers them in full
- An OpenAI API key set in your environment — see the API key primer
- Model support as a team, not a mega-prompt: a triage router plus small specialist agents.
- Tools connect the agent to reality — an order-lookup function tool returns live status.
- Handoffs do the routing; a human-escalation handoff gives you a safe exit hatch.
- A session gives the bot memory, so “I want a refund for it” resolves to the right order.
What your customer support AI agent will do#
The design is a triage router in front of specialists. The triage agent never answers directly. It reads the message and hands off to the specialist that fits: order status, refunds, or FAQs. When nothing fits or the customer is upset, it escalates to a human path.
Each specialist stays small and focused, which makes the whole system easy to test and cheap to run. That is the payoff of the team model over one sprawling prompt.
Step 1: The order-lookup tool#
Specialists need real data. Start with a tool that looks up an order. Here it reads a dictionary; in your app it would call your database or API.
from agents import function_tool
# Stand-in for your real orders database.
ORDERS = {
"A1001": {"status": "shipped", "eta": "2 days"},
"A1002": {"status": "processing", "eta": "5 days"},
}
@function_tool
def lookup_order(order_id: str) -> str:
"""Look up the status of a customer order.
Args:
order_id: The order ID, for example "A1001".
"""
order = ORDERS.get(order_id)
if not order:
return f"No order found with ID {order_id}."
return f"Order {order_id} is {order['status']} (ETA {order['eta']})."The docstring is the tool’s spec — the model reads it to decide when to call lookup_order and what to pass. Keep it specific. To connect real data, swap the dictionary for your database call and leave the rest unchanged.
Step 2: The specialist agents#
Now define the specialists. Each gets a focused instruction and a handoff_description the router uses to route. The order agent gets the tool; the others do not need it.
from agents import Agent
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
order_agent = Agent(
name="Order Status Agent",
handoff_description="Checks order status and shipping ETA.",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
"Help with order status. Use lookup_order for the latest status.",
tools=[lookup_order],
)
refund_agent = Agent(
name="Refund Agent",
handoff_description="Handles refunds and billing disputes.",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
"Resolve refund and billing questions. Be clear about timelines.",
)
faq_agent = Agent(
name="FAQ Agent",
handoff_description="Answers general product and policy questions.",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
"Answer common product and policy questions concisely.",
)The RECOMMENDED_PROMPT_PREFIX primes each agent to understand handoffs, which noticeably improves routing. Skip it and agents sometimes answer when they should transfer.
Step 3: The triage router#
The triage agent ties it together. It gets the specialists in handoffs and one job: route.
import asyncio
from agents import Agent, Runner
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
triage_agent = Agent(
name="Support Triage",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
"You are front-line support. Route each message to the right specialist.",
handoffs=[order_agent, refund_agent, faq_agent],
)
async def main():
result = await Runner.run(triage_agent, "Where is my order A1001?")
print(result.final_output)
print("Handled by:", result.last_agent.name)
if __name__ == "__main__":
asyncio.run(main())Run it and the router transfers to the order agent, which calls the tool and answers. result.last_agent.name tells you which specialist replied — log it so you can see how traffic splits. The handoffs deep-dive covers customizing this routing further.
Step 4: Escalate to a human#
Some messages should not be auto-resolved. Add an escalation handoff that captures a reason and priority, then fires a callback where you would page a human.
from pydantic import BaseModel
from agents import Agent, handoff, RunContextWrapper
class Escalation(BaseModel):
reason: str
priority: str # "low" | "normal" | "high"
human_agent = Agent(
name="Human Escalation",
instructions="Summarize the issue for a teammate and reassure the customer.",
)
async def on_escalate(ctx: RunContextWrapper[None], data: Escalation):
# Push to your ticketing system or on-call queue here.
print(f"[ESCALATION] {data.priority.upper()}: {data.reason}")
escalation = handoff(human_agent, on_handoff=on_escalate, input_type=Escalation)Add escalation to the triage agent’s handoffs list and the router can now escalate with structured context. The input_type forces the model to state a reason and priority, so your on-call queue gets a real ticket, not a shrug. New to Pydantic models? The BaseModel primer is a quick read.
Step 5: Give it memory#
A real chat has more than one turn. Wrap runs in a session and the agent remembers earlier messages, so a vague follow-up still resolves.
import asyncio
from agents import Runner, SQLiteSession
async def main():
session = SQLiteSession("customer_A1001", "support.db")
await Runner.run(triage_agent, "My order is A1001.", session=session)
result = await Runner.run(triage_agent, "Actually, I want a refund for it.", session=session)
print(result.final_output)
print("Handled by:", result.last_agent.name)
if __name__ == "__main__":
asyncio.run(main())The second message says “it,” not the order ID — but the session remembers A1001 from the first turn, and the router sends it to refunds. Sessions turn disconnected calls into a real conversation with almost no extra code.
Guardrails and going to production#
A demo that works is not a bot you trust with customers. Two additions matter most before launch. First, add guardrails so the agent refuses out-of-scope or unsafe requests — the SDK runs input guardrails on the first agent and output guardrails on the final one. Second, ground the FAQ agent in your real help center with retrieval (RAG) instead of the model’s general knowledge.
Never let a support agent take an irreversible action — issuing a refund, cancelling an order — without a check. Keep the money-moving step behind a human approval or a tool that only proposes the action. The agent drafts; a person or a guarded tool commits.
Treat the demo as the skeleton and your tools, guardrails, and knowledge base as the muscle. That is the line between a toy and something you would put in front of customers. Before you ship, add evals so a routing or answer regression fails a test, not a customer.
Common mistakes#
- One overloaded agent. Cramming orders, refunds, and FAQs into a single prompt makes it mediocre at all three. Split into specialists.
- Vague handoff descriptions. The router routes on them. Name each specialist’s domain precisely.
- Auto-committing money. Refunds and cancellations need a guard or a human. Let the agent draft, not execute.
- No memory. Without a session, “it” and “that order” lose their meaning between turns.
- Shipping without evals. A prompt tweak can silently break routing. Catch it in a test.
Summary#
You built a customer support AI agent as a small team: a triage router, specialists for orders, refunds, and FAQs, a tool for real data, a human-escalation path with structured context, and session memory. The structure is the point — each piece is simple, and you extend it by swapping in your own tools and knowledge base, not by growing one giant prompt. Add guardrails and evals, and this skeleton is ready for real customers.
- Need the fundamentals? Start with the OpenAI Agents SDK tutorial.
- Go deeper on routing? Read the handoffs deep-dive.
- New to agents? See What Are AI Agents? and the best agent frameworks in 2026.
Building one for your own product? Tell me your specialist list — orders, refunds, shipping, whatever — and I’ll tell you where the routing tends to get confused.

