Subagent Background Lifecycle
subagent({ run_in_background: true }) returns a handle immediately instead of blocking. The child keeps running in the
background - even across the parent turn boundary - and you retrieve its answer later with
subagent_send({ to, action: "wait" }). This skill is the lifecycle and the policy for when async is worth the extra
bookkeeping.
If you haven't decided whether to delegate at all, read subagent-delegation first.
When background beats synchronous
Sync (run_in_background: false, the default) blocks the current assistant turn until the child finishes. The child
can't outlast the turn. That's fine for:
- A single quick exploration (<30s of child work).
- Anything where you literally cannot proceed without the result.
Go background when any of these is true:
- Fan-out with work to do. You want to spawn two or more children and use the parent's turn to make progress on something else while they run.
- Latency-hiding. The child is going to take a while; meanwhile you could be drafting code, running tests, or reading other files.
- Cross-turn work. The investigation is long enough that it would blow your turn budget or trigger a stall - let it run across multiple parent turns.
- Exploratory "maybe I'll use this". You want a second opinion or an alternative plan in parallel; finish your own attempt first, then compare.
- Steerable tasks. You expect to nudge the child mid-run (new constraint, narrower scope) via
subagent_send({ to, text: "..." }).
Stay sync when:
- The child answer is the immediate next input to your reasoning.
- You have nothing else to do while it runs.
- The task is short enough that polling overhead costs more than it saves.
Fan-out pattern
Multiple subagent calls in a single assistant turn run concurrently. This is the cheapest way to parallelize:
Turn N (parent):
subagent({ agent: "explore", task: "find callers of A", run_in_background: true })
subagent({ agent: "explore", task: "find callers of B", run_in_background: true })
subagent({ agent: "explore", task: "find callers of C", run_in_background: true })
→ returns three handles immediately
Turn N (parent, same turn):
... do other work inline ...
Turn N+1 (parent):
subagent_send({ to: h1, action: "wait" })
subagent_send({ to: h2, action: "wait" })
subagent_send({ to: h3, action: "wait" })
→ merge results
If the tasks truly are independent and you have no inline work to do, you can also call them synchronously in the same
turn - the harness still runs them concurrently. Reach for run_in_background specifically when you want the parent
free to do something else, or when the children might outlast the turn.
The handle
Every subagent({ run_in_background: true }) call returns a short handle string (something like c1, c2, …).
Record it the moment you get it. Good places:
scratchpad- "spawnedc1to find callers of X; waiting."- The
notefield on atodoreview item - "parked pendingc1".
Do NOT try to reconstruct handles from memory across compactions. If the scratchpad doesn't have it, list active
children with the /agents running surface or treat the work as lost and respawn.
subagent_send actions
| Action | Blocking? | Purpose |
|---|---|---|
status (default) |
No | Cheap snapshot: running / finished / aborted, plus turn count. |
wait |
Yes | Block up to timeoutMs for the child to finish; return final answer. |
abort |
No | Cancel a running child. Final answer classified as aborted. |
(no action, text) |
No | Inject a user-role message into a running child to steer it. |
Rules the harness enforces:
textis not combinable withaction: "abort". Pick one.texton a finished child is rejected - steer only while running.waiton a finished child returns the stored answer instantly; safe to call repeatedly.- Only the parent can call
subagent_send. Subagents never see this tool.
Polling loop pattern
For a single long-running child:
spawn → handle h
... do inline work ...
subagent_send({ to: h, action: "status" }) # cheap check
→ still running, do more inline work
subagent_send({ to: h, action: "wait", timeoutMs: 30000 })
→ final answer
For fan-out:
spawn h1, h2, h3 in one turn
... do inline work ...
for h in [h1, h2, h3]:
subagent_send({ to: h, action: "wait" })
merge and act
Prefer wait with a generous timeoutMs over tight status polling - each status call burns a tool slot.
Steering a running child
Use subagent_send({ to, text }) to inject new guidance into a child that's still working. Good reasons:
- You discovered a constraint the child needs to honor ("ignore anything under external/").
- You realized the output shape you asked for was wrong ("return JSON, not prose").
- The child is about to waste turns on the wrong sub-problem ("stop reading search.ts; look at validate.ts instead").
The injected text appears to the child as a new user-role message. Write it the same way you'd write a clarifying reply in chat - short, imperative, paste any paths literally.
Do NOT use steering as a replacement for a better initial task. If you're steering on every spawn, tighten the prompt
(see subagent-delegation → Writing the task).
Abort criteria
Abort a background child when:
- The task is obsolete. The user changed direction, or the parent already solved it inline.
- The child is clearly stuck (repeated reads of the same file, no progress across multiple
statuschecks, turn count climbing with no output growth). - You spawned the wrong agent type and steering won't fix it (e.g., you need write access but spawned
explore).
subagent_send({ to, action: "abort" }) is the clean exit. Don't just stop calling wait and hope it dies - background
children survive turn boundaries and keep consuming until they abort, finish, or time out.
The "don't orphan children" rule
Every background handle you receive is a commitment. Before ending your assistant turn, for every live handle you spawned:
waiton it (if you need the answer), ORstatusit and note inscratchpadthat you'll check next turn, ORabortit (if obsolete).
Silently moving on and never touching a handle again is the worst outcome:
- The child keeps burning tokens until it hits its own turn cap.
- Its final answer ends up in a tempfile the parent never reads.
- Future turns get cluttered with "running" entries in the injected status block.
A quick audit at the end of each turn - "any live handles I haven't resolved?" - catches this.
Combining with todo and scratchpad
Background work and planning tools compose naturally:
- Move the corresponding todo to
reviewwith a note like "parked onc1" when you spawn. completethe todo only afterwaitreturns and you've verified the answer.- Keep handle → task mapping in
scratchpadso you can re-attach after compaction:c1 = explore callers of searchHandler (src/api/search.ts:412).
Anti-patterns
- Don't spawn background children you have nothing to do with. If you'll just
waitimmediately, spawn sync. Background is for latency-hiding, not syntax sugar. - Don't tight-poll with
status. Onestatusbetween substantial inline work is fine; astatusevery line of reasoning is pure waste. Usewaitwith a timeout instead. - Don't steer on every turn. If you're injecting text more than once or twice, the original
taskwas wrong - abort and respawn with a better prompt. - Don't forget handles exist across turns. Unlike sync children, background children outlive the turn that spawned them. Treat them like open file descriptors.
- Don't race children against the user. If the user is typing a follow-up, a background explorer can deliver its
answer mid-conversation and derail the thread. Abort or
waitbefore pivoting direction. - Don't spawn a child per file. If you're about to loop
for each file: subagent(...), write onetaskthat handles the batch, or fan out with a handful of children covering ranges - not dozens.
Quick reference
| Goal | Move |
|---|---|
| Spawn and keep working | subagent({ run_in_background: true, ... }), record handle |
| Cheap progress check | subagent_send({ to, action: "status" }) |
| Block for the final answer | subagent_send({ to, action: "wait", timeoutMs: 30000 }) |
| Push a new constraint into a running child | subagent_send({ to, text: "also ignore external/" }) |
| Cancel a running child | subagent_send({ to, action: "abort" }) |
| Fan out 3 independent explorations | 3× subagent({ run_in_background: true }) in one turn, wait next turn |
| Never again for this handle | abort or wait - don't orphan |