Introduction: Building Agent Teams

In the previous lessons, you learned how to build powerful single agents by managing state, connecting external MCP servers, and creating custom tools. These techniques allow you to create agents that can handle complex tasks with specialized capabilities. However, some problems are so multifaceted that even a well-equipped single agent struggles to handle them optimally. Just as software development teams divide work among specialists — backend engineers, frontend developers, and database administrators — your agent systems can benefit from the same division of labor.

In this lesson, you'll learn how to design multi-agent systems in which specialized agents coordinate to solve complex problems. You'll discover how to define sub-agents with specific roles, configure a main orchestrator agent that delegates work, and observe how these agents communicate through the SDK's internal Task tool. By the end of this lesson, you'll understand how to build agent hierarchies that leverage each specialist's unique strengths, creating systems that are more capable than any single agent could be.

The Sub-Agent Concept

A sub-agent is a specialized agent designed to excel at a specific type of task. Rather than creating one generalist agent that tries to do everything, you create multiple focused agents, each optimized for its particular domain. The main agent, often called an orchestrator, coordinates these specialists by delegating appropriate tasks to each one.

Consider a code review workflow. A single agent could analyze code, fix bugs, and write documentation, but this approach has limitations. The agent might use an expensive model like Sonnet for all tasks, even simple documentation that could be handled by the faster, cheaper Haiku model. It might also struggle to maintain focus, mixing analysis concerns with implementation details. A multi-agent approach solves these problems by creating three specialists: an analyzer that uses Sonnet with Read and Grep tools to deeply understand code structure, a fixer that uses Sonnet with Read and Write tools to implement high-quality corrections, and a documenter that uses Haiku with Read and Write tools to efficiently generate clear documentation. Each agent focuses on what it does best, using the right model and tools for its specific role.

Multi-agent architectures shine when tasks have distinct phases that require different capabilities, when different subtasks benefit from different models or tool sets, when you want to optimize costs by using cheaper models for simpler tasks, or when you need clear separation of concerns for maintainability. The orchestrator agent doesn't need to know how to do everything — it just needs to know which specialist to call for each type of work.

Defining Sub-Agents with AgentDefinition

Before you can create a multi-agent system, you need to define each sub-agent using the AgentDefinition class. This class allows you to specify everything that makes each agent unique: its role, its instructions, the tools it can use, and which model powers it.

from claude_agent_sdk import AgentDefinition

# Analyzer Agent: Uses Sonnet for deep reasoning and Read/Grep tools to explore the codebase
analyzer_agent = AgentDefinition(
    description="Code analysis specialist",
    prompt="You are a code analyzer. Review code for issues, patterns, and improvements.",
    tools=["Read", "Grep"],
    model="sonnet"
)

The AgentDefinition takes four key parameters that shape the agent's behavior and capabilities:

  • description: A brief label that helps the orchestrator understand what this agent does — think of it as the agent's job title
  • prompt: The system instructions that define how this agent should approach its work, similar to the system_prompt you've used in previous lessons but specific to this sub-agent's role
  • tools: A list of tool names this agent is allowed to use, giving you fine-grained control over each agent's capabilities. If omitted, the agent will inherit all tools available to the orchestrator
  • model: Which Claude model powers this agent. You can specify "haiku", "sonnet", or "opus" just like you do for the main agent. There's also a special "inherit" option that makes the sub-agent use whatever model the orchestrator is using.

In this example, the analyzer agent is configured as:

  • A code analysis specialist
  • With Read and Grep tools for examining code structure
  • Powered by Sonnet for deep reasoning

Let's define two more specialists to complete our code review team.

Defining the Fixer and Documenter Agents

The fixer and documenter agents complete our specialized team, each optimized for their specific roles in the code improvement workflow.

# Fixer Agent: Uses Sonnet for high-quality code generation and Read/Write tools to apply fixes
fixer_agent = AgentDefinition(
    description="Bug fixing specialist",
    prompt="You are a bug fixer. Identify and fix code issues efficiently.",
    tools=["Read", "Write"],
    model="sonnet"
)

