Technology · Claude API

Intermediate

Claude API Tool Use

A quick reference for defining tools, running the agentic loop, and using the SDK tool runner with Claude.

TL;DR
  1. 01Define tools with a name, description, and JSON Schema input_schema.
  2. 02When stop_reason is tool_use, execute the tool and send results back.
  3. 03Use the SDK beta tool runner to handle the loop automatically.

Define a Tool

  • Each tool needs a name, description, and input_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 required array of input_schema.

  • Keep tool names short and verb-noun: get_weather, search_docs, run_query.

The Agentic Loop

  • When Claude calls a tool, stop_reason is "tool_use" — do not treat it as a final response.

    if message.stop_reason == "tool_use":
        # extract tool calls, run them, send results back
    
  • Extract tool calls from message.content by filtering for type == "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_result content 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 betaZodTool with 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_choice lets 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: True forces 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 assistant content block (including tool_use blocks) back in the next turn — omitting it causes a validation error.

Claude Thinking and Effort