Technology · Claude API
IntermediateClaude API Tool Use
A quick reference for defining tools, running the agentic loop, and using the SDK tool runner with Claude.
- 01Define tools with a name, description, and JSON Schema input_schema.
- 02When stop_reason is tool_use, execute the tool and send results back.
- 03Use the SDK beta tool runner to handle the loop automatically.
Define a Tool
Each tool needs a
name,description, andinput_schema(JSON Schema).tools = [ { "name": "get_weather", "description": "Returns current weather for a given city.", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "The city name, e.g. 'London'" } }, "required": ["city"] } } ]Pass the tools array to
messages.create().message = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=[{"role": "user", "content": "What's the weather in Tokyo?"}] )Write clear, specific descriptions — Claude uses them to decide when to call a tool.
Mark every field the tool requires in the
requiredarray ofinput_schema.Keep tool names short and verb-noun:
get_weather,search_docs,run_query.
The Agentic Loop
When Claude calls a tool,
stop_reasonis"tool_use"— do not treat it as a final response.if message.stop_reason == "tool_use": # extract tool calls, run them, send results backExtract tool calls from
message.contentby filtering fortype == "tool_use".tool_calls = [b for b in message.content if b.type == "tool_use"] for call in tool_calls: print(call.name) # "get_weather" print(call.input) # {"city": "Tokyo"} print(call.id) # "toolu_..."Execute each tool and build
tool_resultcontent blocks.tool_results = [] for call in tool_calls: result = run_tool(call.name, call.input) # your dispatch logic tool_results.append({ "type": "tool_result", "tool_use_id": call.id, "content": str(result) })Append Claude's response and the tool results to
messages, then call the API again.messages.append({"role": "assistant", "content": message.content}) messages.append({"role": "user", "content": tool_results}) next_message = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages )Loop until
stop_reason == "end_turn"— Claude may call multiple tools in sequence.while message.stop_reason == "tool_use": # ... run tools and send results message = client.messages.create(...) final_text = message.content[0].text
SDK Tool Runner
The SDK beta tool runner handles the agentic loop automatically.
import anthropic from anthropic.lib.beta import beta_tool # Python beta @beta_tool def get_weather(city: str) -> str: """Returns current weather for a given city.""" return f"Sunny, 22°C in {city}" response = client.beta.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=[get_weather], messages=[{"role": "user", "content": "Weather in Tokyo?"}] ) print(response.content[0].text)In TypeScript, use
betaZodToolwith a Zod schema for type-safe inputs.import { betaZodTool } from "@anthropic-ai/sdk/beta"; import { z } from "zod"; const getWeather = betaZodTool({ name: "get_weather", description: "Returns current weather for a given city.", schema: z.object({ city: z.string() }), execute: async ({ city }) => `Sunny, 22°C in ${city}`, }); const response = await client.beta.messages.create({ model: "claude-opus-4-8", max_tokens: 1024, tools: [getWeather], messages: [{ role: "user", content: "Weather in Tokyo?" }], });The tool runner calls your function and sends the result back automatically — no manual loop.
Use the manual loop when you need approval gates, custom logging, or conditional execution.
Parallel tool calls: Claude may invoke multiple tools in one turn — the runner handles all of them.
Tool Choice Control
tool_choicelets you control whether Claude must, may, or must not use tools.Value Behaviour {"type": "auto"}Claude decides whether to call a tool (default) {"type": "any"}Claude must call at least one tool {"type": "tool", "name": "X"}Claude must call the named tool {"type": "none"}Claude must not call any tool Force a specific tool call to guarantee structured output from Claude.
message = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, tool_choice={"type": "tool", "name": "get_weather"}, messages=[{"role": "user", "content": "Tokyo please"}] )Use
"none"to disable tools mid-conversation once you have the data you need.disable_parallel_tool_use: Trueforces one tool call per turn if order matters.tool_choice={"type": "auto", "disable_parallel_tool_use": True}
Tip: Use
tool_choice: {type: "tool"}instead of asking Claude to produce structured JSON — it's more reliable and type-safe.
Warning: Always send the full
assistantcontent block (including tool_use blocks) back in the next turn — omitting it causes a validation error.