# Documenter Agent: Uses Haiku for speed/cost efficiency, perfect for generating text/docs
documenter_agent = AgentDefinition(
    description="Documentation specialist",
    prompt="You are a documentation writer. Create clear, comprehensive documentation.",
    tools=["Read", "Write"],
    model="haiku"
)

The fixer agent is designed to implement corrections:

  • Focuses on identifying and fixing issues efficiently through its prompt
  • Has access to Read for examining code and Write for making changes
  • Uses Sonnet because generating correct, high-quality code fixes requires sophisticated reasoning about code semantics and potential side effects

The documenter agent takes a different approach:

  • Has Read and Write tools like the fixer
  • Uses the Haiku model instead of Sonnet
  • This is a strategic choice because documentation generation is primarily a text generation task that doesn't require the same level of complex reasoning as code analysis or bug fixing
  • Haiku is faster and more cost-effective, making it perfect for this role

This demonstrates one of the key advantages of multi-agent systems: you can optimize each agent's model choice based on the complexity of its specific task. Now that we have all three specialists defined, we can configure the orchestrator that will coordinate them.

Configuring the Orchestrator

With your sub-agents defined, you now configure the main orchestrator agent that will coordinate them. This happens through the agents parameter in ClaudeAgentOptions, which creates a registry of available specialists to which the orchestrator can delegate work.

options = ClaudeAgentOptions(
    model="haiku",
    max_turns=15,
    allowed_tools=["Read", "Write", "Bash", "Grep"],
    permission_mode="acceptEdits",
    # Register the sub-agents in the orchestrator's agent registry using descriptive keys
    agents={
        "analyzer": analyzer_agent,
        "fixer": fixer_agent,
        "documenter": documenter_agent
    }
)

The configuration establishes the orchestrator's capabilities and its team of specialists:

  • model="haiku": The orchestrator's job is primarily coordination and delegation, which doesn't require the reasoning power of Sonnet. The orchestrator needs to understand the task, decide which specialist to call, and synthesize their results — tasks that Haiku handles efficiently.
  • max_turns=15: Allows for extended conversations since multi-agent workflows often require multiple back-and-forth exchanges as specialists complete their work and report back
  • allowed_tools: Acts as a global allow-list for the entire multi-agent system — sub-agents can only use tools that appear in this list.
  • permission_mode="acceptEdits": Set to automatically enable other file editing tools
  • agents: This dictionary maps string keys to AgentDefinition objects, creating a registry of specialists. The keys you choose here become the identifiers the orchestrator uses when delegating work, so descriptive names like "analyzer", "fixer", and "documenter" make it clear which specialist handles which type of work.

However, defining sub-agents and registering them isn't enough — you also need to tell the orchestrator how to use them.

Orchestration Through System Prompts

The system_prompt in a multi-agent system defines the delegation strategy and workflow. Unlike previous lessons, where the system prompt simply guided a single agent's behavior, here it tells the orchestrator which specialists to call and in what order.

options = ClaudeAgentOptions(
    model="haiku",
    max_turns=15,
    allowed_tools=["Read", "Write", "Bash", "Grep"],
    permission_mode="acceptEdits",
    agents={
        "analyzer": analyzer_agent,
        "fixer": fixer_agent,
        "documenter": documenter_agent
    },
    # Define the orchestration strategy and workflow for the orchestrator via the system prompt
    system_prompt=(
        "You are a Lead Developer. Your goal is to improve code quality.\n"
        "When asked to improve a file, you MUST follow this strict process:\n"
        "1. Delegate to 'analyzer' to find issues\n"
        "2. Delegate to 'fixer' to resolve them\n"
        "3. Delegate to 'documenter' to add docs\n"
        "Report back after each step."
    )
)

