Routing
Routing lets the LLM decide what happens after a task completes. There are two routing mechanisms: conditional (router) and unconditional (send_to).
Choosing Between depends_on, router, and send_to
These three mechanisms look similar but solve different problems. Pick the wrong one and you’ll either lose context, fire work that shouldn’t run, or hang the mission.
Default to depends_on. Build a static DAG whenever you can.
A depends_on edge tells the runner “this task waits for those tasks, then runs.” The runner knows about it upfront from the topological sort, schedules it automatically, and gives it full ancestor context. This is the right tool for the vast majority of task relationships — sequential pipelines, fan-in from multiple predecessors, anything where every listed predecessor is expected to run.
Reach for send_to or router only when a task genuinely should not always run — i.e. when the set of downstream tasks depends on what happened at runtime.
| You want to… | Use |
|---|---|
| Run task B after task A always completes | depends_on = [tasks.A] on B |
| Run task C after A and B both complete (fan-in, both always run) | depends_on = [tasks.A, tasks.B] on C |
| Pick one of several branches based on LLM judgment | router on the source |
| Fan out — source always triggers multiple parallel branches | send_to on the source |
| Multiple conditional sources converge on one shared handler (first-one-wins fan-in) | send_to (or router) from each source, pointing at the shared task |
| Chain a sequence where every step always runs | depends_on all the way — not send_to |
Common mistake: using send_to for a static sequence
# ❌ Wrong — send_to used for a linear pipeline that always runs end-to-end
task "fetch" {
objective = "Fetch data"
send_to = [tasks.process]
}
task "process" {
objective = "Process it"
send_to = [tasks.publish]
}
task "publish" {
objective = "Publish result"
}This works, but it throws away the benefits of the static DAG. process and publish become dynamically activated — they can’t be referenced by depends_on from anywhere else, they’re invisible to the topological sort, and the runner has no upfront picture of the mission. Prefer:
# ✅ Right — static DAG, every task is schedulable and discoverable
task "fetch" {
objective = "Fetch data"
}
task "process" {
objective = "Process it"
depends_on = [tasks.fetch]
}
task "publish" {
objective = "Publish result"
depends_on = [tasks.process]
}Rule of thumb: if every branch would always run, it belongs in depends_on. send_to and router are for runtime-shaped graphs — fan-out that’s conceptually “trigger these in parallel,” or fan-in where which predecessor fires is decided at runtime.
Conditional Routing (router)
A router block presents route options to the commander after the task finishes. The commander evaluates the conditions and picks one — or none.
task "classify" {
objective = "Classify the incoming request"
router {
route {
target = tasks.handle_billing
condition = "The request is about billing or payments"
}
route {
target = tasks.handle_support
condition = "The request is a technical support issue"
}
route {
target = tasks.handle_general
condition = "The request doesn't fit other categories"
}
}
}
task "handle_billing" { objective = "Process billing request" }
task "handle_support" { objective = "Handle the support ticket" }
task "handle_general" { objective = "Handle general inquiry" }Only the chosen branch executes. If the commander picks “none”, no branch activates and the mission completes.
How It Works
Route options are injected as a system prompt when the commander starts, so it knows the available routes upfront. When calling task_complete, the commander includes a route parameter alongside the summary:
- Commander sees route options in its system prompt (injected automatically from the
routerconfig) - Commander does its work, delegating to agents as needed
- Commander calls
task_completewithsummaryandroute— choosing the route whose condition best matches the results, ornoneif no route applies - The chosen route’s target task is activated; unchosen branches never execute
This happens in a single tool call — no extra turns needed for route selection.
Chained Routers
Route targets can themselves have routers. This creates natural decision chains:
task "classify" {
objective = "Classify the request type"
router {
route {
target = tasks.handle_billing
condition = "Billing related"
}
}
}
task "handle_billing" {
objective = "Determine the billing action"
router {
route {
target = tasks.process_refund
condition = "Customer wants a refund"
}
route {
target = tasks.correct_invoice
condition = "Invoice needs correction"
}
}
}
task "process_refund" {
objective = "Process the refund"
}
task "correct_invoice" {
objective = "Fix the invoice"
}Unconditional Routing (send_to)
send_to activates target tasks immediately when the source completes. No LLM decision — all targets fire.
task "fetch" {
objective = "Fetch data from the API"
send_to = [tasks.process_a, tasks.process_b]
}
task "process_a" { objective = "Process path A" }
task "process_b" { objective = "Process path B" }Use send_to when:
- Fan-out — one task triggers multiple parallel branches that each do distinct work. (If they do the same work across items, use an iterator instead.)
- Conditional fan-in onto a shared handler — a router (or chain of routers) has several branches that all need to end at the same downstream task. Only one branch actually runs, so the shared task can’t use
depends_on(it would hang waiting on the branches that were never activated). Instead each branchsend_tos the shared task; whichever branch runs activates it.
# A router picks one of three handlers; every handler ends with the same notify step.
task "classify" {
objective = "Classify the incoming ticket"
router {
route {
target = tasks.handle_billing
condition = "Billing issue"
}
route {
target = tasks.handle_bug
condition = "Technical bug"
}
route {
target = tasks.handle_general
condition = "General inquiry"
}
}
}
task "handle_billing" {
objective = "Resolve billing issue"
send_to = [tasks.notify]
}
task "handle_bug" {
objective = "File a bug report"
send_to = [tasks.notify]
}
task "handle_general" {
objective = "Answer the inquiry"
send_to = [tasks.notify]
}
task "notify" {
objective = "Notify the customer that the ticket was handled"
}notify can’t be depends_on = [handle_billing, handle_bug, handle_general] — that would wait for all three, but the router only activates one. send_to from each branch is the right shape: whichever handler fires pushes into notify.
Don’t use send_to as a substitute for depends_on in a linear pipeline where every step always runs — that makes the graph dynamic for no reason and prevents downstream tasks from waiting on those steps. See Choosing Between….
Cross-Mission Routing
Route targets can reference other missions using missions.name instead of tasks.name. When a mission route is chosen, the current mission completes and the target mission launches as a new instance.
task "handle_complaint" {
objective = "Draft a response to the complaint"
router {
route {
target = missions.escalation_mission
condition = "The complaint is severe and needs escalation"
}
route {
target = tasks.close_ticket
condition = "The complaint can be resolved without escalation"
}
}
}When the commander selects a mission route, it also provides any required inputs for the target mission. The task_complete tool presents the target mission’s input requirements and the commander fills them in.
A mission can route to itself — this creates a new instance, not a loop. This is useful for retry or restart patterns.
Full Example
mission "escalation_mission" {
directive = "Handle an escalated complaint"
commander {
model = models.anthropic.claude_haiku_4_5
}
agents = [agents.assistant]
input "complaint_summary" {
type = "string"
description = "Summary of the original complaint"
}
input "severity" {
type = "string"
description = "Severity level"
default = "high"
}
task "triage" {
objective = <<-EOT
Perform high-priority triage for the escalated complaint.
Complaint: "${inputs.complaint_summary}"
Severity: "${inputs.severity}"
EOT
}
}
mission "support_triage" {
directive = "Triage incoming customer messages"
commander {
model = models.anthropic.claude_haiku_4_5
}
agents = [agents.assistant]
input "customer_message" {
type = "string"
description = "The customer's message"
}
task "analyze" {
objective = "Analyze the customer message: ${inputs.customer_message}"
router {
route {
target = tasks.handle_complaint
condition = "The customer is unhappy"
}
}
}
task "handle_complaint" {
objective = "Draft a response to the complaint"
router {
route {
target = missions.escalation_mission
condition = "The complaint mentions legal action or cancellation"
}
route {
target = tasks.close_ticket
condition = "The complaint can be resolved directly"
}
}
}
task "close_ticket" {
objective = "Close the support ticket with a summary"
}
}Static vs Dynamic Tasks
Tasks are either statically scheduled (part of the initial topological sort) or dynamically activated (only run when a router or send_to fires).
A dynamically activated task is the root of its own sub-DAG. These two worlds do not mix:
- Dynamic targets cannot have
depends_on. They start when activated, not when some other set of tasks completes. - No task can
depends_ona dynamic target. If a dynamic target is never activated, the dependent task would hang forever. routerandsend_toare mutually exclusive. A task pushes work either conditionally or unconditionally, not both.
First-One-Wins
A dynamic target can be referenced by multiple routers or send_to sources. The first activation wins — once a task starts, subsequent activations are silently ignored. Tasks run at most once.
Ancestor Context
When a dynamically activated task starts, the runner treats the activating task as its parent for context purposes. The routed-to task gets access to the full ancestry of the task that activated it — the same depth of context as if it had depends_on.
Validation Rules
| Rule | Description |
|---|---|
| No cycles | Combined depends_on + router + send_to edges must be acyclic |
Dynamic targets cannot have depends_on | Tasks reachable via router or send_to cannot also declare dependencies |
No task can depends_on a dynamic target | Static tasks cannot wait on dynamically activated tasks |
router and send_to mutually exclusive | A task can have one or the other, not both |
| No self-routing / self-send | A task cannot target itself |
| No duplicate targets | Each target can appear at most once within a router or send_to |
| Targets must exist | Task targets must reference existing tasks; mission targets must reference existing missions |
| Parallel iterators cannot have a router | Sequential iterators can — the route is evaluated after the final iteration |
| At least one startable task | Every mission must have at least one task with no dependencies that isn’t router-only |
Iterator Interaction
- Parallel iterators cannot have a
router(each iteration is independent — there’s no single decision point) - Sequential iterators can have a
router— the route is evaluated after the final iteration completes - Both parallel and sequential iterators can use
send_to— targets activate after iteration completes
Persistence & Resume
Route decisions are persisted to the database. On resume:
- Route decisions are loaded to reconstruct which task activated which
- Incomplete dynamic targets are re-queued for execution
- The activating task’s commander is restored so the dynamic target can query it for context
See Also
- Tasks - Task configuration and dependencies
- Internal Tools -
task_completerouting parameters - Missions Overview - Mission structure and execution