Subagents

View Source

Official Documentation: This guide is based on the official Claude Agent SDK documentation. Examples are adapted for Elixir.

Define and invoke subagents to isolate context, run tasks in parallel, and apply specialized instructions in your Claude Agent SDK applications.

Subagents are separate agent instances that your main agent can spawn to handle focused subtasks. Use subagents to isolate context for focused subtasks, run multiple analyses in parallel, and apply specialized instructions without bloating the main agent's prompt.

This guide explains how to define and use subagents in the SDK using the agents option.

Overview

You can create subagents in three ways:

MethodDescription
ProgrammaticUse ClaudeCode.Agent structs with the agents option in start_link/1 or stream/3 (recommended for SDK applications)
Filesystem-basedDefine agents as markdown files in .claude/agents/ directories (see defining subagents as files)
Built-in general-purposeClaude can invoke the built-in general-purpose subagent at any time via the Task tool without you defining anything

This guide focuses on the programmatic approach, which is recommended for SDK applications.

When you define subagents, Claude determines whether to invoke them based on each subagent's description field. Write clear descriptions that explain when the subagent should be used, and Claude will automatically delegate appropriate tasks. You can also explicitly request a subagent by name in your prompt (for example, "Use the code-reviewer agent to...").

Benefits of using subagents

Context management

Subagents maintain separate context from the main agent, preventing information overload and keeping interactions focused. This isolation ensures that specialized tasks don't pollute the main conversation context with irrelevant details.

Example: a research-assistant subagent can explore dozens of files and documentation pages without cluttering the main conversation with all the intermediate search results, returning only the relevant findings.

Parallelization

Multiple subagents can run concurrently, dramatically speeding up complex workflows.

Example: during a code review, you can run style-checker, security-scanner, and test-coverage subagents simultaneously, reducing review time from minutes to seconds.

Specialized instructions and knowledge

Each subagent can have tailored system prompts with specific expertise, best practices, and constraints.

Example: a database-migration subagent can have detailed knowledge about SQL best practices, rollback strategies, and data integrity checks that would be unnecessary noise in the main agent's instructions.

Tool restrictions

Subagents can be limited to specific tools, reducing the risk of unintended actions.

Example: a doc-reviewer subagent might only have access to Read and Grep tools, ensuring it can analyze but never accidentally modify your documentation files.

How agents are delivered

Agent configurations are sent to the CLI via the control protocol initialize handshake, not as CLI flags. When a session starts, the adapter sends an initialize control request that includes the agents map. This matches the behavior of the Python/TypeScript Agent SDKs.

Creating subagents

Define subagents directly in your code using ClaudeCode.Agent structs and the agents option. This example creates two subagents: a code reviewer with read-only access and a test runner that can execute commands. The Task tool must be included in allowed_tools since Claude invokes subagents through the Task tool.

alias ClaudeCode.Agent

{:ok, session} = ClaudeCode.start_link(
  agents: [
    Agent.new(
      name: "code-reviewer",
      # description tells Claude when to use this subagent
      description: "Expert code review specialist. Use for quality, security, and maintainability reviews.",
      # prompt defines the subagent's behavior and expertise
      prompt: """
      You are a code review specialist with expertise in security, performance, and Elixir best practices.

      When reviewing code:
      - Identify security vulnerabilities
      - Check for performance issues
      - Verify adherence to coding standards
      - Suggest specific improvements

      Be thorough but concise in your feedback.
      """,
      # tools restricts what the subagent can do (read-only here)
      tools: ["Read", "Grep", "Glob"],
      # model overrides the default model for this subagent
      model: "sonnet"
    ),
    Agent.new(
      name: "test-runner",
      description: "Runs and analyzes test suites. Use for test execution and coverage analysis.",
      prompt: """
      You are a test execution specialist. Run tests and provide clear analysis of results.

      Focus on:
      - Running test commands
      - Analyzing test output
      - Identifying failing tests
      - Suggesting fixes for failures
      """,
      # Bash access lets this subagent run test commands
      tools: ["Bash", "Read", "Grep"]
    )
  ],
  # Task tool is required for subagent invocation
  allowed_tools: ["Read", "Grep", "Glob", "Task"]
)

Agent fields

