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.
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
- Claude Agent Testing and Eval — Unit tests, integration tests, eval harness
- Claude Agent Memory Patterns — Conversation history, compression, vector memory
- Streaming vs Batch Agent Patterns — When to stream, when to batch
- Production Deployment for Claude Agents — Fly.io, AWS Lambda, Vercel
- Claude Agent SDK + Playwright — Browser automation agents
- Claude Agent for Data Pipelines — ETL, validation, quality checks
- Claude DevOps Agent — Monitoring and remediation
- Building a Content Generation Agent — Multi-stage quality pipeline
- Sales Outreach Agent with Compliance — CAN-SPAM/GDPR compliance
Related API & SDK Guides
- Python SDK Complete Guide — Full Python SDK reference
- Node.js & TypeScript SDK Guide — Node.js and TypeScript integration
- Concurrent Requests & Rate Limiting — Concurrency patterns and rate limit handling
- Node.js 가이드 (한국어) — 한국어 Node.js 가이드
- Structured Outputs and JSON with Claude — Guaranteed structured responses
- Vision and Multimodal with Claude — Images, PDFs, and mixed inputs
- API Authentication Setup — Keys, headers, and environment config
- Files API Guide — Upload and reference files in API calls
- Real-World Claude API Use Cases — Production integration patterns
- Go (Golang) API Guide — Go SDK integration and concurrency patterns
- API Cost Monitoring Guide — Track, alert, and optimize API spend
- Java & Spring Boot API Guide — Claude API integration with Java and Spring Boot
- Rust API Guide — Claude API integration with Rust and async patterns
- Error Codes Reference — Complete reference for Claude API error codes and handling
- PHP & Laravel API Guide — PHP integration with Guzzle and Laravel HTTP client
- C# .NET API Guide — Claude API integration with C# and .NET
- Semantic Search & RAG Patterns — Voyage AI embeddings, vector search, and RAG pipelines
- GraphQL Integration Guide — Apollo Server resolvers, subscriptions, streaming
- Slack Bot with Claude API — Build a Bolt.js Slack bot powered by Claude
- PDF & Document Parsing — Native PDF support, multi-page strategies, structured extraction
- Discord Bot with Claude API — discord.js v14 bot with slash commands and conversation context
- AWS Bedrock vs Direct Anthropic API — Pricing, latency, IAM, and feature parity comparison
- Content Moderation with Claude — Toxicity, NSFW, spam, PII classification at scale
- Ruby on Rails API Guide — Anthropic gem, ActionController::Live streaming, ActiveJob
- Swift & iOS API Guide — SwiftUI streaming chat with secure backend proxy pattern
- Translation & Localization with Claude — Batch JSON localization, glossary consistency, formatting preservation
- Email Generation Templates — Personalized cold/warm/transactional email at scale
- Meeting Notes Summarization — Whisper/Zoom transcripts to action items + decisions
- Claude vs GPT-4 Benchmark (2026) — Side-by-side pricing, context, latency, MMLU/HumanEval
- JWT Authentication for Claude Proxies — RS256 issuance, middleware, refresh rotation, per-user quotas
- Migrate from OpenAI SDK to Claude — Line-by-line migration guide
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.