The system prompt establishes the orchestrator's identity as a Lead Developer and defines its goal: improving code quality. More importantly, it provides explicit instructions for the delegation workflow. The phrase "you MUST follow this strict process" creates a strong directive that the orchestrator should follow these steps in order. The numbered list specifies exactly which specialist to call at each stage, using the same keys defined in the agents dictionary. The instruction to "Report back after each step" ensures the orchestrator communicates progress to the user, making the multi-agent workflow transparent. This system prompt is deliberately prescriptive because orchestration requires clear coordination — without explicit instructions, the orchestrator might skip specialists, call them in the wrong order, or try to do the work itself instead of delegating. The prompt essentially defines a workflow in which analysis happens first to understand what needs fixing, then fixes are applied to resolve identified issues, and finally documentation is added to make the improved code maintainable. To understand how this delegation actually works, we need to look at the Task tool.

Observing Delegation with the Task Tool

When the orchestrator delegates work to a sub-agent, the Claude Agent SDK uses an internal tool called Task to make the delegation happen. Understanding how this works helps you observe and debug multi-agent interactions.

from claude_agent_sdk import AssistantMessage, TextBlock, ToolUseBlock

async def display_response(client: ClaudeSDKClient):
    """Helper function to display agent messages with formatting"""
    async for message in client.receive_response():
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(f"\n💬 Claude Response:")
                    print(block.text)
                elif isinstance(block, ToolUseBlock):
                    print(f"\n🔧 [Tool: {block.name}]")
                    # Display input if tool is Task
                    if block.name == "Task":
                        if block.input:
                            for key, value in block.input.items():
                                print(f"{key}: {value}")

This enhanced version of display_response checks if a tool being used is named "Task". When it detects a Task tool call, it prints the input parameters, which reveal exactly how the orchestrator is delegating work. The Task tool's input typically includes parameters like subagent_type (which specialist to call), description (a brief summary of what to do), and prompt (detailed instructions for the sub-agent). By displaying these parameters, you can see which specialist is being called, what task they're being given, and what specific instructions they're receiving. This visibility is valuable because it helps you verify that the orchestrator is following the delegation strategy you defined in the system prompt, shows you how the orchestrator translates high-level goals into specific instructions for each specialist, and helps you debug issues by revealing exactly what instructions each sub-agent received. Now let's see the complete multi-agent system in action.

Running the Multi-Agent System

With all the pieces in place — the three specialized sub-agents defined, the orchestrator configured with its delegation strategy, and the display_response helper ready to observe the workflow — you're ready to execute the multi-agent system. The agents will work on a Python file that contains several types of issues, making it an ideal candidate for demonstrating how different specialists handle different aspects of code improvement.

Here's the file that the agents will improve:

def calculate_average(numbers):
    total = 0
    for num in numbers:
        total += num
    return total / len(numbers)

def process_data(data):
    # TODO: Add error handling
    return calculate_average(data)

This code has no input validation, risks a ZeroDivisionError when the list is empty, uses an inefficient manual loop instead of the built-in sum() function, lacks type hints and docstrings, and has an unresolved TODO comment. These issues span multiple categories — bugs, code quality, and documentation problems — making it perfect for demonstrating how the analyzer identifies problems, the fixer resolves them, and the documenter adds comprehensive documentation.

To run the multi-agent system, you create the client with your configured options and issue a simple, high-level query:

# Run the agent
async with ClaudeSDKClient(options=options) as client:
    await client.query("Please improve the sample.py file.")

    await display_response(client)

The query is deliberately high-level — it doesn't specify how to improve the file or what steps to take because the orchestrator's system prompt handles that coordination. The orchestrator will ensure the work flows through the analyzer, fixer, and documenter in sequence, with each specialist using its specific tools and model to handle its part of the workflow. Let's observe how this unfolds.

Observing the Orchestrator's Preparation

When you run the complete example, the orchestrator begins by explaining its plan and using its own tools to locate and examine the file before delegating to specialists.

💬 Claude Response:
I'll help you improve the sample.py file. First, let me locate and read the file to understand its current state.

🔧 [Tool: Glob]

💬 Claude Response:
Now let me read the file:

🔧 [Tool: Read]

💬 Claude Response:
Good! I found the file. Now I'll follow the strict improvement process:

## Step 1: Analyze for Issues