FieldTypeRequiredDescription
namestringYesUnique agent identifier (used as the map key when sent to the CLI)
descriptionstringRecommendedNatural language description of when to use this agent. Claude uses this to decide when to delegate.
promptstringRecommendedSystem prompt defining the agent's role and behavior
toolslistNoTools the agent can use. If omitted, inherits all tools.
modelstringNoModel override: "sonnet", "opus", "haiku", or "inherit". Defaults to session model.

The Elixir SDK allows nil for description and prompt, but the official SDK treats them as required. Always provide both for reliable subagent behavior.

Subagents cannot spawn their own subagents. Don't include "Task" in a subagent's tools list.

Filesystem-based definition (alternative)

You can also define subagents as markdown files in .claude/agents/ directories. See the Claude Code subagents documentation for details on this approach. Programmatically defined agents take precedence over filesystem-based agents with the same name.

Even without defining custom subagents, Claude can spawn the built-in general-purpose subagent when Task is in your allowed_tools. This is useful for delegating research or exploration tasks without creating specialized agents.

Invoking subagents

Automatic invocation

Claude automatically decides when to invoke subagents based on the task and each subagent's description. For example, if you define a performance-optimizer subagent with the description "Performance optimization specialist for query tuning", Claude will invoke it when your prompt mentions optimizing queries.

Write clear, specific descriptions so Claude can match tasks to the right subagent.

# Claude will automatically delegate to the code-reviewer agent
session
|> ClaudeCode.stream("Review the authentication module for security issues")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)

Explicit invocation

To guarantee Claude uses a specific subagent, mention it by name in your prompt. This bypasses automatic matching and directly invokes the named subagent.

session
|> ClaudeCode.stream("Use the code-reviewer agent to check the authentication module")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)

Dynamic agent configuration

Create agent definitions dynamically based on runtime conditions. This example creates a security reviewer with different strictness levels, using a more capable model for strict reviews:

defmodule MyApp.Agents do
  alias ClaudeCode.Agent

  # Factory function that returns an Agent struct
  # This pattern lets you customize agents based on runtime conditions
  def security_reviewer(level) do
    strict? = level == :strict

    Agent.new(
      name: "security-reviewer",
      description: "Security code reviewer",
      # Customize the prompt based on strictness level
      prompt: if(strict?,
        do: "You are a strict security reviewer. Flag all potential issues, even minor ones.",
        else: "You are a balanced security reviewer. Focus on critical and high-severity issues."
      ),
      tools: ["Read", "Grep", "Glob"],
      # Key insight: use a more capable model for high-stakes reviews
      model: if(strict?, do: "opus", else: "sonnet")
    )
  end
end

# The agent is created at session time, so each session can use different settings
{:ok, session} = ClaudeCode.start_link(
  agents: [MyApp.Agents.security_reviewer(:strict)],
  allowed_tools: ["Read", "Grep", "Glob", "Task"]
)

Detecting subagent invocation

Subagents are invoked via the Task tool. To detect when a subagent is invoked, check for tool_use blocks with name: "Task". Messages from within a subagent's context include a parent_tool_use_id field.

This example iterates through streamed messages, logging when a subagent is invoked and when subsequent messages originate from within that subagent's execution context:

session
|> ClaudeCode.stream("Review the code and write tests for lib/my_app.ex")
|> Stream.each(fn
  %ClaudeCode.Message.AssistantMessage{message: %{content: blocks}} ->
    Enum.each(blocks, fn
      %ClaudeCode.Content.ToolUseBlock{name: "Task", input: input} ->
        IO.puts("Subagent invoked: #{input["subagent_type"]}")
      _ -> :ok
    end)

  %{parent_tool_use_id: id} when not is_nil(id) ->
    IO.puts("  (running inside subagent)")

  _ -> :ok
end)
|> Stream.run()

Resuming subagents

Subagents can be resumed to continue where they left off. Resumed subagents retain their full conversation history, including all previous tool calls, results, and reasoning. The subagent picks up exactly where it stopped rather than starting fresh.

When a subagent completes, Claude receives its agent ID in the Task tool result. To resume a subagent programmatically:

  1. Capture the session ID: Extract session_id from messages during the first query
  2. Extract the agent ID: Parse agentId from the message content
  3. Resume the session: Pass resume: session_id in the second query's options, and include the agent ID in your prompt

You must resume the same session to access the subagent's transcript. Each ClaudeCode.query/2 call starts a new session by default, so pass resume: session_id to continue in the same session.

If you're using a custom agent (not a built-in one), you also need to pass the same agent definition in the agents option for both queries.

