Subagent fan-out design and parent process result aggregation in the Claude Agent SDK
Running multiple agents in parallel to increase throughput is a familiar idea. In practice, however, it can be hard to see the full picture: where and how to define subagents, how to collect results back into the parent process, and exactly where and how to prevent file conflicts.
This post breaks down how the Claude Agent SDK and Claude Code CLI each handle subagents, and walks through designing parallel execution with a fan-out/fan-in pattern. Because the commercial API signatures are still evolving, all Python code below is pseudocode meant to illustrate the conceptual flow. For actual function names, class names, and signatures, consult the Claude Agent SDK official documentation and the Claude Code Subagents documentation (as of September 2026).
Concepts: Orchestrator and Subagents
The orchestrator (parent process) receives a high-level request, breaks it into subtasks, delegates them to subagents, and synthesizes the collected results. Each subagent focuses on a single subtask handed down by the orchestrator and has three key properties:
- Independent context window: The parent's long conversation history and noise from sibling agents do not bleed in.
- Individual system prompt: Each agent can receive different role instructions.
- Individual tool permissions: Only the necessary tools are exposed, keeping the agent's scope narrow.
Because of this isolation, it is effective to push large exploratory tasks that would easily pollute the parent context — log scanning, large-scale code analysis, etc. — into subagents. The flip side is that result summaries continuously flow back to the parent, so at large fan-out scales the parent context can itself be exhausted — a pitfall covered later.
Two Execution Environments
There are two main paths for working with subagents. The concepts are similar, but the execution models and configuration approaches differ. Conflating them leads to mismatched code examples.
| Claude Code CLI | Claude Agent SDK (Python/TS) | |
|---|---|---|
| Execution environment | Local CLI process | User application process |
| Subagent definition | .claude/agents/*.md (YAML front matter) |
SDK option/config objects |
| Delegation mechanism | Built-in Task Tool calls | Delegation API provided by the SDK |
| Parallel execution | Orchestrator instructs Task to issue multiple jobs | Application code explicitly schedules concurrent calls |
In the CLI, the orchestrator prompt must instruct "delegate these tasks in parallel" for the Task Tool to issue them concurrently. In SDK code, the application directly schedules concurrent calls in the event loop. Both approaches share the same idea of per-subagent isolated contexts.
Defining Subagents in Claude Code CLI
In the CLI environment, subagents are defined as markdown files inside a .claude/agents/ directory in the project. The filename becomes the agent identifier, and YAML front matter specifies metadata.
project/
├── .claude/
│ └── agents/
│ ├── code-reviewer.md
│ ├── security-checker.md
│ └── doc-writer.md
└── src/Each file follows roughly this structure. Available fields and tool names vary by CLI version, so check the official documentation.
---
name: code-reviewer
description: A reviewer focused on code readability and pattern violations
tools:
- Read
- Grep
---
You are an experienced code reviewer.
Read the given file and point out readability, maintainability, and potential bugs.
Always return results strictly in the following JSON schema.
{ 'file': ..., 'issues': [{ 'severity': ..., 'message': ... }] }When the orchestrator is instructed to "review each module in parallel," the Task Tool uses the above definition to issue multiple instances of the same agent type concurrently.
Fan-Out Execution with the Python SDK
The skeleton for scheduling parallel subagent execution from application code looks like this. The code below is not the actual API signature — it is a conceptual example to illustrate the flow. Actual function names vary by SDK version.
# Conceptual example — refer to official docs for actual API signatures
import asyncio
from typing import Any
async def run_parallel_reviews(modules: list[str]) -> dict[str, Any]:
async def review_one(module: str) -> dict[str, Any]:
# Conceptual subagent execution function provided by the SDK
return await run_subagent(
agent="code-reviewer",
prompt=(
f"Review the {module} module and "
"return only JSON containing an issues array."
),
)
results = await asyncio.gather(*(review_one(m) for m in modules))
return synthesize(results)
def synthesize(results: list[dict[str, Any]]) -> dict[str, Any]:
issues: list[dict[str, Any]] = []
for r in results:
issues.extend(r.get("issues", []))
return {"issue_count": len(issues), "issues": issues}Three key points: First, each review_one call spawns a subagent with an independent context. Second, the result schema is enforced as JSON so the parent can parse it. Third, the synthesis logic is handled in code, saving one LLM call.
Whether using the CLI or SDK, subagents do not respond directly to the parent — instead, the delegation layer (Task Tool or SDK delegation API) collects results and delivers them to the parent. This matches the actual behavior.
Mixing Sequential and Parallel Stages in a Pipeline
Real-world pipelines more commonly use a mixed structure — "gather shared context sequentially → run independent analyses in parallel → synthesize results sequentially" — rather than pure parallelism.
# Conceptual example — parse return types explicitly in code
import json
async def mixed_pipeline(codebase_path: str) -> dict:
# Stage 1: Sequential — retrieve module list as JSON
raw = await run_agent(
agent="orchestrator",
prompt=(
f"Return only JSON in the form "
f'{{"modules": [...]}} listing the key modules in {codebase_path}.'
),
)
modules: list[str] = json.loads(raw)["modules"]
# Stage 2: Parallel — independent module analysis
analysis = await asyncio.gather(*(
run_subagent(agent="code-reviewer", prompt=f"Analyze the {m} module")
for m in modules
))
# Stage 3: Sequential — consolidated report
return await run_agent(
agent="orchestrator",
prompt=f"Consolidate the following analysis results into a unified report: {analysis}",
)Because the return value may be a string, it is safer to explicitly parse it with json.loads before indexing. Pairing this with a prompt constraint of "return only JSON" on the subagent side works well in practice.
Preventing File Conflicts: Git Worktree Isolation
When multiple subagents modify the same file simultaneously, race conditions occur. The canonical mitigation is to give each agent its own Git worktree. Conceptually, worktrees are created before execution and passed to each subagent as its working directory.
# Conceptual example — check actual option names in SDK/CLI docs
import subprocess, pathlib
def create_worktree(base_repo: str, branch: str, path: str) -> pathlib.Path:
subprocess.run(
["git", "-C", base_repo, "worktree", "add", path, branch],
check=True,
)
return pathlib.Path(path)
async def fix_in_isolation(module: str) -> dict:
wt = create_worktree("/repo", f"fix/{module}", f"/tmp/wt-{module}")
return await run_subagent(
agent="code-fixer",
prompt=f"Fix the issues in the {module} module within this worktree.",
cwd=str(wt),
)Claude Code CLI has separate isolation options per subagent, so CLI users should first check the documentation to see if the option is supported. In environments where it is not, creating worktrees at the application layer and passing them in — as shown above — is a viable alternative.
Common Pitfalls
Parallelizing dependent tasks: If A's output is B's input, the relationship is sequential, not parallel. Before designing a fan-out, draw the dependency graph and place only tasks that can be completed without knowing each other's results at the same level.
Vague subagent prompts: Subagents have no access to the parent's conversation history. Any background established in the parent prompt must be restated self-sufficiently in the subagent prompt. Phrases like "following the rules decided in the previous step" are a blank to a subagent.
Context backflow: You spin up dozens of subagents for isolation, but all of their result summaries flow back to the parent and accumulate, causing the parent context to overflow instead. Mitigate this by receiving results as structured JSON and having the parent extract only the needed fields before feeding them back into higher-level prompts.
No channel for user queries: A subagent cannot pause mid-execution to ask a human for confirmation. For example, delegating a task that requires approval for destructive actions — file deletion, remote push, schema changes — to a subagent will cause the task to fail or be auto-rejected at the point where approval is needed. Such tasks should be performed at the orchestrator level, or structured so that the subagent only handles diagnosis and draft generation while the parent performs the actual execution.
No real-time communication between sibling agents: There is no channel for subagent B to immediately reference subagent A's intermediate results during execution. If this is needed, collect A's final result, include it in a prompt, and start B fresh in a sequential structure.
Conservative default behavior: Orchestrators tend to be conservative about issuing parallel jobs unless explicitly instructed. To induce fan-out, it is most reliable to state it clearly in the prompt, such as "delegate the following N tasks in parallel."
Monitoring and Debugging
Once multi-agent execution is running, you need a way to trace after the fact which agent made which decision and why. The Anthropic Console's execution history lets you review the call sequence and tool usage for each subagent, so use it alongside your own logs. At minimum, logging the following locally makes reproduction much easier:
- The final prompt text passed to each subagent
- The raw response returned and whether JSON parsing succeeded
- Start/end timestamps and token counts
When Parallelism Is Actually a Disadvantage
Parallelization is not always beneficial. The following decision flow helps catch bad fan-outs before they happen.
If there are dependencies, if resource contention cannot be isolated, or if the returned results would push out the parent context, parallel fan-out is a net loss. Conversely, when all three conditions pass, the benefits of fan-out become clear.
Closing Thoughts
Both the Claude Agent SDK and Claude Code CLI are still actively evolving, so rather than tightly coupling your code to specific function names or options, it is more maintainable to first document the decision flow and result-retrieval contracts (JSON schema, isolation policy, logging contract). If you plan to put public-beta features into production, start with a small fan-out scale, observe context accumulation and failure patterns, and expand gradually.