Let me delegate to the analyzer to find issues:

The orchestrator starts by explaining its approach and using its own tools to prepare for delegation. It uses Glob to search for the file in the filesystem and Read to examine the current contents. This demonstrates an important aspect of multi-agent systems: the orchestrator isn't just blindly delegating — it's intelligently preparing by gathering the information it needs. After confirming it found the file, the orchestrator explicitly states it will follow the strict three-step process defined in its system prompt. This shows the orchestrator is following its instructions to work methodically through analysis, fixing, and documentation phases. Now let's observe how it delegates to the analyzer.

Observing the Analyzer Delegation

With the file located and examined, the orchestrator delegates to the analyzer specialist with detailed instructions about what to investigate.

🔧 [Tool: Task]
subagent_type: analyzer
description: Analyze sample.py for issues
prompt: Analyze the file /usercode/FILESYSTEM/sample.py and identify all code quality issues, including:
- Error handling gaps
- Code style problems
- Performance issues
- Missing validation
- Missing documentation
- Logic errors
- Best practice violations

Provide a detailed report of all issues found.

The orchestrator calls the Task tool to delegate to the analyzer. The Task tool input reveals the delegation details: subagent_type: analyzer specifies which specialist to call, the description provides a brief summary of the task, and the prompt contains comprehensive instructions organized as a bulleted list of what to analyze. This structured prompt ensures the analyzer knows exactly what to look for — error handling gaps, code style problems, performance issues, missing validation, missing documentation, logic errors, and best practice violations. By providing this level of detail, the orchestrator guides the analyzer to conduct a thorough investigation across all relevant quality dimensions. The analyzer will now use its Read and Grep tools to examine the code.

Observing the Analyzer's Work

The analyzer specialist uses its tools to examine the code and produces a comprehensive report categorizing all the issues it found.

🔧 [Tool: Read]

💬 Claude Response:
Excellent! The analyzer found 7 categories of issues. Now let me proceed to **Step 2: Fix the issues**.

The analyzer uses its Read tool to examine the file contents and identifies issues across seven categories. The orchestrator reports back that the analysis is complete and summarizes the finding — seven categories of issues were discovered. While the detailed report isn't shown in full here, the orchestrator's response confirms that the analyzer successfully identified error handling gaps, code style problems, performance issues, missing validation, missing documentation, logic errors, and best practice violations. This comprehensive analysis provides the foundation for the next phase. The orchestrator explicitly announces it's moving to Step 2, demonstrating it's following the strict process defined in its system prompt. Now let's observe how it delegates the fixing work.

Observing the Fixer Delegation

With the analysis complete, the orchestrator moves to the second phase by delegating to the fixer specialist with highly detailed instructions based on the analyzer's findings.

🔧 [Tool: Task]
subagent_type: fixer
description: Fix issues in sample.py
prompt: Fix all the code quality issues found in /usercode/FILESYSTEM/sample.py. Based on the analysis, implement these fixes:

1. Add error handling for empty lists and division by zero (critical)
2. Add input type validation for numeric values
3. Replace manual summation loop with built-in sum() function
4. Add comprehensive docstrings with parameter and return documentation
5. Add type hints to all functions
6. Add input validation with proper error messages
7. Implement the TODO for error handling
8. Either remove the redundant process_data() function or give it meaningful functionality
9. Fix style issues (use += instead of total = total + num)
10. Add module-level docstring

Write the improved version back to the file /usercode/FILESYSTEM/sample.py with all fixes applied. Ensure the code is production-ready with proper error handling, validation, documentation, and follows PEP 8 style guidelines.

The orchestrator calls the Task tool with subagent_type: fixer and provides an exceptionally detailed prompt. The prompt translates the analyzer's findings into a numbered list of ten specific fixes to implement. Each item is concrete and actionable — add error handling for specific cases, replace the loop with sum(), add type hints, implement the TODO, and so on. The prompt prioritizes critical bugs like division by zero at the top and includes instructions about ensuring production-ready code quality. This demonstrates the orchestrator's coordination skill: it synthesizes information from the analyzer and translates it into clear, prioritized instructions for the fixer. The fixer doesn't need to re-analyze the code; it receives an explicit checklist of what to fix. Now the fixer will use its Read and Write tools to apply these corrections.

