← All guides

Claude Agent SDK Guide: Build Automation Agents with Tool Use

Build production agents with the Claude API: tool use, agentic loops, error handling, memory, streaming, deployment. Python and TypeScript examples.

Claude Agent SDK Guide: Build Automation Agents with Tool Use

The "Claude Agent SDK" is the Anthropic Python/TypeScript SDK combined with the tool use feature — it's not a separate package. An agent is just a loop: send a message, Claude calls a tool, you execute the tool, send the result back, repeat until done. This guide covers the complete pattern for building reliable production agents.

Want 40 production agent patterns in one PDF?

The Agent SDK Cookbook ($49) has every pattern in this guide plus 30 more — retry logic, multi-agent orchestration, cost guardrails, deterministic testing — with complete Python and TypeScript code.

Get Agent SDK Cookbook →


Quick Start

pip install anthropic
import anthropic

client = anthropic.Anthropic()

# Minimal agent with one tool
tools = [{
    "name": "get_weather",
    "description": "Get current weather for a location",
    "input_schema": {
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "City name"}
        },
        "required": ["location"]
    }
}]

messages = [{"role": "user", "content": "What's the weather in Seoul?"}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

print(response.stop_reason)  # "tool_use" if Claude wants to call a tool

The Agentic Loop

The core pattern: run until stop_reason == "end_turn".

import anthropic
import json

client = anthropic.Anthropic()

def run_agent(tools: list, tool_executor: callable, initial_message: str) -> str:
    """
    Generic agentic loop.
    tool_executor: function(tool_name, tool_input) -> str (tool result)
    Returns: final text response
    """
    messages = [{"role": "user", "content": initial_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )

        # Done — return the text
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return ""

        # Claude wants to call tools
        if response.stop_reason == "tool_use":
            tool_calls = [b for b in response.content if b.type == "tool_use"]
            
            # Execute all tool calls
            tool_results = []
            for call in tool_calls:
                result = tool_executor(call.name, call.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": call.id,
                    "content": str(result)
                })

            # Add assistant response + tool results to history
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        else:
            # Unexpected stop reason
            break

    return ""

Defining Tools

Basic Tool

SEARCH_TOOL = {
    "name": "search_docs",
    "description": "Search the documentation for relevant information",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "Search query"
            },
            "max_results": {
                "type": "integer",
                "description": "Maximum number of results to return",
                "default": 5
            }
        },
        "required": ["query"]
    }
}

Tool with Enum

STATUS_TOOL = {
    "name": "update_ticket_status",
    "description": "Update the status of a support ticket",
    "input_schema": {
        "type": "object",
        "properties": {
            "ticket_id": {"type": "string"},
            "status": {
                "type": "string",
                "enum": ["open", "in_progress", "resolved", "closed"]
            },
            "note": {"type": "string", "description": "Optional note"}
        },
        "required": ["ticket_id", "status"]
    }
}

Tool with Nested Object

EMAIL_TOOL = {
    "name": "send_email",
    "description": "Send an email",
    "input_schema": {
        "type": "object",
        "properties": {
            "to": {"type": "string"},
            "subject": {"type": "string"},
            "body": {"type": "string"},
            "attachments": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "filename": {"type": "string"},
                        "content_type": {"type": "string"}
                    }
                }
            }
        },
        "required": ["to", "subject", "body"]
    }
}

Error Handling in Tool Results

When a tool fails, return an error in the tool result — don't raise an exception:

def execute_tool(name: str, input: dict) -> str:
    try:
        if name == "search_docs":
            results = search_database(input["query"])
            return json.dumps(results)
        elif name == "update_ticket":
            success = update_ticket(input["ticket_id"], input["status"])
            return json.dumps({"success": success})
        else:
            return json.dumps({"error": f"Unknown tool: {name}"})
    except Exception as e:
        # Return error as tool result — Claude will handle it
        return json.dumps({"error": str(e), "tool": name})

Claude will read the error and decide whether to retry, try a different approach, or explain the problem to the user.


Production Patterns

Timeout and Retry

import time
from anthropic import APITimeoutError, APIConnectionError, RateLimitError

