When I first integrated Tool Use into Beautiful HD Wallpapers—an app I've been developing since 2014 that now has over 50 million cumulative downloads—the error I hit wasn't what I expected. The schema looked fine on the surface. Claude wasn't throwing an obvious error. But the tool_result I was returning had a subtle structural mismatch with what the API expected.
The frustrating part: it all came back as a generic invalid_request_error. No line number, no field name, just a cryptic message that could mean a dozen different things. I spent more time on that one issue than I care to admit.
If you're implementing Tool Use and something's not working, here's the diagnostic flow I now use every time.
Understanding the 3-Phase Structure First
Tool Use involves three distinct phases, and knowing which phase your error occurs in narrows down the cause immediately.
Phase 1: Define tools — You pass a tools array to the API with your tool schemas. Errors here mean the schema structure is malformed.
Phase 2: Claude calls a tool — The response contains a tool_use block with Claude's chosen arguments. Errors here are rare unless you've constructed unusual inputs.
Phase 3: Return the result — You add a user turn containing a tool_result block. Errors here usually mean a structural mismatch or a missing tool_use_id.
When an invalid_request_error comes back immediately after your first call, that's Phase 1. If it happens after you've sent the tool result, that's Phase 3. If there's no error but Claude keeps responding with text instead of calling a tool, that's a description or prompt design issue.
import anthropic
client = anthropic.Anthropic()
# Phase 1: define tools with correct schema
tools = [
{
"name": "classify_image",
"description": "Classify an image into one of the given categories. Call this when the user asks to categorize or classify an image.",
"input_schema": {
"type": "object",
"properties": {
"image_url": {
"type": "string",
"description": "URL of the image to classify"
},
"categories": {
"type": "array",
"items": {"type": "string"},
"description": "List of candidate categories"
}
},
"required": ["image_url", "categories"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Please classify this image: https://example.com/photo.jpg"}]
)Pattern 1: Malformed input_schema
The most common cause is a required field placed inside a property definition instead of at the schema's top level. It's an easy mistake because some validation libraries accept it, but the Claude API follows JSON Schema Draft 7 strictly.
The required field must be a top-level array of property name strings, sibling to properties. Writing "required": true inside a property definition either silently fails or causes an invalid_request_error. Another common issue is omitting type from property definitions entirely. Every property needs an explicit type, even if you're only providing a description.
# ❌ Wrong: required inside the property definition
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"required": True # this field is ignored or causes an error
}
}
}
# ✅ Correct: required as a top-level array of field name strings
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}A quick validation step before sending to the API can catch this immediately. The jsonschema package's Draft7Validator.check_schema() validates the schema structure locally without making any API calls.
Pattern 2: Incorrect tool_result structure
After Claude returns a tool_use block, the result must come back in a specific format as part of a user message. A plain text string won't work — it needs to be a tool_result block inside a content array.
The tool_use_id must exactly match the ID from Claude's tool_use block in the previous response. If you're handling parallel tool calls — where Claude calls multiple tools in one turn — each result needs its own tool_result block with the correct matching ID. Using the wrong ID, or omitting it, causes an invalid_request_error at Phase 3.
# ❌ Wrong: returning a plain text message instead of tool_result
messages.append({
"role": "user",
"content": "The classification result is: landscape"
})
# ✅ Correct: tool_result block inside a content array
if response.stop_reason == "tool_use":
tool_use_block = next(b for b in response.content if b.type == "tool_use")
result = execute_my_tool(tool_use_block.input)
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id, # copy exactly from Claude's response
"content": str(result)
}
]
})For parallel tool calls, you need to collect all results and send them together in a single user message, not as separate messages.
Pattern 3: Claude ignores the tool entirely
If there's no error but Claude keeps responding with text, the problem is almost always in the description field or the user's message.
The description is the primary signal Claude uses to decide whether to call a tool. Vague descriptions like "processes data" or "handles requests" leave Claude uncertain, and it defaults to a text response when the task seems answerable without a tool.
Write descriptions that specify the trigger condition explicitly. "Call this tool when the user asks to X" or "Use this when you need Y" gives Claude a clear decision rule. If you want to guarantee tool use regardless of the prompt, tool_choice lets you force it.
# Force at least one tool to be called
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"}, # Claude must call at least one tool
messages=[{"role": "user", "content": "Classify this image"}]
)
# Force a specific tool
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "classify_image"},
messages=[{"role": "user", "content": "Analyze this image"}]
)Using type: "any" is a good diagnostic step: if it works with forced tool use but not with type: "auto", the issue is in how Claude is interpreting the trigger conditions in your description and prompt.
Additional Schema Design Notes
A few smaller issues that tend to surface in production after the initial implementation works.
enum casing — When you use "enum" to restrict string values, Claude will pick from your list, but the case of the output string can sometimes differ from your definitions. Normalizing to lowercase on your side before processing is safer than assuming exact case matches.
Nested objects — Every level of nested objects needs its own "type": "object" declaration along with its own properties. It's repetitive, but the structure can't be abbreviated. Missing type at any nesting level causes validation failures.
Description length — Descriptions that are too long increase token usage. A two-to-three sentence description covering "what the tool does" and "when to call it" is the practical sweet spot. With multiple tools defined, long descriptions across all of them add up quickly in context costs.
additionalProperties: false — Adding this to your schema prevents Claude from including fields you didn't define. It's useful for strict output validation, but it also means if Claude tries to add any extra field, the tool call itself fails. Start without it and add it once your tool definitions are stable.
Putting It Together
The pattern I follow now when something breaks: check which phase the error is in, then go down the checklist for that phase. Phase 1 errors → look at schema structure. Phase 3 errors → look at tool_result format and ID matching. No error but no tool call → fix the description or use tool_choice to diagnose.
Once you've built this mental model, most Tool Use errors resolve in under ten minutes. The implementation itself is straightforward once you get past the initial schema learning curve.