Subagents
Delegate to specialized agents with isolated context: defining subagents, parallel fan-out/verify/synthesize (modeled in Python), when not to delegate, and a shared subagent library.
Learning objectives
- Explain subagents and the context-isolation problem they solve.
- Define a subagent with its own prompt, tools, and model.
- Delegate and run subagents in parallel.
- Design and govern multi-subagent workflows.
code/ak5-subagents/. Python runs offline; configs are ready to use.1 · Why subagents essential
A single agent doing everything fills its context with detail irrelevant to the next step. A subagent is a specialized agent the main session delegates to: its own prompt, tools, model, and — crucially — its own context. It does a job and returns just the conclusion, keeping the main thread clean.
This picture shows what a subagent is: instead of doing everything itself, the main agent hands off one well-defined job to a fresh helper that has its own memory (its own context), and gets back only the answer. Read the boxes left to right — they are the life of one delegated task.
- Main agent (orchestrates) — the session you're talking to. It decides a piece of work is worth handing off rather than doing inline.
- Delegate task (scoped job) — the main agent describes one narrow job, e.g. "review this file". Scoped means small and specific, not "do the whole project".
- Subagent (own context) — a fresh agent spun up just for this job, with its own tools and model and a blank memory. It can read a hundred files here without cluttering the main agent's memory.
- Returns conclusion (not the transcript) — the subagent sends back only its final answer, not everything it read and thought along the way. That's the whole trick.
- Main continues (context stays clean) — the main agent carries on with its memory uncluttered, holding just the conclusion it needed.
In short: A subagent is like asking a colleague to research something and give you a one-paragraph summary — you get the answer without having to read everything they read.
2 · Context isolation, quantified essential
The subagent reads a hundred files and returns three lines; the main agent never sees those files — only the answer. That isolation is what lets long, complex work fit in a context window.
isolation.pydef context_used(main_tokens, subtasks, tokens_per_subtask, inline):
"""inline=True: everything in the main context. inline=False: subagents isolate."""
if inline:
return main_tokens + subtasks * tokens_per_subtask # all piled in
return main_tokens + subtasks * 100 # only the conclusions return
print("6 file-heavy subtasks, inline: ", context_used(2000, 6, 8000, True), "tokens")
print("6 file-heavy subtasks, delegated:", context_used(2000, 6, 8000, False), "tokens")
print("delegation keeps the main thread ~10x leaner")
6 file-heavy subtasks, inline: 50000 tokens
6 file-heavy subtasks, delegated: 2600 tokens
delegation keeps the main thread ~10x leaner
This tiny program puts a number on why delegation is worth it. It compares two ways of doing several file-heavy subtasks: cramming everything into one agent's memory (inline) versus sending each subtask to a subagent that returns only its conclusion (delegated).
context_used(main_tokens, subtasks, tokens_per_subtask, inline)is a function (a reusable recipe) that estimates how many tokens — the chunks of text a model reads and is billed for — end up in the main agent's memory.- When
inlineisTrue, it returnsmain_tokens + subtasks * tokens_per_subtask: every subtask's full material (8000 tokens each) piles into the one context. - When
inlineisFalse(delegated), it returnsmain_tokens + subtasks * 100: each subagent reads its own big pile privately and sends back only a small ~100-token conclusion. - The two
printlines run the same 6 subtasks both ways so you can see the difference side by side.
What the output means: Inline costs 50000 tokens (2000 + 6×8000); delegated costs only 2600 (2000 + 6×100). Same work, roughly 10× less clutter in the main thread — which is exactly what lets long, complex work fit in the context window.
Try this: Change tokens_per_subtask from 8000 to 20000 (heavier files) and re-run. The inline number explodes while the delegated number barely moves — that gap is the case for subagents.
3 · Define a subagent intermediate
A subagent is a markdown file with front-matter: a name, a description of when to use it, its allowed tools, and optionally a model. The body is its system prompt.
code-reviewer.md---
name: code-reviewer
description: >
Reviews a diff for bugs and security issues. Use proactively after code
changes, or when the user asks for a review.
tools: [Read, Grep, Bash]
model: sonnet
---
You are a focused code reviewer. Given a diff:
- Find correctness bugs, edge cases, security issues (injection, secrets, authz).
- Cite file:line. Group by severity. Be terse — no praise, no restating code.
- Return ONLY the findings; the main agent will act on them.
This is not Python — it's a subagent definition file (Markdown). It's how you actually create a subagent: a small header (called front-matter) describes the agent, and the text below it is the agent's instructions (its system prompt). Save this as a .md file and the tool can spin it up as a helper.
- The block fenced by
---lines at the top is the front-matter — settings inkey: valueform. Everything after the second---is the prompt. nameis the agent's id;descriptiontells the main agent when to reach for it ("use proactively after code changes") — this is what makes delegation automatic.tools: [Read, Grep, Bash]is the allow-list of what this agent may do. A reviewer only needs to read and search code, so it gets noEditorgit push— that's least privilege: give each agent only the tools its job needs.model: sonnetpicks which model runs it. The instructions below tell it to find bugs, citefile:line, and return only the findings — a scoped job with a clean output.
Try this: Imagine adding Edit to the tools list. Now a review could silently change your code — the opposite of what you want. Keeping the list narrow is a safety feature, not a limitation.
Read/Grep, not Edit or git push. Narrow tools = safer, cheaper, more focused (the K1 permission idea, per-agent).4 · Advanced — delegate and parallelize advanced
Because each subagent has its own context, you can run several in parallel — the biggest speedup for wide tasks ("review these 6 modules"). Model the wall-clock win.
parallel.pydef wall_clock(tasks_secs, parallel, workers=4):
if not parallel:
return sum(tasks_secs) # one after another
# parallel: time = ceil(n/workers) * slowest-in-a-wave (approx with equal tasks)
import math
waves = math.ceil(len(tasks_secs) / workers)
return waves * max(tasks_secs)
tasks = [30, 30, 30, 30, 30, 30] # 6 module reviews, 30s each
print("serial: ", wall_clock(tasks, parallel=False), "s")
print("parallel:", wall_clock(tasks, parallel=True, workers=4), "s")
serial: 180 s
parallel: 60 s
Because each subagent works in its own context, you can run many at once instead of waiting for each to finish. This program estimates the real-world time saved (wall-clock time — the time you actually wait) for 6 reviews done serially versus in parallel.
wall_clock(tasks_secs, parallel, workers=4)takes a list of how long each task takes, whether to run in parallel, and how many can run at the same time (workers).- If
parallelis false, it returnssum(tasks_secs)— every task runs one after another, so the times simply add up. - If parallel, it splits the tasks into waves of 4 at a time:
math.ceil(len(tasks) / workers)rounds up the number of waves (6 tasks ÷ 4 workers = 2 waves). Each wave takes as long as its slowest task, so time ≈waves × max(tasks_secs). - The test data is 6 module reviews at 30 seconds each; the two prints show both strategies.
What the output means: Serial takes 180 s (6×30). Parallel takes 60 s — 2 waves × 30 s each. Same work, 3× faster wall-clock, purely from running independent subagents side by side.
Try this: Raise workers to 6 so all six fit in one wave, and the parallel time drops to 30 s. More workers help only until every task fits in a single wave.
5 · Advanced — fan-out / verify / synthesize advanced
A powerful pattern: split work, run a subagent per unit in parallel, have a verifier subagent adversarially check each finding, then synthesize survivors. Model the flow.
The fan-out workflow
- Main splits the work into independent units (per module/file/option).
- A subagent per unit runs in parallel, each returning a structured finding.
- A verifier subagent adversarially checks each finding ("try to refute this").
- Main synthesizes the surviving findings into one answer.
fanout.pydef review_module(name): # a 'subagent' returning findings
fake = {"auth": ["hardcoded secret"], "billing": ["float money bug"], "search": []}
return [{"module": name, "issue": i} for i in fake.get(name, [])]
def verify(finding): # a verifier subagent: is it real?
return "secret" in finding["issue"] or "money" in finding["issue"]
modules = ["auth", "billing", "search"]
raw = [f for m in modules for f in review_module(m)] # fan-out
confirmed = [f for f in raw if verify(f)] # verify
print("raw findings:", len(raw), "| confirmed:", len(confirmed))
for f in confirmed: print(" -", f["module"], ":", f["issue"])
raw findings: 2 | confirmed: 2
- auth : hardcoded secret
- billing : float money bug
This models a powerful team pattern: fan-out (send one subagent per unit of work), then verify (a second subagent double-checks each finding before you trust it). Here the 'subagents' are plain functions so the pattern is easy to see without any real API calls.
review_module(name)plays the role of a review subagent: given a module name it returns a list of findings. Thefakedictionary just supplies canned results so the demo runs offline (auth has a secret, billing a money bug, search is clean).verify(finding)is the verifier subagent: it returnsTrueonly if the finding looks genuinely serious (mentions a "secret" or "money"). This is the "try to refute it" step that filters out weak claims.raw = [f for m in modules for f in review_module(m)]is the fan-out: it runs the reviewer over every module and collects all findings into one list.confirmed = [f for f in raw if verify(f)]is the verify step: it keeps only the findings that survive the check. The loop then prints each survivor.
What the output means: raw findings: 2 | confirmed: 2 — two issues were found and both passed verification (the empty search module contributes nothing). You then see the auth secret and the billing money bug listed as the trusted, synthesized result.
Try this: Add "typo" to one module's fake findings and re-run. It shows up in raw but not in confirmed, because verify rejects it — that's the adversarial check earning its keep.
6 · Professional — when NOT to delegate professional
Subagents add latency and cost (each spins up its own context). Delegate when work is genuinely independent or context-heavy; for a quick edit, the main agent is faster. Over-delegation is a real anti-pattern.
delegate_decision.pydef should_delegate(independent, context_heavy, quick_edit):
if quick_edit: return False, "trivial — main agent is faster"
if independent and context_heavy: return True, "isolate + parallelize"
if context_heavy: return True, "isolate to keep main context clean"
return False, "not worth the spin-up overhead"
for case in [(True,True,False),(False,True,False),(False,False,True),(False,False,False)]:
print(case, "->", should_delegate(*case))
(True, True, False) -> (True, 'isolate + parallelize')
(False, True, False) -> (True, 'isolate to keep main context clean')
(False, False, True) -> (False, 'trivial — main agent is faster')
(False, False, False) -> (False, 'not worth the spin-up overhead')
Subagents aren't free — each one spins up its own context, which costs time and money. This function encodes the rule of thumb for when to delegate and when not to, so you don't over-use subagents (a real anti-pattern).
should_delegate(independent, context_heavy, quick_edit)takes three yes/no facts about the work and returns a(decision, reason)pair.- The checks run in order, and the first match wins.
if quick_edit: return False— a trivial change is faster done inline, so bail out immediately. if independent and context_heavy→ delegate to both isolate and parallelize (the ideal case).if context_heavyalone → still delegate, just to keep the main context clean.- If none of those hold, the final
return Falsesays it's not worth the spin-up overhead. The loop feeds four example cases through and prints each verdict.
What the output means: Each line pairs the inputs with the decision, e.g. (True, True, False) -> (True, 'isolate + parallelize'). The two True results are context-heavy work; the two False results are a quick edit and a trivial task — exactly when the main agent should just do it.
Try this: Trace should_delegate(True, False, False) by hand (independent but light, not a quick edit): it falls past every if to the last line and returns False — being independent alone isn't enough to justify a subagent.
7 · Tech-lead — a subagent library + composition tech-lead
A lead builds a library of subagents (reviewer, researcher, test-writer) the team shares, and composes them with Skills (K4): a subagent uses a Skill to do its job your way. This is the backbone of the capstone workbench.
Exercise AK5.1 — Build a review crew
Context: A parallel review crew is the fan-out pattern made concrete: least-privilege reviewers running independently, then a consolidation step — and a wall-clock comparison that shows why parallel beats serial.
Your task: Write a code-reviewer subagent with least-privilege tools, run three of them in parallel over three files, consolidate the results, and justify and measure the choice.
Requirements:
- The
code-reviewersubagent has least-privilege (read-only) tools - The main agent runs three reviewers in parallel, one per file
- The results are consolidated into a single review
- Justify the delegation with
should_delegate - Compare wall-clock parallel vs serial using the speedup model from the ladder
💡 Hint: Because the three reviews are independent and read-only, they're the textbook fan-out case — let should_delegate confirm it before you parallelize.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: A subagent runs a task in its own context and returns only a conclusion, so the exploration cost — files read, dead ends — stays isolated and never bloats the main thread.
Your task: Explain the benefit of a subagent with a concrete before/after of what lands in the main context.
Requirements:
- Show a "without subagent" case where search noise (many file reads, dead ends) floods the main context
- Show a "with subagent" case where the same search runs in a sub-context
- The parent receives only the distilled one-line conclusion
- Make clear the exploration cost is discarded with the sub-context
- Explain why fan-out searches are a natural fit
💡 Hint: Contrast what the main thread absorbs in each case — the win is what it doesn't see.
Show solution
Isolation keeps the main thread clean — only the conclusion returns:
Without subagent: main context absorbs 40 files of grep output,
3 dead-end reads, and the eventual answer.
-> context bloated with search noise.
With subagent: "Find where auth tokens are validated" runs in a
sub-context; it reads 40 files there and returns
"validated in src/auth/verify.ts:reviewToken()".
-> main context gets the one-line conclusion only.
The exploration cost (files read, false starts) stays in the subagent's context and is discarded; the parent keeps only the distilled result. That's why fan-out searches are a natural fit — the noise never touches the main thread.
Context: Subagents are defined as files with frontmatter — a restricted tool set bounds what one can do, and a cheaper model fits search-heavy work — so a read-only explorer can't accidentally change anything.
Your task: Sketch a read-only "explorer" subagent defined as a file with frontmatter. Config — verify keys in the docs.
Requirements:
- Defined as a Markdown file with frontmatter (e.g. under
.claude/agents/) - Frontmatter has a
nameand adescriptionsaying when to use it - The
toolslist is read-only (Read/Grep/Glob; no Edit/Write/Bash) - A cheaper model is chosen to fit search-heavy work
- The body is the subagent's system prompt / instructions
- Verify the exact frontmatter keys and the agents directory in the docs
💡 Hint: The frontmatter is the scope — the tool list is what actually guarantees it can't modify a file.
Show solution
A subagent is a Markdown file with frontmatter + a system prompt:
# .claude/agents/explorer.md
---
name: explorer
description: Read-only code explorer. Use to locate where something is
implemented across the codebase without editing anything.
tools: Read, Grep, Glob # no Edit/Write/Bash -> can't change anything
model: claude-haiku-4-5 # cheap model for search-heavy work
---
You locate code. Search broadly, read excerpts, and return the file paths
and symbols that answer the question. Do not modify any file.
The frontmatter scopes the subagent: a restricted tool set (read-only) bounds what it can do, and a cheaper model fits search-heavy work. The body is its instructions. Verify the exact frontmatter keys and the agents directory in the Claude Code docs.
Context: Fanning out independent subagents turns a sum into a max: work that ran serially finishes in about the time of the slowest branch when parallelized — but only genuinely independent work qualifies.
Your task: Model the speedup of running four independent searches in parallel subagents versus serially.
Requirements:
- Represent each task by a duration
- Serial total is the sum of the durations
- Parallel total is the max of the durations (wait for the slowest)
- Compute and report the speedup
- State the caveat: only genuinely independent work can be parallelized; dependent steps still run in order
💡 Hint: Serial is sum, parallel is max — the ratio is your speedup.
Show solution
Independent work fans out; total time approaches the slowest branch, not the sum:
tasks = [3, 5, 2, 4] # seconds each, independent searches
serial = sum(tasks) # 14s — one after another
parallel = max(tasks) # 5s — all at once, wait for slowest
print(f"serial: {serial}s parallel: {parallel}s speedup: {serial/parallel:.1f}x")
# serial: 14s parallel: 5s speedup: 2.8x
Fanning out independent subagents turns a sum into a max: four searches that took 14s serially finish in ~5s in parallel, and each one's noise stays isolated. Only parallelize genuinely independent work — dependent steps must still run in order.
Context: The strong multi-agent pattern is fan out to gather, run a fresh verifier, then synthesize — the verifier must be a separate context, because a gatherer checking its own work inherits its own blind spots.
Your task: Model the fan-out / verify / synthesize pipeline and explain why the verifier must be independent.
Requirements:
- Fan-out stage: parallel gatherer subagents each return a finding
- Verify stage: a fresh subagent that did not gather checks the findings against the source
- Synthesize stage: the parent combines verified findings into one answer
- Explain why the verifier must be a separate context (avoid inherited blind spots)
- Each stage's exploration cost stays isolated in its own context
💡 Hint: The load-bearing rule is separation: whoever verifies must not be whoever gathered.
Show solution
Three roles, each in its own context, so verification is unbiased:
def pipeline(question):
# 1. FAN-OUT: parallel gatherers, each returns a finding
findings = [subagent("explorer", q) for q in split(question)]
# 2. VERIFY: a FRESH subagent that did not gather, checks the findings
verdict = subagent("verifier",
f"Independently confirm these against the code: {findings}")
# 3. SYNTHESIZE: the parent combines verified findings into one answer
return synthesize(findings, verdict)
The verifier must be a separate context that did not produce the findings — a gatherer checking its own work inherits its own blind spots. Fan-out gets breadth cheaply, an independent verifier catches the errors, and the parent synthesizes; each stage's exploration cost stays isolated.
Context: A subagent starts with a blank context and costs a round trip, so delegation pays off for independent, exploration-heavy work but hurts for small, sequential, context-bound tasks.
Your task: Write the rule for when delegating to a subagent hurts more than it helps.
Requirements:
- The rule takes: independent, exploration-heavy, and needs-shared-context
- Needs the parent's in-progress state → do it inline (the subagent can't see it)
- Independent and exploration-heavy → delegate (isolate noise, maybe parallelize)
- A single read or quick sequential edit → cheaper inline
- Demonstrate the outcomes with example cases
💡 Hint: The disqualifier is shared context — if the task needs the parent's working state, no delegation, regardless of size.
Show solution
Delegate for breadth/isolation; stay direct for small, sequential, context-bound work:
def should_delegate(independent, exploration_heavy, needs_shared_context):
if needs_shared_context:
return "NO — subagent can't see the parent's working state; do it inline"
if independent and exploration_heavy:
return "YES — isolate the noise and/or run in parallel"
return "NO — a single file read or sequential edit is cheaper inline"
print(should_delegate(True, True, False)) # YES (fan-out search)
print(should_delegate(False, False, True)) # NO (needs shared context)
print(should_delegate(False, False, False)) # NO (trivial inline task)
A subagent starts with a blank context, so anything depending on the parent's in-progress state must stay inline. Delegation pays off when the task is independent and exploration-heavy (isolate the noise, maybe parallelize); a single read or a quick sequential edit is cheaper done directly.
Context: A healthy subagent library is a few sharply-scoped agents (least-privilege tools, right-sized model) plus an orchestrator that routes by task kind — scopes must not overlap and read-only agents can be marked parallel-safe.
Your task: As tech lead, design a small library of composable subagents and the rule the orchestrator uses to pick among them.
Requirements:
- A registry of a few narrow subagents, each with its tool set, model, and purpose
- Each is least-privilege (read-only where possible) and right-sized on model
- A selector routes by task kind (e.g. locate → explorer, confirm → verifier, review → reviewer)
- They compose via the fan-out / verify / synthesize pattern
- Library health depends on non-overlapping scopes and marking read-only agents parallel-safe
- Verify frontmatter/registry details in the docs
💡 Hint: Keep each agent narrow and let the orchestrator match task kind to agent — overlap between scopes is what breaks routing.
Show solution
A few sharply-scoped subagents plus an orchestrator that routes by task kind:
REGISTRY = {
"explorer": {"tools": ["Read","Grep","Glob"], "model": "claude-haiku-4-5",
"use": "locate code / gather facts, read-only"},
"verifier": {"tools": ["Read","Grep"], "model": "claude-opus-4-8",
"use": "independently confirm findings"},
"reviewer": {"tools": ["Read"], "model": "claude-opus-4-8",
"use": "review a diff for bugs"},
}
def pick(task_kind):
return {"locate": "explorer", "confirm": "verifier",
"review": "reviewer"}.get(task_kind, "explorer")
print(pick("locate")) # explorer (cheap, read-only, parallel-safe)
print(pick("confirm")) # verifier (independent, higher model)
Keep each subagent narrow (least-privilege tools, right-sized model) and compose them via the fan-out/verify/synthesize pattern. The orchestrator routes by task kind; the library stays healthy when scopes don't overlap and read-only agents are marked parallel-safe. Verify frontmatter/registry details in the docs.
✓ Checkpoint — you can move on when you can…
- Explain context isolation and quantify its benefit.
- Define a least-privilege subagent.
- Delegate, parallelize, and run fan-out/verify/synthesize.
- Decide when NOT to delegate; curate a subagent library.
Knowledge check check yourself
Why does delegating a context-heavy subtask to a subagent keep the main session's context lean?
Show answer
In the fan-out/verify/synthesize pattern, what does the verifier subagent do, and why add that extra step?