The example below demonstrates this flow: the first query runs a subagent and captures the session ID and agent ID, then the second query resumes the session to ask a follow-up question that requires context from the first analysis.

alias ClaudeCode.Message.{AssistantMessage, SystemMessage}

# First invocation - use the Explore agent to find API endpoints
{agent_id, session_id} =
  ClaudeCode.query("Use the Explore agent to find all API endpoints in this codebase",
    allowed_tools: ["Read", "Grep", "Glob", "Task"]
  )
  |> Enum.reduce({nil, nil}, fn
    %SystemMessage{session_id: sid}, {aid, _sid} ->
      {aid, sid}

    %AssistantMessage{message: %{content: blocks}}, {aid, sid} ->
      # Search content blocks for the agentId (appears in Task tool results)
      new_aid =
        blocks
        |> Enum.find_value(fn block ->
          text = inspect(block)
          case Regex.run(~r/agentId:\s*([a-f0-9-]+)/, text) do
            [_, id] -> id
            _ -> nil
          end
        end)

      {new_aid || aid, sid}

    _other, acc ->
      acc
  end)

# Second invocation - resume and ask follow-up
if agent_id && session_id do
  ClaudeCode.query(
    "Resume agent #{agent_id} and list the top 3 most complex endpoints",
    allowed_tools: ["Read", "Grep", "Glob", "Task"],
    resume: session_id
  )
  |> ClaudeCode.Stream.final_result()
end

Subagent transcripts persist independently of the main conversation:

  • Main conversation compaction: When the main conversation compacts, subagent transcripts are unaffected. They're stored in separate files.
  • Session persistence: Subagent transcripts persist within their session. You can resume a subagent after restarting Claude Code by resuming the same session.
  • Automatic cleanup: Transcripts are cleaned up based on the cleanupPeriodDays setting (default: 30 days).

Per-query agent overrides

Override agent definitions for specific queries:

session
|> ClaudeCode.stream("Review this module",
     agents: [
       Agent.new(
         name: "code-reviewer",
         description: "Security-focused code reviewer",
         prompt: "Focus exclusively on security vulnerabilities and OWASP issues.",
         tools: ["Read", "Grep"]
       )
     ])
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)

Using the agent option

Select a specific agent for the entire session:

{:ok, session} = ClaudeCode.start_link(
  agents: [
    Agent.new(
      name: "reviewer",
      description: "Code reviewer",
      prompt: "You review code for quality."
    )
  ],
  agent: "reviewer"
)

Tool restrictions

Subagents can have restricted tool access via the tools field:

  • Omit the field: agent inherits all available tools (default)
  • Specify tools: agent can only use listed tools

This example creates a read-only analysis agent that can examine code but cannot modify files or run commands:

{:ok, session} = ClaudeCode.start_link(
  agents: [
    Agent.new(
      name: "code-analyzer",
      description: "Static code analysis and architecture review",
      prompt: """
      You are a code architecture analyst. Analyze code structure,
      identify patterns, and suggest improvements without making changes.
      """,
      # Read-only tools: no Edit, Write, or Bash access
      tools: ["Read", "Grep", "Glob"]
    )
  ],
  allowed_tools: ["Read", "Grep", "Glob", "Task"]
)

Common tool combinations

Use caseToolsDescription
Read-only analysisRead, Grep, GlobCan examine code but not modify or execute
Test executionBash, Read, GrepCan run commands and analyze output
Code modificationRead, Edit, Write, Grep, GlobFull read/write access without command execution
Full accessAll toolsInherits all tools from parent (omit tools field)

Troubleshooting

Claude not delegating to subagents

If Claude completes tasks directly instead of delegating to your subagent:

  1. Include the Task tool: subagents are invoked via the Task tool, so it must be in allowed_tools
  2. Use explicit prompting: mention the subagent by name in your prompt (for example, "Use the code-reviewer agent to...")
  3. Write a clear description: explain exactly when the subagent should be used so Claude can match tasks appropriately

Subagents not spawning their own subagents

This is by design. Don't include "Task" in a subagent's "tools" list.

Filesystem-based agents not loading

Agents defined in .claude/agents/ are loaded at startup only. If you create a new agent file while Claude Code is running, restart the session to load it.

Windows: long prompt failures

On Windows, subagents with very long prompts may fail due to command line length limits (8191 chars). Keep prompts concise or use filesystem-based agents for complex instructions.