Observing the Fixer's Work

The fixer specialist reads the current code, applies all the necessary corrections, and writes the improved version back to the file.

🔧 [Tool: Read]

🔧 [Tool: Write]

💬 Claude Response:
Great! Now let me proceed to **Step 3: Add Documentation**.

The fixer uses Read to examine the current code, then Write to apply all ten fixes from the detailed checklist. The orchestrator confirms the fixes are complete and explicitly announces it's moving to Step 3, demonstrating it continues to follow the strict process. At this point, the code has been transformed — it now includes proper error handling for empty lists and division by zero, input type validation, the efficient sum() function instead of a manual loop, type hints throughout, comprehensive docstrings, resolved TODOs, enhanced functionality in process_data(), PEP 8 compliant style, and a module-level docstring. The code is functionally correct and well-structured. Now the orchestrator delegates to the documenter to add the final layer of comprehensive documentation.

Observing the Documenter Delegation

With the code functionally correct and well-structured, the orchestrator moves to the final phase by delegating to the documenter specialist to enhance the documentation.

🔧 [Tool: Task]
subagent_type: documenter
description: Add comprehensive documentation to sample.py
prompt: Add comprehensive documentation to /usercode/FILESYSTEM/sample.py. Review the current file and enhance it with:

1. Detailed module-level documentation explaining the purpose and usage
2. Complete docstrings for all functions (if not already present)
3. Inline comments for complex logic or important validations
4. Type hint clarifications if needed
5. Usage examples in docstrings
6. Any additional documentation that would help developers understand and maintain this code

The goal is to make the code self-documenting and easy for other developers to understand and use.

The orchestrator calls the Task tool with subagent_type: documenter and provides detailed instructions about what documentation to add. The prompt is organized as a numbered list covering module-level documentation, function docstrings, inline comments, type hint clarifications, usage examples, and any additional helpful documentation. Importantly, the prompt acknowledges that some documentation may already be present from the fixer's work (note the phrase "if not already present"), instructing the documenter to review and enhance rather than blindly overwrite. The goal statement at the end emphasizes making the code "self-documenting" — documentation so clear that developers can understand and maintain the code easily. This is where using Haiku for the documenter pays off, as documentation enhancement is primarily a text generation task that doesn't require Sonnet's reasoning capabilities. The documenter will now use its Read and Write tools to add comprehensive documentation.

Observing the Documenter's Work and Final Summary

The documenter specialist reads the fixed code, adds comprehensive documentation, writes the enhanced version, and the orchestrator verifies the results before providing a detailed summary of the entire workflow.

🔧 [Tool: Read]

🔧 [Tool: Write]

💬 Claude Response:
Perfect! Let me verify the final improved version:

🔧 [Tool: Read]

💬 Claude Response:
## 🎉 Improvement Complete!

I've successfully improved the **sample.py** file following the strict 3-step process:

### **Step 1: Analysis ✅**
Found **7 categories of issues**:
- Critical error handling gaps (division by zero, type validation)
- Missing docstrings and type hints
- Inefficient manual summation loop
- No input validation
- Redundant function design
- Poor code style
- Missing documentation

### **Step 2: Fixes Applied ✅**
- ✓ Added comprehensive error handling with descriptive messages
- ✓ Replaced manual loop with efficient `sum()` function
- ✓ Added type hints throughout (`List[Union[int, float]]`)
- ✓ Implemented complete input validation
- ✓ Enhanced `process_data()` to provide meaningful statistics
- ✓ Fixed style issues (PEP 8 compliant)

### **Step 3: Documentation Added ✅**
- ✓ Comprehensive module-level docstring (55 lines)
- ✓ Detailed function docstrings with NumPy conventions
- ✓ Multiple practical usage examples for each function
- ✓ Inline comments explaining validation logic and design decisions
- ✓ Clear error documentation and cross-references

