Skip to content

Subagents in the SDK

Define specialist subagents in code, control their tools and models, detect and resume them, and cap how far delegation can spread.

A subagent is a separate agent instance that your main agent hands a focused job to. It runs in its own conversation, does the work, and passes back a single final message. In an SDK application that is the cleanest way to keep the main context tidy, run independent jobs side by side and give narrow tasks a narrow set of tools.

This page covers defining subagents in code, what they can and cannot see, how to tell when one ran, how to resume one, and how to stop a single prompt turning into an expensive tree of agents.

Three ways to get a subagent

SourceHowWhen I use it
ProgrammaticThe agents option on query()Almost always in SDK apps: the definition ships with the code and can be built at runtime
FilesystemMarkdown files in .claude/agents/ (see subagents)When the same agents are shared with people using the CLI
Built-in general-purposeNothing to defineClaude can call it through the Agent tool whenever it likes

If a programmatic agent and a file-based agent share a name, the programmatic one wins.

When Claude calls the Agent tool without a subagent_type, it gets general-purpose. Setting CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1 removes that default, and such a call then fails with subagent_type is required.

What you gain

  • Isolated context. Everything the subagent reads and every tool result it sees stays in its own conversation. The parent gets the final report only, so a subagent can trawl through forty files without filling the main window.
  • Parallel work. Independent subagents run at the same time. A lint check, a dependency audit and a test run finish in the time of the slowest, not the sum.
  • Focused instructions. Each subagent has its own system prompt. Detailed migration rules can live in a db-migrator agent rather than cluttering the main prompt.
  • Narrow permissions. A subagent can be restricted to a handful of tools, so a reviewer physically cannot edit files.

Defining subagents in code

Pass a map of name to definition in agents. Claude delegates through the Agent tool, so include Agent in allowedTools if you want delegation to be pre-approved.

This example sets up a release-notes writer and a changelog checker for a small library:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const msg of query({
  prompt: "Prepare release notes for v2.4.0 and make sure CHANGELOG.md agrees with them",
  options: {
    allowedTools: ["Read", "Grep", "Glob", "Bash", "Agent"],
    agents: {
      "notes-writer": {
        description: "Writes user-facing release notes from git history. Use when release notes are needed.",
        prompt: "You turn commit logs into short, plain-English release notes grouped by Added, Changed and Fixed.",
        tools: ["Bash", "Read", "Grep"],
        model: "haiku"
      },
      "changelog-checker": {
        description: "Compares CHANGELOG.md with a set of release notes and lists mismatches. Read-only.",
        prompt: "You never edit files. Report every entry that appears in one source but not the other.",
        tools: ["Read", "Grep", "Glob"]
      }
    }
  }
})) {
  if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}

The Python version uses AgentDefinition:

from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition

opts = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob", "Bash", "Agent"],
    agents={
        "changelog-checker": AgentDefinition(
            description="Compares CHANGELOG.md with release notes and lists mismatches. Read-only.",
            prompt="You never edit files. Report every entry missing from either source.",
            tools=["Read", "Grep", "Glob"],
        ),
    },
)

Definition fields

FieldTypeRequiredPurpose
descriptionstringYesTells Claude when this agent is the right choice. This is what automatic delegation matches against.
promptstringYesThe subagent's system prompt.
toolsstring[]NoAllowlist of tools. Leave it out and the agent inherits every tool available to subagents.
disallowedToolsstring[]NoTools to strip. Also accepts mcp__server, mcp__server__* (every tool on one server) and mcp__* (every MCP tool).
modelstringNoAn alias (fable, opus, sonnet, haiku), inherit for the main model, or a full model ID. If omitted, Claude Code follows its normal subagent model order.
skillsstring[]NoSkills preloaded into the agent's context at start. Others can still be invoked through the Skill tool.
memoryuser, project or localNoWhich memory source the agent uses.
mcpServers(string or object)[]NoMCP servers for this agent, by name or as inline config.
initialPromptstringNoSent automatically as the first user turn when this agent is the main-thread agent. Ignored when it runs as a subagent.
maxTurnsnumberNoTurn cap. On hitting it, the output comes back marked partial (Claude Code v2.1.246+), and the agent can be resumed.
backgroundbooleanNoAlways run as a non-blocking background task, whatever Claude asks for.
omitClaudeMdbooleanNoSkip user, project and local CLAUDE.md files when running as a subagent. Managed policy files still load. TypeScript SDK v0.3.271+ only; Python's AgentDefinition lacks it.
effortlow, medium, high, xhigh, max or a numberNoReasoning effort.
permissionModePermissionModeNoPermission mode inside this agent, subject to the inheritance rules on the permissions page.

Note: In Python, multi-word fields keep their camelCase names (disallowedTools, mcpServers) to match the wire format.

Background by default

An Agent tool call that does not set run_in_background launches a background subagent. Claude passes run_in_background: false when it needs the answer before it can carry on. Setting background: true on a definition forces background running regardless. Before Claude Code v2.1.198 this default was still rolling out, so older versions might run such calls synchronously.

Building definitions at runtime

Because definitions are plain objects, you can generate them per request. I use this to pick a stronger model only when the stakes justify it:

def reviewer_for(risk: str) -> AgentDefinition:
    high = risk == "high"
    return AgentDefinition(
        description="Reviews a pull request for security problems",
        prompt=("Treat every unchecked input as a finding." if high
                else "Flag clear vulnerabilities only."),
        tools=["Read", "Grep", "Glob"],
        model="opus" if high else "sonnet",
    )

What a subagent can see

A normal subagent (anything other than a fork) starts with a fresh context. The only thing passed from parent to child is the prompt string in the Agent tool call, so that prompt needs to carry any file paths, error text or decisions the child needs.

