Claude’s tool use lets you give the model access to functions it can call mid-conversation. The real power shows up in multi-turn flows where Claude decides which tools to call, you execute them, feed results back, and Claude keeps going – sometimes calling more tools based on what it learned. Here’s the core pattern:

 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
import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'San Francisco'"}
            },
            "required": ["city"]
        }
    }
]

response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)

print(response.stop_reason)  # "tool_use"
print(response.content)
# [ToolUseBlock(type='tool_use', id='toolu_01...', name='get_weather', input={'city': 'Tokyo'})]

When stop_reason is "tool_use", Claude wants you to run something. The response content array contains tool_use blocks with the function name and arguments. Your job is to execute the function and send the result back.

Defining Tools with JSON Schema

Each tool needs a name, description, and input_schema following JSON Schema. The description matters a lot – Claude uses it to decide when to call the tool. Be specific about what the tool does, what it returns, 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
tools = [
    {
        "name": "get_weather",
        "description": "Get current weather conditions for a specific city. Returns temperature in Fahrenheit, conditions, and humidity.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name, e.g. 'San Francisco' or 'London'"
                },
                "units": {
                    "type": "string",
                    "enum": ["fahrenheit", "celsius"],
                    "description": "Temperature unit. Defaults to fahrenheit."
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "schedule_event",
        "description": "Schedule a calendar event. Returns a confirmation with the event ID.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "Event title"
                },
                "date": {
                    "type": "string",
                    "description": "Event date in YYYY-MM-DD format"
                },
                "time": {
                    "type": "string",
                    "description": "Event time in HH:MM 24-hour format"
                },
                "duration_minutes": {
                    "type": "integer",
                    "description": "Duration in minutes, defaults to 60"
                }
            },
            "required": ["title", "date", "time"]
        }
    }
]

A few things to get right: required should only list genuinely required fields. Optional parameters with good descriptions let Claude use them when context makes it obvious. Enum values constrain the model’s output – use them whenever a parameter has a fixed set of valid values.

The Tool Use Message Flow

The conversation follows a specific structure. Claude sends a tool_use block, you respond with a tool_result block, and Claude generates its final answer (or calls another tool). Here’s the exact message structure:

 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
87
88
89
90
91
92
93
94
95
96
import anthropic
import json

client = anthropic.Anthropic()

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

def schedule_event(title, date, time, duration_minutes=60):
    event_id = f"evt_{hash(title + date + time) % 100000:05d}"
    return {"event_id": event_id, "title": title, "date": date, "time": time, "duration_minutes": duration_minutes}

def execute_tool(name, tool_input):
    if name == "get_weather":
        return get_weather(**tool_input)
    elif name == "schedule_event":
        return schedule_event(**tool_input)
    else:
        return {"error": f"Unknown tool: {name}"}

# Start the conversation
messages = [
    {"role": "user", "content": "What's the weather in San Francisco? If it's nice, schedule a picnic for tomorrow at 2pm."}
]

tools = [
    {
        "name": "get_weather",
        "description": "Get current weather conditions for a specific city. Returns temperature, conditions, and humidity.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
                "units": {"type": "string", "enum": ["fahrenheit", "celsius"]}
            },
            "required": ["city"]
        }
    },
    {
        "name": "schedule_event",
        "description": "Schedule a calendar event. Returns confirmation with event ID.",
        "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": "Time in HH:MM 24-hour format"},
                "duration_minutes": {"type": "integer", "description": "Duration in minutes"}
            },
            "required": ["title", "date", "time"]
        }
    }
]

response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

# Claude asks to check the weather first
# Append the assistant's full response to messages
messages.append({"role": "assistant", "content": response.content})

# Process each tool_use block
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 tool results back
messages.append({"role": "user", "content": tool_results})

# Claude now sees the weather and may call schedule_event
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

The key detail: tool_use_id in each tool_result must match the id from the corresponding tool_use block. Claude uses this to pair results with requests. Get it wrong and you’ll get a validation error.

Building a Conversation Loop

Manually chaining calls gets tedious. Wrap it in a 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
 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
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
import anthropic
import json

client = anthropic.Anthropic()

def get_weather(city, units="fahrenheit"):
    weather_data = {
        "San Francisco": {"temp": 62, "conditions": "Foggy", "humidity": 78},
        "Tokyo": {"temp": 55, "conditions": "Clear", "humidity": 45},
    }
    data = weather_data.get(city, {"temp": 70, "conditions": "Sunny", "humidity": 50})
    if units == "celsius":
        data["temp"] = round((data["temp"] - 32) * 5 / 9)
    return data

def schedule_event(title, date, time, duration_minutes=60):
    event_id = f"evt_{hash(title + date + time) % 100000:05d}"
    return {"event_id": event_id, "title": title, "date": date, "time": time, "duration_minutes": duration_minutes}

TOOL_DISPATCH = {
    "get_weather": get_weather,
    "schedule_event": schedule_event,
}