### **Key Improvements:**
- **From 10 lines** to **325 lines** of well-documented, production-ready code
- **Robust error handling** with clear, actionable error messages
- **Type safety** with full type hints
- **Edge case protection** (empty lists, non-numeric values, boolean rejection)
- **Self-documenting code** that's easy for other developers to understand and maintain

The file is now **production-ready** with enterprise-level code quality! 🚀

The documenter uses Read to examine the fixed code, then Write to add comprehensive documentation throughout. After the documentation is complete, the orchestrator performs a verification step by reading the final version one more time to confirm the results. This demonstrates good orchestration practice — verifying work completion before declaring success. The orchestrator then provides an impressive final summary organized into sections for each phase. For Step 1, it recaps the seven categories of issues found. For Step 2, it lists the six major fixes applied with checkmarks. For Step 3, it details the documentation enhancements including specific metrics like "55 lines" of module-level documentation. The summary concludes with key improvements that include a dramatic transformation metric: from 10 lines of problematic code to 325 lines of production-ready, well-documented code. This demonstrates the power of multi-agent coordination — each specialist focused on what it does best while the orchestrator ensured they worked together in the right sequence to achieve a remarkable transformation.

Extending Sub-Agents with MCP Tools

The multi-agent code review system you just built demonstrates how specialized agents coordinate through built-in tools like Read, Write, and Grep. But what if your sub-agents need capabilities from custom tools or external MCP servers? Perhaps your analyzer needs to search API documentation, or your documenter needs to query a knowledge base.

To enable sub-agents to use MCP tools, you must follow one critical rule: the orchestrator's allowed_tools must include any MCP tools that sub-agents will use. The orchestrator acts as a gatekeeper — if an MCP tool isn't in its allowed_tools, no sub-agent can use it, even if the tool is specified in the sub-agent's configuration.

Let's enhance our code review system with a documentation researcher that uses MCP tools:

# Connect Context7 MCP server
context7_config = {
    "type": "stdio",
    "command": "npx",
    "args": ["-y", "@upstash/context7-mcp"]
}

# Researcher sub-agent that uses MCP tools
researcher_agent = AgentDefinition(
    description="Documentation researcher",
    prompt="Search library documentation to help understand best practices.",
    tools=[
        "mcp__docs__resolve-library-id",
        "mcp__docs__get-library-docs",
        "Read"
    ],
    model="haiku"
)

options = ClaudeAgentOptions(
    model="haiku",
    mcp_servers={"docs": context7_config},
    # Orchestrator must allow MCP tools for sub-agents to use them
    allowed_tools=[
        "Read", "Write", "Grep",
        "mcp__docs__resolve-library-id",
        "mcp__docs__get-library-docs"
    ],
    agents={
        "analyzer": analyzer_agent,
        "fixer": fixer_agent,
        "documenter": documenter_agent,
        "researcher": researcher_agent
    }
)

The orchestrator's allowed_tools includes both its own tools (Read, Write, Grep) and the MCP tools that the researcher sub-agent needs. The researcher can now use these MCP tools to fetch API documentation and inform the code review process with authoritative information about best practices.

Summary: Mastering Multi-Agent Coordination

You've now mastered the art of designing sophisticated multi-agent systems that coordinate specialized agents to solve complex problems. The pattern is clear: define each sub-agent using AgentDefinition with its specific description, prompt, tools, and model; register these specialists in the agents parameter using descriptive keys; configure the orchestrator's delegation strategy through a detailed system_prompt; and observe the delegation through the Task tool to understand the workflow. Sub-agents can access built-in tools and MCP tools, allowing you to equip specialists with external capabilities. Multi-agent systems excel when tasks have distinct phases, when different subtasks benefit from different models or tool sets, when you want to optimize costs, and when you need clear separation of concerns.

In the upcoming practice exercises, you'll design your own multi-agent systems for different domains, experiment with orchestration strategies, and build workflows that leverage each agent's unique strengths to create solutions more powerful than any single agent could achieve alone!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal