Skip to Content
MissionsRouting

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 completesdepends_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 judgmentrouter on the source
Fan out — source always triggers multiple parallel branchessend_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 runsdepends_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:

  1. Commander sees route options in its system prompt (injected automatically from the router config)
  2. Commander does its work, delegating to agents as needed
  3. Commander calls task_complete with summary and route — choosing the route whose condition best matches the results, or none if no route applies
  4. 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:

  1. 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.)
  2. 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 branch send_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_on a dynamic target. If a dynamic target is never activated, the dependent task would hang forever.
  • router and send_to are 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

RuleDescription
No cyclesCombined depends_on + router + send_to edges must be acyclic
Dynamic targets cannot have depends_onTasks reachable via router or send_to cannot also declare dependencies
No task can depends_on a dynamic targetStatic tasks cannot wait on dynamically activated tasks
router and send_to mutually exclusiveA task can have one or the other, not both
No self-routing / self-sendA task cannot target itself
No duplicate targetsEach target can appear at most once within a router or send_to
Targets must existTask targets must reference existing tasks; mission targets must reference existing missions
Parallel iterators cannot have a routerSequential iterators can — the route is evaluated after the final iteration
At least one startable taskEvery 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:

  1. Route decisions are loaded to reconstruct which task activated which
  2. Incomplete dynamic targets are re-queued for execution
  3. The activating task’s commander is restored so the dynamic target can query it for context

See Also

Last updated on