tools = [
    {
        "name": "get_weather",
        "description": "Get current weather conditions for a city. Returns temperature, conditions, humidity.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "units": {"type": "string", "enum": ["fahrenheit", "celsius"]}
            },
            "required": ["city"]
        }
    },
    {
        "name": "schedule_event",
        "description": "Schedule a calendar event. Returns event ID and confirmation.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "date": {"type": "string", "description": "YYYY-MM-DD"},
                "time": {"type": "string", "description": "HH:MM 24-hour"},
                "duration_minutes": {"type": "integer"}
            },
            "required": ["title", "date", "time"]
        }
    }
]

def run_conversation(user_message, system_prompt=None):
    messages = [{"role": "user", "content": user_message}]
    kwargs = {
        "model": "claude-sonnet-4-5-20250514",
        "max_tokens": 4096,
        "tools": tools,
        "messages": messages,
    }
    if system_prompt:
        kwargs["system"] = system_prompt

    while True:
        response = client.messages.create(**kwargs)
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            # Claude is done -- extract the final text
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text
            return final_text

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    func = TOOL_DISPATCH.get(block.name)
                    if func:
                        try:
                            result = func(**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,
                                "is_error": True,
                                "content": f"Error executing {block.name}: {str(e)}",
                            })
                    else:
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "is_error": True,
                            "content": f"Unknown tool: {block.name}",
                        })

            messages.append({"role": "user", "content": tool_results})
            kwargs["messages"] = messages

result = run_conversation(
    "Check the weather in San Francisco and Tokyo. If either city is below 60F, schedule an indoor meeting for 2026-02-20 at 10:00.",
    system_prompt="You are a helpful assistant with access to weather and calendar tools. Today is 2026-02-18."
)
print(result)

This loop handles everything: single tool calls, multiple tool calls in one response, errors, and unknown tools. The is_error: True flag on tool results tells Claude the call failed, so it can adjust its response instead of hallucinating a result.

Notice that response.content can contain both text and tool_use blocks in the same response. Claude might say “Let me check that for you” alongside a tool call. Always append the full response.content as the assistant message, not just the tool blocks.

Handling Multiple Tool Calls in One Response

Claude can request multiple tools in a single response. When asking about weather in two cities, you’ll get two tool_use blocks. You must return a tool_result for every tool_use block in a single user message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Claude's response.content might look like:
# [
#   TextBlock(type='text', text="I'll check both cities."),
#   ToolUseBlock(type='tool_use', id='toolu_01abc', name='get_weather', input={'city': 'San Francisco'}),
#   ToolUseBlock(type='tool_use', id='toolu_01def', name='get_weather', input={'city': 'Tokyo'}),
# ]

# Your tool_result message MUST include results for BOTH tool calls:
tool_results_message = {
    "role": "user",
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": "toolu_01abc",  # matches first tool_use
            "content": json.dumps({"temp": 62, "conditions": "Foggy", "humidity": 78})
        },
        {
            "type": "tool_result",
            "tool_use_id": "toolu_01def",  # matches second tool_use
            "content": json.dumps({"temp": 55, "conditions": "Clear", "humidity": 45})
        }
    ]
}

Missing a tool_result for any tool_use block causes a 400 error. The loop in the previous section handles this correctly by iterating over all blocks.

Common Errors and Fixes

tool_use_id mismatch

1
anthropic.BadRequestError: 400 - tool_result block(s) that do not have a matching tool_use block

Every tool_result must reference a tool_use_id from the immediately preceding assistant message. Copy the id field directly from the tool_use block – don’t generate your own.

Missing tool_result for a tool call

1
anthropic.BadRequestError: 400 - Mismatch between tool_use and tool_result blocks

If Claude’s response has 3 tool_use blocks, your next message needs exactly 3 tool_result blocks. The conversation loop above handles this by iterating over every block with type == "tool_use".

Sending tool_result without the assistant tool_use message

1
anthropic.BadRequestError: 400 - tool_result is not allowed as the first content block

You must append Claude’s full response (including the tool_use blocks) as an assistant message before sending tool_result. This is easy to forget:

1
2
3
4
5
6
# WRONG - skipping the assistant message
messages.append({"role": "user", "content": tool_results})

# RIGHT - include the assistant's response first
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})

Tool execution failure

When your function throws, don’t crash the loop. Return an error result so Claude can handle it gracefully:

1
2
3
4
5
6
tool_results.append({
    "type": "tool_result",
    "tool_use_id": block.id,
    "is_error": True,
    "content": "Weather service is temporarily unavailable. Try again later."
})

Claude will acknowledge the failure and adjust – often telling the user it couldn’t complete the request, or trying an alternative approach.

Invalid JSON schema in tool definition

1
anthropic.BadRequestError: 400 - Invalid tool input_schema

The input_schema must be a valid JSON Schema object with type: "object" at the top level. You can’t use type: "string" or type: "array" at the root – tool inputs are always objects. Double-check that properties is a dict and required is a list of strings.