Claude’s tool use API lets you give the model a set of functions it can call, then handle the results in a loop until the task is finished. This is the core pattern behind every agentic workflow – the model decides what to do, you execute it, and feed the result back. Here’s the minimal version so you can see the shape of it before we get into details:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "get_weather",
        "description": "Get current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"}
            },
            "required": ["city"]
        }
    }],
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)

print(response.stop_reason)  # "tool_use"
print(response.content)      # Contains a ToolUseBlock with name and input

When Claude decides it needs to call a tool, the response comes back with stop_reason: "tool_use" and one or more ToolUseBlock objects in the content array. Your job is to execute those calls and send the results back.

Defining Tools with JSON Schema

Each tool is a dictionary with a name, description, and an input_schema that follows JSON Schema. The description matters a lot – Claude uses it to decide when and how to call the tool, so be specific about what the tool does and when to use it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a given city. Returns temperature in Fahrenheit, conditions, and humidity.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name, e.g. 'San Francisco'"
                },
                "units": {
                    "type": "string",
                    "enum": ["fahrenheit", "celsius"],
                    "description": "Temperature units. Defaults to fahrenheit."
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "check_calendar",
        "description": "Check the user's calendar for events on a specific date. Returns a list of events with times and titles.",
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {
                    "type": "string",
                    "description": "Date in YYYY-MM-DD format"
                }
            },
            "required": ["date"]
        }
    },
    {
        "name": "create_calendar_event",
        "description": "Create a new calendar event. Use this when the user asks to schedule something.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Event title"},
                "date": {"type": "string", "description": "Date in YYYY-MM-DD format"},
                "time": {"type": "string", "description": "Start time in HH:MM format (24h)"},
                "duration_minutes": {"type": "integer", "description": "Duration in minutes"}
            },
            "required": ["title", "date", "time"]
        }
    }
]

A few things to keep in mind: the name must be alphanumeric with underscores (no dashes or spaces). The description should explain both what the tool returns and when Claude should pick it. If you have similar tools, make the descriptions distinct enough that Claude picks the right one.

Processing Tool Calls and Sending Results

When Claude responds with a tool call, you need to parse the ToolUseBlock, run your function, and send the result back as a tool_result message. Each tool use block has an id that you must reference in the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import anthropic
import json

client = anthropic.Anthropic()

# Simulated tool implementations
def get_weather(city, units="fahrenheit"):
    # In production, call a real weather API
    data = {
        "San Francisco": {"temp": 62, "conditions": "Foggy", "humidity": 78},
        "Tokyo": {"temp": 55, "conditions": "Clear", "humidity": 45},
        "London": {"temp": 48, "conditions": "Rainy", "humidity": 90},
    }
    result = data.get(city, {"temp": 70, "conditions": "Unknown", "humidity": 50})
    if units == "celsius":
        result["temp"] = round((result["temp"] - 32) * 5 / 9)
    return result

def check_calendar(date):
    events = {
        "2026-02-18": [
            {"time": "09:00", "title": "Team standup"},
            {"time": "14:00", "title": "Design review"},
        ],
        "2026-02-19": [
            {"time": "10:00", "title": "Client call"},
        ],
    }
    return events.get(date, [])

def create_calendar_event(title, date, time, duration_minutes=60):
    return {"status": "created", "title": title, "date": date, "time": time, "duration_minutes": duration_minutes}

# Map tool names to functions
tool_handlers = {
    "get_weather": get_weather,
    "check_calendar": check_calendar,
    "create_calendar_event": create_calendar_event,
}

def execute_tool(name, input_args):
    handler = tool_handlers.get(name)
    if handler is None:
        return {"error": f"Unknown tool: {name}"}
    try:
        return handler(**input_args)
    except Exception as e:
        return {"error": str(e)}

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

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,  # defined in previous section
    messages=messages,
)

# Process tool calls
if response.stop_reason == "tool_use":
    # Add Claude's response to message history
    messages.append({"role": "assistant", "content": response.content})

    # Execute each tool call and collect results
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = execute_tool(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result),
            })

    # Send results back
    messages.append({"role": "user", "content": tool_results})

    # Get Claude's final answer
    final_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
    print(final_response.content[0].text)

