Create specialized agents and a router
On this page
From theory to practice
The previous lesson introduced the concept of handoffs. Now you will build the complete system in the actual order of construction: first the specialists, then the router, then the wiring between them, and finally the conversation that sets everything in motion. This order isn’t arbitrary—the router can only point to agents that already exist.
Step 1: Create the specialist agents
Each agent receives a precise domain of expertise and nothing else.
from mistralai.client import Mistral
from mistralai.client.models import CompletionArgs, ResponseFormat, JSONSchema
import os
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
# Web search agent
web_agent = client.beta.agents.create(
model="mistral-large-latest",
name="web-search-agent",
description="Agent that searches for information online.",
instructions="Perform precise web searches. Always cite your sources.",
tools=[{"type": "web_search"}]
)
# Finance agent
finance_agent = client.beta.agents.create(
model="mistral-large-latest",
name="finance-agent",
description="Agent specialized in financial questions.",
instructions="""You are a financial expert.
- Use numerical data
- Explain concepts clearly
- Flag risks"""
)
# Calculator agent
calculator_agent = client.beta.agents.create(
model="mistral-large-latest",
name="calculator-agent",
description="Agent for detailed mathematical calculations.",
instructions="Explain each step of the calculation. Use the code interpreter for complex calculations.",
tools=[{"type": "code_interpreter"}]
)
These four fields play very different roles, and one deserves special attention. The name in kebab-case is for you: it appears in your logs and tests, and a readable name saves you from comparing opaque IDs. The instructions address the model once it starts working, so they must be domain-specific—“flag risks” means something to a finance agent, while “be helpful” means nothing to anyone. The tools follow the principle of strict necessity: the finance agent above has no tools because it reasons without needing them, and giving it tools would just invite misuse. Finally, the description isn’t documentation: it’s the text the router reads to decide whether to transfer here or elsewhere.
Step 2: Create the router agent
The router has no tools. Its entire job consists of analyzing the request and choosing the recipient.
router_agent = client.beta.agents.create(
model="mistral-large-latest",
name="router-agent",
description="Router agent that distributes requests.",
instructions="""You are an orchestrator.
Analyze each request and transfer to the right agent:
- Questions requiring online data → web-search-agent
- Financial questions → finance-agent
- Calculations and numerical analysis → calculator-agent
Never answer directly—always transfer."""
)
That last line prevents the system from collapsing. Without it, a router built on a large model will answer questions that seem easy to it, bypass your specialists, and produce responses without the tools they would have used. You end up with a multi-agent architecture where half the agents never get used.
Step 3: Define handoff targets
Now you need to tell the router which agents it is allowed to transfer to.
router_agent = client.beta.agents.update(
agent_id=router_agent.id,
handoffs=[web_agent.id, finance_agent.id, calculator_agent.id]
)
The handoffs parameter takes a list of agent IDs, and the router can transfer to any of them. This isn’t a privilege reserved for the router: specialists can also declare their own targets.
# The finance agent can transfer to web search and the calculator
finance_agent = client.beta.agents.update(
agent_id=finance_agent.id,
handoffs=[web_agent.id, calculator_agent.id]
)
A chain like router → finance → web search → calculator then becomes possible. When the finance agent faces a question that requires a current rate, it doesn’t hand back to the router: it goes directly to the specialist it needs, just like a consultant calling a colleague rather than going back through reception.
Step 4: Start the conversation
response = client.beta.conversations.start(
agent_id=router_agent.id,
inputs="Find the current ECB key interest rate and calculate compound interest over 10 years for €100,000."
)
# Analyze the flow
for entry in response.outputs:
if entry.type == "agent.handoff":
print(f"[HANDOFF] → {entry.next_agent_id}")
elif entry.type == "tool.execution":
print(f"[TOOL] {entry.tool_name}")
elif entry.type == "message.output":
print(f"[RESPONSE] {entry.content[:200]}...")
Run this request and observe the trace before looking at the answer. You should see two handoffs and at least one tool execution; if you see none, your router answered on its own, and it’s time to tighten its instructions.
The main lever: the description
When routing fails, the description is almost always to blame. The router doesn’t know your intentions; it reads only this text for each candidate agent.
# Bad — too vague
description="A useful agent."
# Good — the router knows when to transfer
description="Agent that searches for current information on the Internet and cites its sources."
Write each description as an answer to the question “in what case should I call this agent?” rather than as a summary of what it is. The second example names the situation—current information, on the Internet—while the first leaves the router guessing. Also verify that no pair of descriptions overlaps: two agents described in similar terms will produce unstable routing from one execution to the next.
Complete example
Here is the condensed version, with handoffs passed directly during router creation since the specialists already exist at that point.
from mistralai.client import Mistral
import os
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
# 1. Specialist agents
research = client.beta.agents.create(
model="mistral-large-latest",
name="research-agent",
description="Online information search with sources.",
tools=[{"type": "web_search"}]
)
analyst = client.beta.agents.create(
model="mistral-large-latest",
name="analyst-agent",
description="Data analysis and calculations with Python.",
tools=[{"type": "code_interpreter"}]
)
writer = client.beta.agents.create(
model="mistral-large-latest",
name="writer-agent",
description="Drafting structured and professional reports.",
instructions="Write clear reports with an introduction, analysis, and recommendations."
)
# 2. Router with handoffs
router = client.beta.agents.create(
model="mistral-large-latest",
name="router",
description="Orchestrates complex requests.",
instructions="Analyze the request and transfer to the appropriate specialist.",
handoffs=[research.id, analyst.id, writer.id]
)
# 3. Launch
response = client.beta.conversations.start(
agent_id=router.id,
inputs="Analyze the European cloud computing market in 2026: size, players, trends."
)
Key takeaways
- Create specialist agents with precise, task-oriented descriptions
- The router uses descriptions to choose the target agent
client.beta.agents.update(handoffs=[...])defines transfer targets- Handoffs can be nested (agent A → agent B → agent C)
- Each agent’s description is the primary lever for routing quality