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.
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 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.
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.