The tool_result content can be a string or a list of content blocks. For most cases, just JSON-serialize your function’s return value. The tool_use_id must match the id from the corresponding ToolUseBlock – if you get this wrong, the API returns an error.

Building the Agentic Loop

The real power shows up when you let Claude chain multiple tool calls. A single user question like “Is it good weather for my outdoor meeting tomorrow?” requires checking the calendar, getting the weather, and synthesizing an answer. The pattern is a while loop that keeps running until Claude stops asking for tools.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import anthropic
import json

client = anthropic.Anthropic()

def run_agent(user_message, tools, tool_handlers, model="claude-sonnet-4-20250514", max_turns=10):
    """Run an agentic loop that processes tool calls until Claude gives a final answer."""
    messages = [{"role": "user", "content": user_message}]

    for turn in range(max_turns):
        response = client.messages.create(
            model=model,
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )

        # If Claude is done, return the text response
        if response.stop_reason == "end_turn":
            text_parts = [block.text for block in response.content if hasattr(block, "text")]
            return "\n".join(text_parts)

        # If Claude wants to use tools, process them
        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})

            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"  [Tool call] {block.name}({json.dumps(block.input)})")
                    result = execute_tool(block.name, block.input)
                    print(f"  [Result] {json.dumps(result)}")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result),
                    })

            messages.append({"role": "user", "content": tool_results})

    return "Agent reached maximum turns without completing."

# Run the agent with a multi-step question
answer = run_agent(
    "Do I have any meetings tomorrow (2026-02-19)? And what's the weather going to be like in San Francisco? "
    "Should I plan anything outdoors?",
    tools=tools,
    tool_handlers=tool_handlers,
)
print(answer)

Claude will call check_calendar with 2026-02-19, then get_weather with San Francisco, and finally combine both results into a natural language answer. Sometimes it calls both tools in a single response (parallel tool use) – your code handles this correctly because it iterates over all tool_use blocks.

The max_turns guard is important. Without it, a buggy tool or a confusing prompt can cause an infinite loop. In production, you’d also want to add token budget tracking and timeout logic.

Handling Errors in Tool Results

When a tool call fails, you should still send a tool_result back – but mark it as an error. Claude handles errors gracefully and will either retry with different parameters or explain the problem to the user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
tool_results = []
for block in response.content:
    if block.type == "tool_use":
        try:
            result = execute_tool(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result),
            })
        except Exception as e:
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps({"error": str(e)}),
                "is_error": True,
            })

The is_error field tells Claude that the tool execution failed. Without it, Claude might try to interpret the error message as a valid result. When is_error is True, Claude knows to either try a different approach or tell the user what went wrong.

Common Errors and Fixes

tool_use_id mismatch: Every tool_result must reference the exact id from the corresponding tool_use block. If you hardcode or mix up IDs, you get a 400 error. Always read the ID from block.id directly.

Missing tool_result for a tool call: If Claude calls two tools and you only send one result back, the API rejects the request. You must send a tool_result for every tool_use block in the assistant’s response.

stop_reason is "end_turn" but you expected "tool_use": This happens when Claude decides it can answer without tools, or when your tool descriptions don’t clearly match the question. Improve your tool descriptions and make sure the description field explains when the tool should be used.

Tool name validation error: Tool names must match ^[a-zA-Z0-9_]+$. No dashes, no spaces, no special characters. Use snake_case.

Schema validation errors on input_schema: The schema must be a valid JSON Schema object. Common mistakes include forgetting "type": "object" at the top level, or putting required inside properties instead of at the same level as properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Wrong - required inside properties
{
    "type": "object",
    "properties": {
        "city": {"type": "string", "required": True}  # Wrong!
    }
}

# Correct - required at top level
{
    "type": "object",
    "properties": {
        "city": {"type": "string"}
    },
    "required": ["city"]  # Correct
}

Response content is a mix of text and tool_use blocks: Claude can return both text and tool calls in the same response. Always iterate over response.content and handle each block by its type – don’t assume the first block is always text or always a tool call.

Rate limits during agentic loops: Each iteration of the agent loop is a separate API call. If your agent chains 8-10 tool calls, that’s 8-10 API requests in quick succession. Use exponential backoff or the SDK’s built-in retry logic (client = anthropic.Anthropic(max_retries=3)).