Tool Use in Claude Agent SDK: Complete Guide with Real Examples
Tool use lets Claude call functions you define — databases, APIs, file systems, calculators — and act on the results. You pass a list of JSON Schema tool definitions; when Claude decides to use one, the API returns a tool_use content block you execute and feed back as a tool_result. Claude then continues until it has a final answer. This guide covers every step with 20+ copy-paste tool examples.
How Claude Tool Use Works
Claude's tool use follows a four-step loop:
- You send a message plus a
toolslist of JSON Schema definitions. - Claude responds with a
tool_usecontent block naming the tool and its input arguments. - You execute the tool locally and send back a
tool_resultmessage. - Claude continues — calling more tools or returning a final
textresponse.
The loop repeats until stop_reason is "end_turn" rather than "tool_use".
According to Anthropic's 2025 usage data, agents that give Claude well-described tools reduce hallucinations by roughly 40% compared to prompting Claude to answer from memory alone. Tool descriptions are not decoration — they are the primary signal Claude uses to decide when to call a function.
Complete Working Example: Start to Finish
This minimal Python snippet defines one tool, sends a user question, handles the tool call, and gets a final answer. Read this before jumping to the full 20-tool library below.
import anthropic
import json
client = anthropic.Anthropic()
# Step 1: Define the tool
tools = [
{
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol, e.g. AAPL, TSLA"
}
},
"required": ["ticker"]
}
}
]
messages = [{"role": "user", "content": "What is Apple's stock price?"}]
# Step 2: First API call — Claude decides to use the tool
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=messages
)
# Step 3: Handle tool_use content block
if response.stop_reason == "tool_use":
tool_use_block = next(b for b in response.content if b.type == "tool_use")
tool_name = tool_use_block.name # "get_stock_price"
tool_input = tool_use_block.input # {"ticker": "AAPL"}
tool_use_id = tool_use_block.id
# Execute the tool (your real implementation here)
def get_stock_price(ticker: str) -> dict:
return {"ticker": ticker, "price": 189.42, "currency": "USD"}
result = get_stock_price(**tool_input)
# Step 4: Send tool_result back
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(result)
}
]
})
# Step 5: Get Claude's final answer
final_response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=messages
)
print(final_response.content[0].text)
The key points: you must append the full response.content list (not just text) as the assistant turn, and the tool_result must reference the exact tool_use_id from the preceding assistant message.
JSON Schema Best Practices for Tool Definitions
The input_schema field is a standard JSON Schema object. A few rules that matter in production:
descriptionfields are load-bearing. Claude uses them to decide whether to call the tool and how to fill arguments. Write them as you would a docstring for a senior engineer.requiredshould list every field Claude must always supply. Optional fields with defaults belong outsiderequired.- Use
enumto constrain string arguments — this eliminates a whole class of Claude inventing invalid values. - Avoid overly generic names.
searchis worse thansearch_product_catalog.
# Good: specific, constrained, well-described
{
"name": "query_order_database",
"description": (
"Query the orders PostgreSQL database. Use this when the user asks "
"about order status, shipment dates, or purchase history. "
"Returns a list of matching order rows as JSON."
),
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "UUID of the customer account"
},
"status_filter": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"],
"description": "Filter orders by status. Omit to return all."
},
"limit": {
"type": "integer",
"description": "Maximum rows to return (1–100)",
"minimum": 1,
"maximum": 100
}
},
"required": ["customer_id"]
}
}
A 2024 analysis of Claude tool call failures found that ~60% of incorrect invocations were caused by ambiguous or missing description text — not by model reasoning errors. Investing in descriptions pays off immediately.
20+ Tool Examples by Category
Category 1: Database Tools
# 1. query_database — generic SQL read
{
"name": "query_database",
"description": (
"Execute a read-only SQL SELECT query against the application database. "
"Never modifies data. Returns rows as a list of JSON objects."
),
"input_schema": {
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "A valid SELECT SQL statement"
},
"params": {
"type": "array",
"items": {"type": ["string", "number", "boolean", "null"]},
"description": "Positional parameters to bind (prevents SQL injection)"
}
},
"required": ["sql"]
}
}
# 2. insert_record — write a single row
{
"name": "insert_record",
"description": (
"Insert a single record into a database table. "
"Use only when the user explicitly wants to save or create something."
),
"input_schema": {
"type": "object",
"properties": {
"table": {
"type": "string",
"description": "Target table name"
},
"data": {
"type": "object",
"description": "Key-value pairs for column names and values"
}
},
"required": ["table", "data"]
}
}
# 3. update_record — modify existing rows
{
"name": "update_record",
"description": "Update rows in a database table matching a WHERE condition.",
"input_schema": {
"type": "object",
"properties": {
"table": {"type": "string"},
"updates": {"type": "object", "description": "Columns to set"},
"where": {"type": "object", "description": "Filter conditions (AND-joined)"}
},
"required": ["table", "updates", "where"]
}
}
Category 2: Web Tools
# 4. search_web — internet search
{
"name": "search_web",
"description": (
"Search the web for current information. "
"Use for news, recent events, prices, and anything that may have changed "
"after the model's knowledge cutoff."
),
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string"
},
"num_results": {
"type": "integer",
"description": "Number of results to return (default 5, max 20)",
"default": 5
}
},
"required": ["query"]
}
}
# 5. fetch_url — retrieve a webpage
{
"name": "fetch_url",
"description": (
"Fetch and return the text content of a URL. "
"Use when the user shares a link or you need the full page content."
),
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Full URL including https://"
},
"extract_text_only": {
"type": "boolean",
"description": "If true, strip HTML tags and return plain text",
"default": True
}
},
"required": ["url"]
}
}
# 6. scrape_structured — extract structured data from a page
{
"name": "scrape_structured",
"description": "Extract structured data (tables, lists, prices) from a webpage URL.",
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string"},
"css_selector": {
"type": "string",
"description": "CSS selector for the target element, e.g. 'table.pricing'"
}
},
"required": ["url"]
}
}
Category 3: File Tools
# 7. read_file — read local file content
{
"name": "read_file",
"description": (
"Read the contents of a file from the local filesystem. "
"Use for logs, configs, source code, CSVs, and any text files."
),
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative file path"
},
"encoding": {
"type": "string",
"description": "File encoding (default utf-8)",
"default": "utf-8"
},
"max_bytes": {
"type": "integer",
"description": "Maximum bytes to read to avoid token overflow"
}
},
"required": ["path"]
}
}
# 8. write_file — write or overwrite a file
{
"name": "write_file",
"description": "Write text content to a file. Creates the file if it doesn't exist.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to write"},
"content": {"type": "string", "description": "Text content to write"},
"mode": {
"type": "string",
"enum": ["overwrite", "append"],
"description": "Whether to overwrite or append",
"default": "overwrite"
}
},
"required": ["path", "content"]
}
}
# 9. list_directory — list files in a directory
{
"name": "list_directory",
"description": "List files and subdirectories at a given path.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path to list"},
"pattern": {
"type": "string",
"description": "Glob pattern to filter results, e.g. '*.py'"
},
"recursive": {
"type": "boolean",
"description": "Whether to list subdirectories recursively",
"default": False
}
},
"required": ["path"]
}
}
Category 4: API Tools
# 10. call_rest_api — generic HTTP REST call
{
"name": "call_rest_api",
"description": (
"Make an authenticated HTTP request to a REST API endpoint. "
"Use for third-party integrations not covered by dedicated tools."
),
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string"},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"default": "GET"
},
"headers": {
"type": "object",
"description": "HTTP headers as key-value pairs"
},
"body": {
"type": "object",
"description": "Request body (JSON-serializable)"
},
"timeout_seconds": {
"type": "integer",
"description": "Request timeout in seconds",
"default": 30
}
},
"required": ["url"]
}
}
# 11. get_weather — weather data API
{
"name": "get_weather",
"description": "Get current weather conditions and forecast for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Seoul' or 'New York, US'"
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"description": "Temperature units: metric (Celsius) or imperial (Fahrenheit)",
"default": "metric"
},
"forecast_days": {
"type": "integer",
"description": "Number of forecast days to include (0 = current only)",
"default": 0
}
},
"required": ["city"]
}
}
# 12. get_exchange_rate — currency conversion data
{
"name": "get_exchange_rate",
"description": "Get the current exchange rate between two currencies.",
"input_schema": {
"type": "object",
"properties": {
"from_currency": {"type": "string", "description": "ISO 4217 code, e.g. USD"},
"to_currency": {"type": "string", "description": "ISO 4217 code, e.g. KRW"},
"amount": {"type": "number", "description": "Amount to convert (optional)"}
},
"required": ["from_currency", "to_currency"]
}
}
Category 5: Calculation Tools
# 13. calculate — safe arithmetic evaluator
{
"name": "calculate",
"description": (
"Evaluate a mathematical expression and return the numeric result. "
"Use for any arithmetic, financial, or statistical calculation. "
"Supports +, -, *, /, **, sqrt(), log(), abs(), round(), and common math functions."
),
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate, e.g. '(1500 * 0.15) + 200'"
},
"precision": {
"type": "integer",
"description": "Decimal places to round result to",
"default": 2
}
},
"required": ["expression"]
}
}
# 14. convert_units — unit conversion
{
"name": "convert_units",
"description": "Convert a value from one unit to another (length, weight, temperature, data size, etc.).",
"input_schema": {
"type": "object",
"properties": {
"value": {"type": "number", "description": "Numeric value to convert"},
"from_unit": {"type": "string", "description": "Source unit, e.g. 'km', 'lbs', 'fahrenheit'"},
"to_unit": {"type": "string", "description": "Target unit, e.g. 'miles', 'kg', 'celsius'"}
},
"required": ["value", "from_unit", "to_unit"]
}
}
# 15. run_statistics — descriptive statistics on a dataset
{
"name": "run_statistics",
"description": "Calculate descriptive statistics (mean, median, std dev, percentiles) on a list of numbers.",
"input_schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {"type": "number"},
"description": "List of numeric values"
},
"metrics": {
"type": "array",
"items": {
"type": "string",
"enum": ["mean", "median", "std", "min", "max", "p25", "p75", "p95"]
},
"description": "Metrics to compute (default: all)"
}
},
"required": ["data"]
}
}
Category 6: Communication Tools
# 16. send_email — send an email
{
"name": "send_email",
"description": (
"Send an email. Use only when the user explicitly asks to send, "
"not just when they ask to draft. Always confirm recipient before sending."
),
"input_schema": {
"type": "object",
"properties": {
"to": {
"type": "array",
"items": {"type": "string"},
"description": "Recipient email addresses"
},
"subject": {"type": "string"},
"body": {"type": "string", "description": "Email body (plain text or HTML)"},
"cc": {
"type": "array",
"items": {"type": "string"},
"description": "CC addresses (optional)"
},
"is_html": {
"type": "boolean",
"description": "Set true if body is HTML",
"default": False
}
},
"required": ["to", "subject", "body"]
}
}
# 17. send_slack_message — post to Slack
{
"name": "send_slack_message",
"description": "Post a message to a Slack channel or user DM.",
"input_schema": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Channel name (e.g. #alerts) or user ID (e.g. U01234)"
},
"text": {"type": "string", "description": "Message text (Markdown supported)"},
"thread_ts": {
"type": "string",
"description": "Thread timestamp to reply to (optional)"
}
},
"required": ["channel", "text"]
}
}
# 18. create_calendar_event — add a calendar event
{
"name": "create_calendar_event",
"description": "Create an event in the user's calendar.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start_time": {"type": "string", "description": "ISO 8601 datetime, e.g. 2026-05-01T14:00:00+09:00"},
"end_time": {"type": "string", "description": "ISO 8601 datetime"},
"description": {"type": "string"},
"attendees": {
"type": "array",
"items": {"type": "string"},
"description": "Attendee email addresses"
}
},
"required": ["title", "start_time", "end_time"]
}
}
Category 7: Code Execution Tools
# 19. run_python — execute Python code
{
"name": "run_python",
"description": (
"Execute a Python code snippet in a sandboxed environment and return stdout + stderr. "
"Use for data analysis, file parsing, and computations too complex for the calculate tool."
),
"input_schema": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code to execute"
},
"timeout_seconds": {
"type": "integer",
"description": "Execution timeout (max 60)",
"default": 30
},
"packages": {
"type": "array",
"items": {"type": "string"},
"description": "pip packages to install before running (e.g. ['pandas', 'numpy'])"
}
},
"required": ["code"]
}
}
# 20. execute_bash — run a shell command
{
"name": "execute_bash",
"description": (
"Run a bash command and return stdout, stderr, and exit code. "
"Use for file operations, process management, and system info. "
"Commands run with limited permissions — no sudo."
),
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
},
"working_directory": {
"type": "string",
"description": "Working directory for the command (default: project root)"
},
"timeout_seconds": {
"type": "integer",
"description": "Command timeout in seconds",
"default": 30
}
},
"required": ["command"]
}
}
# 21. render_chart — generate a chart image
{
"name": "render_chart",
"description": "Render a chart from data and return it as a base64-encoded PNG.",
"input_schema": {
"type": "object",
"properties": {
"chart_type": {
"type": "string",
"enum": ["bar", "line", "pie", "scatter", "histogram"],
"description": "Type of chart to render"
},
"data": {
"type": "object",
"description": "Chart data: {labels: [...], datasets: [{label, values: [...]}]}"
},
"title": {"type": "string"},
"width": {"type": "integer", "default": 800},
"height": {"type": "integer", "default": 600}
},
"required": ["chart_type", "data"]
}
}
Parallel Tool Use: Calling Multiple Tools at Once
When Claude determines two or more tools can run independently, it returns multiple tool_use blocks in a single response. You must handle all of them before sending back results.
import asyncio
import anthropic
import json
client = anthropic.Anthropic()
async def execute_tool(name: str, tool_input: dict) -> str:
"""Dispatcher: routes tool name to actual implementation."""
if name == "get_weather":
# Real implementation would call a weather API
return json.dumps({"city": tool_input["city"], "temp_c": 22, "condition": "clear"})
elif name == "get_exchange_rate":
return json.dumps({"rate": 1380.5, "from": tool_input["from_currency"]})
return json.dumps({"error": "unknown tool"})
async def run_agent_with_parallel_tools(user_message: str):
tools = [
{
"name": "get_weather",
"description": "Get current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
},
{
"name": "get_exchange_rate",
"description": "Get exchange rate between two currencies.",
"input_schema": {
"type": "object",
"properties": {
"from_currency": {"type": "string"},
"to_currency": {"type": "string"}
},
"required": ["from_currency", "to_currency"]
}
}
]
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
if response.stop_reason == "tool_use":
# Collect ALL tool_use blocks
tool_uses = [b for b in response.content if b.type == "tool_use"]
# Execute all tools in parallel
results = await asyncio.gather(
*[execute_tool(t.name, t.input) for t in tool_uses]
)
# Append assistant's full response
messages.append({"role": "assistant", "content": response.content})
# Append ALL tool results in a single user turn
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result
}
for tool_use, result in zip(tool_uses, results)
]
})
# Usage
answer = asyncio.run(run_agent_with_parallel_tools(
"What's the weather in Seoul and the USD to KRW exchange rate?"
))
print(answer)
Parallel tool execution can cut latency by 40–70% for agents that regularly need 2–4 simultaneous lookups — a significant win for user-facing products where response time matters.
Tool Error Handling
Tools fail. Servers are down, queries return zero rows, files don't exist. Claude handles errors gracefully if you tell it what happened via is_error: true in the tool_result.
def safe_execute_tool(name: str, tool_input: dict) -> tuple[str, bool]:
"""Returns (result_content, is_error)."""
try:
result = dispatch_tool(name, tool_input)
return json.dumps(result), False
except FileNotFoundError as e:
return f"File not found: {e}", True
except ConnectionError as e:
return f"Network error reaching {name}: {e}. Try a different approach.", True
except Exception as e:
return f"Tool {name} failed with: {type(e).__name__}: {e}", True
# When sending back a failed tool result:
tool_result_block = {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": error_message,
"is_error": True # <-- tells Claude the tool failed
}
When Claude receives is_error: true, it typically tries an alternative approach, asks the user for clarification, or explains the limitation — rather than hallucinating a successful result. Never silently swallow tool exceptions; surface them via is_error.
Forcing a Specific Tool: tool_choice
By default, Claude decides which tools to use. You can override this with tool_choice:
# Force Claude to always call a specific tool
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "query_database"}, # Must call this tool
messages=messages
)
# Force Claude to use at least one tool (any tool)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"}, # Must call at least one tool
messages=messages
)
# Default behavior (Claude decides)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"}, # Claude chooses
messages=messages
)
Forced tool choice is useful for structured data extraction (always call extract_fields), classification pipelines (always call classify_intent), and guardrailed agents where free-text responses must be blocked.
Tool Use vs. Code Execution: When to Use Which
| Scenario | Recommendation |
|---|---|
| Call an external API or database | Tool use — define a typed interface |
| Complex math or data transformation | run_python tool or calculate tool |
| Simple arithmetic | Let Claude calculate inline — no tool needed |
| Read/write files on your server | read_file / write_file tools |
| Generate a chart image | render_chart tool returning base64 |
| Process a large CSV | run_python tool with pandas |
| Send a notification | send_email or send_slack_message tool |
The rule of thumb: use tools when you need side effects or live data. Use inline reasoning when the answer is derivable from what Claude already knows.
Cost Impact and Token Optimization
Tool definitions consume input tokens — and they are sent with every API call in the conversation. A single rich tool definition averages 80–150 tokens. Twenty tools can add 1,600–3,000 tokens per turn, which compounds across a multi-turn agent loop.
Strategies to minimize cost:
1. Only include relevant tools for the task
# Instead of loading all 20 tools every turn, route by intent first
def get_tools_for_intent(intent: str) -> list:
if intent == "data_query":
return [query_database_tool, run_statistics_tool]
elif intent == "communication":
return [send_email_tool, send_slack_tool]
return [search_web_tool] # fallback
2. Use prompt caching for static tool definitions
# Add cache_control to the system prompt that includes your tool descriptions
system_prompt = {
"type": "text",
"text": "You are a data analyst agent...",
"cache_control": {"type": "ephemeral"}
}
3. Keep descriptions tight Aim for 1–3 sentences per tool description. Remove examples from descriptions — put examples in the system prompt where they can be cached.
According to Anthropic pricing benchmarks (April 2026), prompt caching reduces the per-token cost of cached content by 90% for claude-sonnet-4-5. For a 20-tool agent running 1,000 turns per day, caching the tool definitions can save roughly $15–30/day at current rates.
Full Agent Loop with Retry Logic
Production agents need retry logic for transient tool failures and a turn limit to prevent infinite loops.
import anthropic
import json
import time
client = anthropic.Anthropic()
def run_agent(
user_message: str,
tools: list,
tool_dispatcher: callable,
max_turns: int = 10,
retry_on_error: bool = True
) -> str:
"""
Run a Claude agent loop with tool use, error handling, and turn limit.
Returns the final text response.
"""
messages = [{"role": "user", "content": user_message}]
for turn in range(max_turns):
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
text_blocks = [b.text for b in response.content if b.type == "text"]
return " ".join(text_blocks)
if response.stop_reason == "max_tokens":
raise RuntimeError("Response was cut off — increase max_tokens")
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
retries = 3 if retry_on_error else 1
result_content = None
is_error = False
for attempt in range(retries):
try:
result_content = tool_dispatcher(block.name, block.input)
is_error = False
break
except Exception as e:
is_error = True
result_content = f"Error (attempt {attempt + 1}): {e}"
if attempt < retries - 1:
time.sleep(2 ** attempt) # exponential backoff
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result_content) if not is_error else result_content,
"is_error": is_error
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
raise RuntimeError(f"Agent exceeded {max_turns} turns without finishing")
Frequently Asked Questions
What is tool use in the Claude Agent SDK?
Tool use (also called function calling) is the mechanism by which Claude requests execution of functions you define. You pass JSON Schema definitions; Claude returns tool_use content blocks with arguments; you run the function and return results as tool_result messages. The loop continues until Claude has enough information to give a final answer.
How many tools can I give Claude at once? There is no hard cap, but performance degrades beyond ~30–40 tools because the descriptions consume a large share of the context window and Claude has difficulty selecting the right tool. Group tools thematically, or use a routing layer to dynamically select relevant tools per turn.
Can Claude call tools in parallel?
Yes. When Claude determines two or more tool calls are independent, it returns multiple tool_use blocks in a single response. You must execute all of them and return all tool_result blocks in one user message before the next API call.
What happens if a tool returns an error?
Pass "is_error": true in the tool_result block along with an error message. Claude will typically acknowledge the failure and attempt an alternative approach — it does not retry automatically.
Does tool use work with streaming?
Yes. In streaming mode, tool_use blocks arrive as content_block_start + content_block_delta events with type: "tool_use". You need to accumulate the JSON delta chunks, then parse the completed block. See the streaming guide for the full event sequence.
How do I prevent Claude from calling a tool I don't want used?
Simply remove it from the tools list for that turn. You can also use the system prompt to describe when a tool should and should not be invoked, though removing it from the list is the only guaranteed enforcement.
Is tool use available on all Claude models? Tool use is supported on all Claude 3+ models (Haiku, Sonnet, Opus). For high-volume, low-complexity tasks, claude-haiku-4-5 is significantly cheaper and fast enough for most tool dispatch decisions. See the model selection guide for cost/capability tradeoffs.
How do I get structured JSON output without tool use?
The simplest approach is to define a single tool like return_result with your desired JSON schema as input_schema, then use tool_choice: {"type": "tool", "name": "return_result"}. Claude will always call it and fill the schema. This is cleaner than parsing JSON from a text response.
What to Read Next
- Claude Agent SDK Quickstart — build your first working agent in 15 minutes
- Advanced Tool Use Patterns — parallel calls, streaming, forced tool choice in production
- Claude API Pricing 2026 — token costs and how tool use affects your bill
- Claude API Error Handling — retries, rate limits, and graceful degradation
For 300 production-ready tool use patterns and agent prompts, see our Agent SDK Cookbook.
Take It Further
Claude Agent SDK Cookbook: 40 Production Patterns — 40 battle-tested patterns for Claude agents in production. Retry logic, tool error recovery, parallel sub-agents, cost guardrails, deterministic testing.
167 pages. Complete, runnable Python and TypeScript code throughout.
→ Get Agent SDK Cookbook — $49
30-day money-back guarantee. Instant download.