IncludedNot included
Its own prompt plus the Agent tool's promptThe parent's conversation and tool results
Project CLAUDE.md (when loaded through settingSources), unless omitClaudeMd is setSkill content, unless listed in skills
Tool definitions: inherited, or the subset in tools, filtered for background runsThe parent's system prompt

It also inherits the session's extended thinking settings. A subagent that has the SendMessage tool is given a list of the other named agents in the session on its first turn so it knows who it can message; forks do not get this because they already inherit the parent conversation.

What comes back

The parent receives the subagent's final message, though it may paraphrase it in its own reply. If you need the subagent's output verbatim, say so in the main prompt or system prompt.

From v2.1.210, Claude Code scans that final message for instruction-shaped text before the parent reads it:

  • Imitations of harness-only tags such as <system-reminder> are neutralised by inserting a backslash after the <.
  • Mentions of permission configuration (.claude/settings.json, bypassPermissions, --dangerously-skip-permissions) are left as written.
  • Lines starting Human: or Assistant: get a backslash before the colon.

For tag or permission matches a [harness: ...] line naming the patterns is prepended. Nothing is ever deleted or reworded.

An API error that kills a subagent early, such as a rate limit, is never presented as its result.

Getting Claude to delegate

Claude decides on its own when to delegate, matching the task against each description. A vague description means little delegation. "Use for any SQL performance question, including slow queries and index design" works much better than "Database helper".

To force a particular agent, name it in the prompt: Have the changelog-checker agent compare the two files.

Detecting subagent runs

Look for tool_use blocks whose name is Agent. Messages produced inside a subagent carry a parent_tool_use_id.

Note: In the system:init tool list the tool is still called Task, and tool_use blocks used Task before Claude Code v2.1.63. Match both names if you support older versions.

In TypeScript, assistant content lives at message.message.content; in Python it is message.content.

for await (const msg of run) {
  if (msg.type === "assistant") {
    for (const block of msg.message.content) {
      if (block.type === "tool_use" && (block.name === "Agent" || block.name === "Task")) {
        console.log("Delegated to", (block.input as any).subagent_type);
      }
    }
  }
  if ("parent_tool_use_id" in msg && msg.parent_tool_use_id) {
    console.log("  ...message from inside a subagent");
  }
}

Resuming a subagent

A resumed subagent keeps its whole history: tool calls, results and reasoning. That is useful for follow-up questions that build on earlier exploration.

When a subagent finishes, the Agent tool result contains a line like agentId: a1b2c3. The built-in Explore and Plan agents are one-shot and return no ID, so use a custom agent or general-purpose if you plan to resume.

  1. Capture session_id from the messages of the first query.
  2. Pull the agent ID out of the Agent tool result text with a regex such as agentId:\s*([\w-]+).
  3. Start a second query() with resume set to that session ID, the same agents map, and a prompt that names the agent ID, for example Resume agent a1b2c3 and list the three riskiest endpoints.

Each query() starts a new session unless you resume, and the subagent's transcript is only reachable from the session that created it. Subagent transcripts live in their own files; see subagents for compaction and the cleanupPeriodDays retention setting.

Restricting tools

  • Omit tools and the subagent gets every tool available to subagents.
  • List tools and it gets exactly those. A tool you leave out simply does not exist in its session: no prompt, no error.
JobTools
Read-only reviewRead, Grep, Glob
Running testsBash, Read, Grep
Editing without a shellRead, Edit, Write, Grep, Glob
Everythingomit tools

Capping depth, concurrency and spend

Subagents can spawn their own subagents, and each one makes its own API calls that count towards total_cost_usd. Three limits keep that under control. They apply from TypeScript SDK v0.3.219 and Python SDK v0.2.127 (bundling Claude Code v2.1.219 or later).

LimitSet withDefaultAt the limit
Nesting depthCLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH3 layers below the main agent; 1 means subagents cannot spawnBottom-layer agents cannot delegate and do the work themselves
Concurrent subagentsCLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS20Spawning is refused with Concurrent subagent limit reached until one finishes. Sessions with ultracode active are never refused.
SpendmaxBudgetUsd (TS) or max_budget_usd (Python)NoneFurther spawns refused with Budget limit reached, running background subagents stopped, and the query ends with error_max_budget_usd

Depth and concurrency are environment variables passed through env. Remember that TypeScript replaces the environment (spread process.env) while Python merges.

opts = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob", "Agent"],
    env={
        "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1",
        "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "4",
    },
    max_budget_usd=3.0,
)

Opus 5 delegates more

Claude Opus 5 reaches for subagents more readily than earlier models. With the claude_code preset system prompt and Opus 5, Claude Code adds a line telling Claude not to use the Agent tool unless asked (the tool stays available). With a custom system prompt, that line is absent, so add your own delegation guidance. Either way, set the hard limits too: instructions steer, limits enforce.

Many agents: workflows

For a handful of delegations per turn, subagents are ideal. For jobs that fan out to dozens or hundreds of agents, the Workflow tool moves orchestration into a script that runs outside the conversation. It is available from TypeScript SDK v0.3.149; add Workflow to allowedTools to pre-approve runs. See workflows.

Troubleshooting

Claude does the work itself. Name the agent in the prompt and sharpen its description.

A file-based agent does not appear. Claude Code watches ~/.claude/agents/ and .claude/agents/ and picks up changes within seconds, but:

  • A directory created after the session started is not watched. Restart. This is the usual cause.
  • Bad YAML frontmatter or a duplicate name stops it loading.
  • Sessions started with --disable-slash-commands do not watch these folders.
  • Agent folders under extra directories (additionalDirectories in TypeScript, add_dirs in Python, --add-dir on the CLI) load but are not watched.
  • A programmatic agent of the same name overrides the file.