def run_agent_with_retry(
    tools: list,
    tool_executor: callable,
    message: str,
    max_retries: int = 3,
    max_turns: int = 20
) -> str:
    messages = [{"role": "user", "content": message}]
    turns = 0

    while turns < max_turns:
        # Retry logic for API errors
        for attempt in range(max_retries):
            try:
                response = client.messages.create(
                    model="claude-sonnet-4-5",
                    max_tokens=4096,
                    tools=tools,
                    messages=messages,
                    timeout=30.0
                )
                break
            except RateLimitError:
                if attempt == max_retries - 1:
                    raise
                time.sleep(2 ** attempt)  # Exponential backoff
            except (APITimeoutError, APIConnectionError):
                if attempt == max_retries - 1:
                    raise
                time.sleep(1)

        turns += 1

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return ""

        if response.stop_reason == "tool_use":
            tool_calls = [b for b in response.content if b.type == "tool_use"]
            tool_results = []
            for call in tool_calls:
                result = tool_executor(call.name, call.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": call.id,
                    "content": str(result)
                })
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

    raise RuntimeError(f"Agent exceeded max_turns ({max_turns})")

Token Cost Tracking

from dataclasses import dataclass, field

@dataclass
class AgentStats:
    input_tokens: int = 0
    output_tokens: int = 0
    cache_read_tokens: int = 0
    turns: int = 0

    @property
    def estimated_cost_usd(self) -> float:
        # claude-sonnet-4-5 pricing
        input_cost = self.input_tokens * 3.0 / 1_000_000
        output_cost = self.output_tokens * 15.0 / 1_000_000
        cache_cost = self.cache_read_tokens * 0.3 / 1_000_000
        return input_cost + output_cost + cache_cost


def run_agent_with_stats(tools, tool_executor, message) -> tuple[str, AgentStats]:
    stats = AgentStats()
    messages = [{"role": "user", "content": message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )

        stats.input_tokens += response.usage.input_tokens
        stats.output_tokens += response.usage.output_tokens
        stats.turns += 1

        if response.stop_reason == "end_turn":
            result = next((b.text for b in response.content if hasattr(b, "text")), "")
            return result, stats

        if response.stop_reason == "tool_use":
            tool_calls = [b for b in response.content if b.type == "tool_use"]
            tool_results = [
                {
                    "type": "tool_result",
                    "tool_use_id": c.id,
                    "content": str(tool_executor(c.name, c.input))
                }
                for c in tool_calls
            ]
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

TypeScript Agent

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function runAgent(
  tools: Anthropic.Tool[],
  toolExecutor: (name: string, input: Record<string, unknown>) => Promise<string>,
  userMessage: string
): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage }
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 4096,
      tools,
      messages
    });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find(b => b.type === "text");
      return textBlock?.type === "text" ? textBlock.text : "";
    }

    if (response.stop_reason === "tool_use") {
      const toolCalls = response.content.filter(b => b.type === "tool_use");
      const toolResults = await Promise.all(
        toolCalls.map(async (call) => {
          if (call.type !== "tool_use") return null;
          const result = await toolExecutor(call.name, call.input as Record<string, unknown>);
          return {
            type: "tool_result" as const,
            tool_use_id: call.id,
            content: result
          };
        })
      );

      messages.push({ role: "assistant", content: response.content });
      messages.push({ role: "user", content: toolResults.filter(Boolean) as Anthropic.ToolResultBlockParam[] });
    }
  }
}

Specialized Agent Guides

Related API & SDK Guides


Frequently Asked Questions

Is there a separate "Agent SDK" package? No — the agent pattern is built into the standard anthropic Python/TypeScript SDK via the tool use feature. There's no separate installation.

How many tool calls can an agent make? There's no hard limit on turns, but each turn adds context. For long-running tasks, implement context compression. The max_turns guard in your loop prevents infinite loops.

Can multiple agents work together? Yes — you can have an orchestrator agent that spawns subagent tasks. In Claude Code, use --isolation worktree for parallel agents. In custom code, run multiple run_agent() calls concurrently with asyncio.gather().

What's the difference between claude-sonnet and claude-opus for agents? Opus is better at complex multi-step reasoning and ambiguous instructions. Sonnet is 5x cheaper and handles 90% of agent tasks equally well. Start with Sonnet; upgrade specific agents to Opus if results are poor.


Go Deeper

Agent SDK Cookbook — $49 — 15 complete agent implementations: customer support agent, data pipeline agent, code review agent, DevOps monitoring agent, sales outreach agent. Production-ready code with error handling, testing, and deployment configs.

→ Get the Agent SDK Cookbook — $49

30-day money-back guarantee. Instant download.

AI Disclosure: Written with Claude Code.

Tools and references