Split the Brain: Build an Issue Triage Bot with the Router Strategy
This is Part 8 of an 8-part series covering every multi-agent strategy in Agentspan. Today: the router strategy — a dedicated classifier agent decides which specialist handles the task.
In Part 1, we built a sequential pipeline where each agent’s output fed into the next. In Part 2, all reviewers ran simultaneously. In Part 3, the LLM decided which agent ran. In Part 4, agents transferred work between each other. In Part 5, agents took turns in a fixed rotation. In Part 6, agents were selected randomly. In Part 7, the human picked which agent ran.
The handoff strategy in Part 3 worked. The LLM read the issue, picked the right handler, and delegated. But the triage agent was doing two jobs at once — understanding the issue AND choosing the handler. The same expensive model that runs the specialists was also doing the routing. For a single issue, that is fine. For a backlog of 500 issues overnight, you are paying GPT-4o prices for a classification task that GPT-4o-mini could handle.
That is the router strategy. You split the routing decision into a separate agent — a dedicated classifier that does nothing but classify. The classifier picks the specialist. The specialist does the work. Two brains, each doing what it is good at.
What is Agentspan?
Agentspan is an orchestration layer for building, bringing, and observing AI agents as durable workflows.
- Build: define agents with the Agentspan SDK using
Agent,@tool, and multi-agent strategies. Agentspan compiles them into server-side workflows that survive crashes. - Bring: if you already use frameworks such as LangGraph, OpenAI Agents SDK, or Google ADK, you can run those agents through Agentspan and add durability and orchestration on top.
- Observe: every execution is inspectable in the dashboard. You can see agent flows, inputs, outputs, tool calls, token usage, and failures.
Setup
Two commands:
pip install agentspan
agentspan server start
This starts a local Agentspan server with a dashboard at http://localhost:6767.
What we are building
Same issue triage bot from Part 3. Three specialists:
- Bug Handler: analyzes severity, affected component, repro steps
- Feature Handler: summarizes the request, estimates complexity
- Docs Handler: identifies the gap, drafts a reply
But now the routing decision is handled by a separate classifier agent running on a cheaper, faster model (gpt-4o-mini). The classifier reads the issue, outputs an agent name, and the router dispatches to that specialist.
Issue -> [Classifier (gpt-4o-mini)] -> "bug_handler"
|
[Bug Handler (gpt-4o)]
How is this different from handoff?
In Part 3, the triage agent looked like this:
triage = Agent(
name="triage",
model="openai/gpt-4o",
agents=[bug_handler, feature_handler, docs_handler],
strategy=Strategy.HANDOFF,
instructions="You are an issue triage bot. Read the issue. Hand off to ONE agent...",
)
The parent agent’s LLM did everything — read the issue, decided the route, and delegated. The routing happened implicitly, as part of the parent’s reasoning.
With the router strategy, routing is explicit. A separate agent handles it:
classifier = Agent(
name="classifier",
model="openai/gpt-4o-mini", # cheap, fast model
instructions="Read the issue. Reply with one agent name: bug_handler, feature_handler, or docs_handler.",
)
triage = Agent(
name="triage",
agents=[bug_handler, feature_handler, docs_handler],
strategy=Strategy.ROUTER,
router=classifier, # separate agent makes the call
)
| Handoff | Router | |
|---|---|---|
| Who decides | Parent agent’s LLM | Separate classifier agent |
| Model flexibility | Same model routes and works | Cheap model routes, expensive model works |
| Routing control | Implicit — embedded in LLM reasoning | Explicit — classifier has one job |
| Cost at scale | Expensive model every routing decision | Cheap model classifies, expensive model works |
Handoff is great when routing is nuanced and context-heavy. Router is great when classification is straightforward and you want to control it.
Defining the specialists
Three agents, same as Part 3:
from agentspan.agents import Agent, AgentRuntime, Strategy
bug_handler = Agent(
name="bug_handler",
model="openai/gpt-4o",
instructions=(
"You handle bug reports. Read the issue CAREFULLY.\n\n"
"You MUST output EXACTLY this format and NOTHING else:\n\n"
"Severity: P0/P1/P2/P3\n"
"Component: <which part>\n"
"Repro steps: <what the user described, or 'Not provided'>\n"
"Labels: bug, <severity>\n"
"Engineering summary: <2-3 sentences>\n\n"
"ONLY use information the user actually wrote. No guesses."
),
)
feature_handler = Agent(
name="feature_handler",
model="openai/gpt-4o",
instructions=(
"You handle feature requests. Read the issue CAREFULLY.\n\n"
"You MUST output EXACTLY this format and NOTHING else:\n\n"
"Request: <one sentence>\n"
"Use case: <in the user's words, or 'No use case provided'>\n"
"Complexity: small/medium/large\n"
"Labels: enhancement, <area>\n"
"Community summary: <2-3 sentences>\n\n"
"ONLY use information the user actually wrote. No guesses."
),
)
docs_handler = Agent(
name="docs_handler",
model="openai/gpt-4o",
instructions=(
"You handle docs issues and questions. Read the issue CAREFULLY.\n\n"
"You MUST output EXACTLY this format and NOTHING else:\n\n"
"Confusion: <what the user is stuck on>\n"
"Doc gap: <which doc page is missing or unclear>\n"
"Draft reply: <under 50 words — acknowledge the gap>\n"
"Labels: documentation\n\n"
"ONLY describe the gap. Do NOT answer the technical question."
),
)
The classifier
A dedicated agent whose only job is classification:
classifier = Agent(
name="classifier",
model="openai/gpt-4o-mini",
instructions=(
"You are an issue classifier. Read the issue and reply with "
"EXACTLY ONE of these agent names — nothing else:\n\n"
"- bug_handler: error, crash, traceback, regression, broken behavior\n"
"- feature_handler: feature request, suggestion, enhancement\n"
"- docs_handler: docs question, missing or unclear documentation\n\n"
"Reply with the agent name only. No explanation. No punctuation."
),
)
Notice the model: gpt-4o-mini. The classifier does not need a frontier model. It reads the issue, outputs one word, and is done.
The instructions are deliberately tight — reply with the agent name only. This matters because the router uses the classifier’s output to select the specialist. If the classifier returns “I think this should go to bug_handler because…” the routing breaks. One word. That is all it needs.
The router strategy
triage = Agent(
name="triage",
agents=[bug_handler, feature_handler, docs_handler],
strategy=Strategy.ROUTER,
router=classifier,
)
Compare all eight strategies:
# Sequential: fixed order, each runs once
pipeline = a >> b >> c
# Parallel: all run at once
team = Agent(agents=[a, b, c], strategy=Strategy.PARALLEL)
# Handoff: parent LLM picks one
triage = Agent(agents=[a, b, c], strategy=Strategy.HANDOFF)
# Router: separate classifier picks one
triage = Agent(agents=[a, b, c], strategy=Strategy.ROUTER, router=classifier)
# Swarm: agents transfer between each other
team = Agent(agents=[a, b, c], strategy=Strategy.SWARM)
# Round robin: fixed rotation
debate = Agent(agents=[a, b, c], strategy=Strategy.ROUND_ROBIN, max_turns=6)
# Random: random selection each turn
brainstorm = Agent(agents=[a, b, c], strategy=Strategy.RANDOM, max_turns=6)
# Manual: human picks each turn
workflow = Agent(agents=[a, b, c], strategy=Strategy.MANUAL, max_turns=4)
Same Agent class. Eight strategies. One primitive.
Running it
with AgentRuntime() as runtime:
result = runtime.run(
triage,
"File upload fails with a 500 error when the filename has spaces. "
"Uploading 'report.pdf' works, but 'Q1 report.pdf' returns a server "
"error. Looks like the filename isn't being URL-encoded.",
)
result.print_result()
The classifier reads the issue, outputs bug_handler. The router dispatches. The bug handler produces:
Severity: P1
Component: File Upload Service
Repro steps: Upload a file with spaces in the filename like
'Q1 report.pdf'. Returns a 500 error.
Labels: bug, P1
Engineering summary: The File Upload Service fails to handle
filenames with spaces, leading to a 500 server error. Likely
a URL encoding issue with filenames containing spaces.
Swap the input to a feature request — the classifier outputs feature_handler. Swap it to a docs question — docs_handler. Same triage bot, same code. The classifier decides each time.
Using a Python function instead
You do not have to use an LLM for classification. If your categories are well-defined, a plain Python function works:
def classify(prompt: str) -> str:
lower = prompt.lower()
if any(word in lower for word in ["error", "crash", "fail", "bug", "broken", "500"]):
return "bug_handler"
elif any(word in lower for word in ["feature", "request", "suggestion"]):
return "feature_handler"
elif any(word in lower for word in ["docs", "documentation", "unclear", "missing"]):
return "docs_handler"
return "bug_handler"
triage = Agent(
name="triage",
agents=[bug_handler, feature_handler, docs_handler],
strategy=Strategy.ROUTER,
router=classify,
)
Zero LLM calls for routing. Zero cost. Deterministic. The same router= parameter accepts either an Agent or a callable. Use the LLM classifier when categories are fuzzy. Use a function when they are clear.
How durability works
The router compiles into a durable server-side workflow. If your process crashes after the classifier picks bug_handler but before bug_handler finishes:
- The classification decision is persisted on the server.
- You restart your script.
bug_handlerresumes — no re-classification, no re-reading the issue.
The classifier does not re-run. The routing decision was already made and saved.
Composability
The router composes with other strategies:
# Fetch issue from GitHub, then route to the right specialist
pipeline = fetcher >> triage
# Router picks a path, then sequential post-processing
pipeline = fetcher >> triage >> summarizer
# Router picks between a quick answer and a full research pipeline
research_pipeline = Agent(
name="research_pipeline",
agents=[researcher, writer],
strategy=Strategy.SEQUENTIAL,
)
team = Agent(
agents=[quick_answer, research_pipeline],
strategy=Strategy.ROUTER,
router=classifier,
)
The classifier picks the path. If it picks research_pipeline, the researcher and writer run in sequence. Same Agent class throughout.
The full series
This completes the 8-part series on multi-agent strategies in Agentspan:
| Part | Strategy | Pattern | Who decides |
|---|---|---|---|
| 1 | Sequential | a >> b >> c | Nobody — fixed order |
| 2 | Parallel | All at once | Nobody — fan out |
| 3 | Handoff | One runs | Parent LLM |
| 4 | Swarm | Peer-to-peer transfers | Each agent |
| 5 | Round Robin | Fixed rotation | Predetermined order |
| 6 | Random | Random selection | Randomness |
| 7 | Manual | Human picks | The human |
| 8 | Router | One runs | Separate classifier |
Eight strategies. One Agent class. One strategy= parameter. Every coordination pattern is a configuration choice, not a framework change.
Try it
pip install agentspan
agentspan server start
python 04_router_triage.py
- GitHub: github.com/agentspan-ai/agentspan
- Blog examples: router examples
- Docs: agentspan.ai/docs


