§15.02

Claude API Tool Use

Define a tool with a JSON schema, catch the tool_use block, run your function, and send back a tool_result — in a minimal Python loop.

published 06 Sept 2026 checked against docs 06 Sept 2026 4 min in Building with the API Markdown

Step 2 of 6 · Build with the Claude API

On this page4 sections
  1. 1. Define the tool
  2. 2. What comes back
  3. 3. The loop
  4. Errors and guarantees

Claude never executes your code. It returns a structured request to call a function; your app runs it and sends the answer back.

1. Define the tool

tools = [{
    "name": "get_weather",
    "description": "Get the current weather for a given location.",
    "input_schema": {
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "City and state, e.g. San Francisco, CA",
            }
        },
        "required": ["location"],
    },
}]

name, description and input_schema are the only required fields. The description is part of the prompt — write it like documentation, not like a variable name.

2. What comes back

{
  "stop_reason": "tool_use",
  "content": [
    { "type": "text", "text": "Let me check." },
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    }
  ]
}

stop_reason: "tool_use" is the signal to run something. Keep the id — your result has to reference it.

3. The loop

import json
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

def get_weather(location):
    return {"location": location, "temp_c": 15, "sky": "partly cloudy"}

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

while True:
    response = client.messages.create(
        model="claude-opus-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
    if response.stop_reason != "tool_use":
        break

    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(get_weather(**block.input)),
            })
    messages.append({"role": "user", "content": results})

print(next(b.text for b in response.content if b.type == "text"))

Runs until Claude stops asking for tools. Two rules that bite people: tool results go back in a user message, and every tool_use block from one turn must be answered in a single message — splitting them teaches the model to stop calling tools in parallel.

Errors and guarantees

{
    "type": "tool_result",
    "tool_use_id": block.id,
    "content": "Error: unknown city 'xyz'. Ask the user for a valid city.",
    "is_error": True,
}

Reports a failed call instead of dropping it, so Claude can retry or ask a follow-up question. Add "strict": True to a tool definition (with required and "additionalProperties": false) to guarantee the arguments validate against your schema.

block.input is already a parsed dict — use it as data, never string-match the serialized JSON.


Next: reliable JSON output · your first Claude API request.

← All Building with the API plates · Search all guides

↑↓ move↵ openalt+↵ copy first command

Keyboard

⌘/ctrl+K or /
Search all guides
alt+↵
In search: copy the guide's first command
j / k
Move through a list of guides
c
On a guide: copy its first command
t
Toggle light / dark
?
This list