Tutorial: a support-ticket triage bot¶
Mini-project for: agent trees, TemplateSlots, subagents, and dispatch.
The problem¶
A single "do everything" support agent carries the billing policy, the debugging checklist, and the routing logic in one prompt — and every new domain makes it worse at all the others. What you actually want is a small tree: one dispatcher that reads the ticket and routes it, plus focused specialists that each carry only their own domain prompt.
What you'll build¶
A three-agent tree compiled to opencode artifacts:
triage— the primary dispatcher. Reads an incoming ticket, decides the category, and delegates via the Task tool.billing-specialist— a subagent that only knows billing.tech-specialist— a subagent that only knows troubleshooting, running on a different model than the rest of the tree.
Prerequisites: the framework installed
(installation), the opencode
CLI, and at least one configured provider. Adapted from
examples/40_subagents (closing pointer: examples/50_primary_dispatch);
the model strings match those examples — substitute your own freely.
Step 1 — define the specialists¶
Create a project directory with an agents.py. Each specialist is a
plain AgentDefinition with a tight, single-domain system prompt:
"""agents.py — support triage tree."""
from __future__ import annotations
from open_agent_compiler import (
AgentDefinition,
AgentHeader,
AgentRegistry,
AgentToolPermissions,
CompilationConfig,
ModelParameters,
TemplateSlot,
TemplateTree,
)
def _billing_specialist() -> AgentDefinition:
return AgentDefinition(
header=AgentHeader(
agent_id="billing-specialist",
name="billing-specialist",
description="Resolves billing and subscription questions.",
),
usage_explanation_long=(
"Receives a billing-related ticket from its parent and returns"
" a short, actionable answer: cause, next step, refund path."
),
usage_explanation_short="billing answers",
system_prompt=(
"You are a billing support specialist. Answer the ticket in at"
" most 4 sentences: likely cause, the customer's next step, and"
" whether a refund or credit applies. Never answer technical"
" questions."
),
)
def _tech_specialist() -> AgentDefinition:
return AgentDefinition(
header=AgentHeader(
agent_id="tech-specialist",
name="tech-specialist",
description="Diagnoses technical problems from ticket text.",
),
usage_explanation_long=(
"Receives a technical ticket and returns a numbered diagnosis:"
" most likely cause first, then the checks to run, in order."
),
usage_explanation_short="technical diagnosis",
system_prompt=(
"You are a technical support specialist. Reply with a short"
" numbered list: 1) most likely cause, 2-4) the checks to run"
" in order. Be concrete; never discuss billing."
),
)
Step 2 — define the dispatcher¶
The dispatcher references the specialists by AgentHeader with
mode="subagent" — that wiring lists them under "Available Subagents"
in the compiled prompt and scopes the Task tool to those two targets:
def _triage() -> AgentDefinition:
billing_ref = AgentHeader(
agent_id="billing-specialist", name="billing-specialist",
description="Billing and subscription questions.",
mode="subagent",
)
tech_ref = AgentHeader(
agent_id="tech-specialist", name="tech-specialist",
description="Technical problems and outages.",
mode="subagent",
)
return AgentDefinition(
header=AgentHeader(
agent_id="triage",
name="triage",
description="Routes support tickets to the right specialist.",
),
usage_explanation_long=(
"Reads an incoming ticket, classifies it as billing or"
" technical, and delegates via the Task tool. Never answers"
" inline."
),
usage_explanation_short="ticket router",
subagents=[billing_ref, tech_ref],
tool_permissions=AgentToolPermissions(),
system_prompt=(
"You are the support triage dispatcher.\n"
"\n"
"For every incoming ticket:\n"
"1. Classify it: BILLING or TECHNICAL.\n"
"2. Invoke the matching specialist via the Task tool, passing"
" the full ticket text as the prompt.\n"
"3. Reply with two labelled lines:\n"
" `ROUTED TO:` <specialist name>\n"
" `ANSWER:` <the specialist's reply, quoted verbatim>\n"
"\n"
"Never answer a ticket yourself — always delegate."
),
)
Step 3 — register the tree¶
Slots in a TemplateTree name the positions in the tree: the primary
slot becomes the entry-point agent; every other slot compiles in
subagent mode by default. The tech specialist runs on a different
provider — mixed-model trees compile and run unchanged:
def registry() -> AgentRegistry:
reg = AgentRegistry()
triage_id = reg.register_agent(
"triage", _triage(),
ModelParameters(model_name="zai-coding-plan/glm-4.5-air", temperature=0.2),
)
billing_id = reg.register_agent(
"billing-specialist", _billing_specialist(),
ModelParameters(model_name="zai-coding-plan/glm-4.5-air", temperature=0.3),
)
tech_id = reg.register_agent(
"tech-specialist", _tech_specialist(),
ModelParameters(model_name="local-vllm-remote/qwen35-27b", temperature=0.5),
)
reg.register_template(TemplateTree(
name="support-tpl",
slots=[
TemplateSlot(name="primary", default_agent_id=triage_id),
TemplateSlot(name="billing-specialist", default_agent_id=billing_id),
TemplateSlot(name="tech-specialist", default_agent_id=tech_id),
],
))
reg.create_compilation_config(
CompilationConfig(name="prod", template_name="support-tpl"),
)
return reg
Step 4 — compile it¶
Create build_agents.py next to agents.py:
from pathlib import Path
import sys
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from agents import registry
from open_agent_compiler.compiler.script import CompileScript
def main() -> None:
script = CompileScript(target=HERE / "build", factory=registry,
config="prod", clean=True, verbose=True)
result = script.run()
print(f"wrote {len(result.written_files)} file(s); slots: {result.resolved_slots}")
if __name__ == "__main__":
main()
Run it¶
uv run python build_agents.py
cd build
opencode run --agent primary "I was charged twice for my subscription this month."
The compile step reports one markdown artifact per slot, e.g.:
wrote 3 file(s); slots: ['primary', 'billing-specialist', 'tech-specialist']
and the run produces a routed answer shaped like:
ROUTED TO: billing-specialist
ANSWER: This looks like a duplicate charge from an overlapping renewal. ...
Try a technical ticket ("The app crashes on startup after the update")
and watch the routing flip to tech-specialist. Peek at
build/.opencode/agents/primary.md — its SECURITY POLICY block lists
exactly the two allowed Task targets.
Why it works¶
The tree is data, not orchestration code: the dispatcher declares its
children as AgentHeader references, the TemplateTree maps agents
onto named slots, and the compiler emits opencode artifacts where the
Task tool is scoped to precisely the declared subagents. Each
specialist keeps a small single-domain prompt, so adding a fourth
category is one new definition plus one new slot.
Going further¶
- Subagents share the parent's session and complete in seconds. For
long-running or parallel workers with their own identity, reference
the child with
mode="primary"instead — the compiler bundles a bash dispatcher (opencode_manager.py) so the coordinator shells out to a fresh session per worker (examples/50_primary_dispatch). - Workflows and subagents guide
- The agent model
- Registry and compilation