Implement Tool Use

yes
Summary: Step-by-step guide to implementing the tool use loop: define tools, handle tool_use responses, send tool_result.

Editorial Notes

The hands-on companion to the Tool Use Overview. Walk through the complete implementation: define tool schemas with JSON Schema, handle the tool_use stop reason, execute your function, and send tool_result back. The key pattern is the agentic loop — keep calling the API until you get end_turn instead of tool_use. This is the same loop that powers Claude Code and the Agent SDK.


Original Documentation


Choosing a model#

Use the latest Claude Opus (4.6) model for complex tools and ambiguous queries; it handles multiple tools better and seeks clarification when needed.

Use Claude Haiku models for straightforward tools, but note they may infer missing parameters.

If using Claude with tool use and extended thinking, refer to the extended thinking guide for more information.

Specifying client tools#

Client tools (both Anthropic-defined and user-defined) are specified in the tools top-level parameter of the API request. Each tool definition includes:

ParameterDescription
nameThe name of the tool. Must match the regex ^[a-zA-Z0-9_-]{1,64}$.
descriptionA detailed plaintext description of what the tool does, when it should be used, and how it behaves.
input_schemaA JSON Schema object defining the expected parameters for the tool.
input_examples(Optional) An array of example input objects to help Claude understand how to use the tool. See Providing tool use examples.
Example simple tool definition
{
  "name": "get_weather",
  "description": "Get the current weather in a given location",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "The city and state, e.g. San Francisco, CA"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "The unit of temperature, either 'celsius' or 'fahrenheit'"
      }
    },
    "required": ["location"]
  }
}

This tool, named get_weather, expects an input object with a required location string and an optional unit string that must be either “celsius” or “fahrenheit”.

Tool use system prompt#

When you call the Claude API with the tools parameter, the API constructs a special system prompt from the tool definitions, tool configuration, and any user-specified system prompt. The constructed prompt is designed to instruct the model to use the specified tool(s) and provide the necessary context for the tool to operate properly:

In this environment you have access to a set of tools you can use to answer the user's question.
{{ FORMATTING INSTRUCTIONS }}
String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.
Here are the functions available in JSONSchema format:
{{ TOOL DEFINITIONS IN JSON SCHEMA }}
{{ USER SYSTEM PROMPT }}
{{ TOOL CONFIGURATION }}

Best practices for tool definitions#

To get the best performance out of Claude when using tools, follow these guidelines:

  • Provide extremely detailed descriptions. This is by far the most important factor in tool performance. Your descriptions should explain every detail about the tool, including:
    • What the tool does
    • When it should be used (and when it shouldn’t)
    • What each parameter means and how it affects the tool’s behavior
    • Any important caveats or limitations, such as what information the tool does not return if the tool name is unclear. The more context you can give Claude about your tools, the better it will be at deciding when and how to use them. Aim for at least 3-4 sentences per tool description, more if the tool is complex.
  • Prioritize descriptions, but consider using input_examples for complex tools. Clear descriptions are most important, but for tools with complex inputs, nested objects, or format-sensitive parameters, you can use the input_examples field to provide schema-validated examples. See Providing tool use examples for details.
  • Consolidate related operations into fewer tools. Rather than creating a separate tool for every action (create_pr, review_pr, merge_pr), group them into a single tool with an action parameter. Fewer, more capable tools reduce selection ambiguity and make your tool surface easier for Claude to navigate.
  • Use meaningful namespacing in tool names. When your tools span multiple services or resources, prefix names with the service (e.g., github_list_prs, slack_send_message). This makes tool selection unambiguous as your library grows, and is especially important when using tool search.
  • Design tool responses to return only high-signal information. Return semantic, stable identifiers (e.g., slugs or UUIDs) rather than opaque internal references, and include only the fields Claude needs to reason about its next step. Bloated responses waste context and make it harder for Claude to extract what matters.
Example of a good tool description
{
  "name": "get_stock_price",
  "description": "Retrieves the current stock price for a given ticker symbol. The ticker symbol must be a valid symbol for a publicly traded company on a major US stock exchange like NYSE or NASDAQ. The tool will return the latest trade price in USD. It should be used when the user asks about the current or most recent price of a specific stock. It will not provide any other information about the stock or company.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "The stock ticker symbol, e.g. AAPL for Apple Inc."
      }
    },
    "required": ["ticker"]
  }
}
Example poor tool description
{
  "name": "get_stock_price",
  "description": "Gets the stock price for a ticker.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string"
      }
    },
    "required": ["ticker"]
  }
}

The good description clearly explains what the tool does, when to use it, what data it returns, and what the ticker parameter means. The poor description is too brief and leaves Claude with many open questions about the tool’s behavior and usage.

For deeper guidance on tool design (consolidation, naming, and response shaping), see Writing tools for agents.

Providing tool use examples#

You can provide concrete examples of valid tool inputs to help Claude understand how to use your tools more effectively. This is particularly useful for complex tools with nested objects, optional parameters, or format-sensitive inputs.

Basic usage#

Add an optional input_examples field to your tool definition with an array of example input objects. Each example must be valid according to the tool’s input_schema:

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather in a given location",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "The unit of temperature",
                    },
                },
                "required": ["location"],
            },
            "input_examples": [
                {"location": "San Francisco, CA", "unit": "fahrenheit"},
                {"location": "Tokyo, Japan", "unit": "celsius"},
                {
                    "location": "New York, NY"  # 'unit' is optional
                },
            ],
        }
    ],
    messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}],
)

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather in a given location",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "The city and state, e.g. San Francisco, CA"
          },
          unit: {
            type: "string",
            enum: ["celsius", "fahrenheit"],
            description: "The unit of temperature"
          }
        },
        required: ["location"]
      },
      input_examples: [
        {
          location: "San Francisco, CA",
          unit: "fahrenheit"
        },
        {
          location: "Tokyo, Japan",
          unit: "celsius"
        },
        {
          location: "New York, NY"
          // Demonstrates that 'unit' is optional
        }
      ]
    }
  ],
  messages: [{ role: "user", content: "What's the weather like in San Francisco?" }]
});

Examples are included in the prompt alongside your tool schema, showing Claude concrete patterns for well-formed tool calls. This helps Claude understand when to include optional parameters, what formats to use, and how to structure complex inputs.

Requirements and limitations#

  • Schema validation - Each example must be valid according to the tool’s input_schema. Invalid examples return a 400 error
  • Not supported for server-side tools - Only user-defined tools can have input examples
  • Token cost - Examples add to prompt tokens: ~20-50 tokens for simple examples, ~100-200 tokens for complex nested objects

Tool runner (beta)#

The tool runner provides an out-of-the-box solution for executing tools with Claude. Instead of manually handling tool calls, tool results, and conversation management, the tool runner automatically:

  • Executes tools when Claude calls them
  • Handles the request/response cycle
  • Manages conversation state
  • Provides type safety and validation

Use the tool runner for most tool use implementations.

The tool runner is currently in beta and available in the Python, TypeScript, and Ruby SDKs.

Automatic context management with compaction

The tool runner supports automatic compaction, which generates summaries when token usage exceeds a threshold. This allows long-running agentic tasks to continue beyond context window limits.

Basic usage#

Define tools using the SDK helpers, then use the tool runner to execute them.

Use the @beta_tool decorator to define tools with type hints and docstrings.

If you’re using the async client, replace @beta_tool with @beta_async_tool and define the function with async def.

import anthropic
import json
from anthropic import beta_tool

# Initialize client
client = anthropic.Anthropic()


# Define tools using the decorator
@beta_tool
def get_weather(location: str, unit: str = "fahrenheit") -> str:
    """Get the current weather in a given location.

    Args:
        location: The city and state, e.g. San Francisco, CA
        unit: Temperature unit, either 'celsius' or 'fahrenheit'
    """
    # In a full implementation, you'd call a weather API here
    return json.dumps({"temperature": "20°C", "condition": "Sunny"})


@beta_tool
def calculate_sum(a: int, b: int) -> str:
    """Add two numbers together.

    Args:
        a: First number
        b: Second number
    """
    return str(a + b)


# Use the tool runner
runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
for message in runner:
    print(message.content[0].text)

The @beta_tool decorator inspects the function arguments and docstring to extract a JSON schema representation. For example, calculate_sum becomes:

{
  "name": "calculate_sum",
  "description": "Adds two integers together.",
  "input_schema": {
    "additionalProperties": false,
    "properties": {
      "left": {
        "description": "The first integer to add.",
        "title": "Left",
        "type": "integer"
      },
      "right": {
        "description": "The second integer to add.",
        "title": "Right",
        "type": "integer"
      }
    },
    "required": ["left", "right"],
    "type": "object"
  }
}

Use betaZodTool() for type-safe tool definitions with Zod validation, or betaTool() for JSON Schema-based definitions.

TypeScript offers two approaches for defining tools:

Using Zod (recommended) - Use betaZodTool() for type-safe tool definitions with Zod validation (requires Zod 3.25.0 or higher):




const anthropic = new Anthropic();

const getWeatherTool = betaZodTool({
  name: "get_weather",
  description: "Get the current weather in a given location",
  inputSchema: z.object({
    location: z.string().describe("The city and state, e.g. San Francisco, CA"),
    unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit").describe("Temperature unit")
  }),
  run: async (input) => {
    // In a full implementation, you'd call a weather API here
    return JSON.stringify({ temperature: "20°C", condition: "Sunny" });
  }
});

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "What's the weather like in Paris?" }]
});

for await (const message of runner) {
  console.log(message.content[0].text);
}

Using JSON Schema - Use betaTool() for type-safe tool definitions without Zod:

The input generated by Claude will not be validated at runtime. Perform validation inside the run function if needed.



const anthropic = new Anthropic();

const calculateSumTool = betaTool({
  name: "calculate_sum",
  description: "Add two numbers together",
  inputSchema: {
    type: "object",
    properties: {
      a: { type: "number", description: "First number" },
      b: { type: "number", description: "Second number" }
    },
    required: ["a", "b"]
  },
  run: async (input) => {
    return String(input.a + input.b);
  }
});

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [calculateSumTool],
  messages: [{ role: "user", content: "What's 15 + 27?" }]
});

for await (const message of runner) {
  console.log(message.content[0].text);
}

Use the Anthropic::BaseTool class to define tools with typed input schemas.

require "anthropic"

# Initialize client
client = Anthropic::Client.new

# Define input schema
class GetWeatherInput < Anthropic::BaseModel
  required :location, String, doc: "The city and state, e.g. San Francisco, CA"
  optional :unit, Anthropic::InputSchema::EnumOf["celsius", "fahrenheit"],
           doc: "Temperature unit"
end

# Define tool
class GetWeather < Anthropic::BaseTool
  doc "Get the current weather in a given location"
  input_schema GetWeatherInput

  def call(input)
    # In a full implementation, you'd call a weather API here
    JSON.generate({temperature: "20°C", condition: "Sunny"})
  end
end

class CalculateSumInput < Anthropic::BaseModel
  required :a, Integer, doc: "First number"
  required :b, Integer, doc: "Second number"
end

class CalculateSum < Anthropic::BaseTool
  doc "Add two numbers together"
  input_schema CalculateSumInput

  def call(input)
    (input.a + input.b).to_s
  end
end

# Use the tool runner
runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [GetWeather.new, CalculateSum.new],
  messages: [
    {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}
  ]
)

runner.each_message do |message|
  message.content.each do |block|
    puts block.text if block.respond_to?(:text)
  end
end

The Anthropic::BaseTool class uses the doc method for the tool description and input_schema to define the expected parameters. The SDK automatically converts this to the appropriate JSON schema format.

The tool function must return a content block or content block array, including text, images, or document blocks. This allows tools to return rich, multimodal responses. Returned strings will be converted to a text content block. If you want to return a structured JSON object to Claude, encode it to a JSON string before returning it. Numbers, booleans, or other non-string primitives must also be converted to strings.

Iterating over the tool runner#

The tool runner is an iterable that yields messages from Claude. This is often referred to as a “tool call loop”. Each iteration, the runner checks if Claude requested a tool use. If so, it calls the tool and sends the result back to Claude automatically, then yields the next message from Claude to continue your loop.

You can end the loop at any iteration with a break statement. The runner will loop until Claude returns a message without a tool use.

If you don’t need intermediate messages, you can get the final message directly:

Use runner.until_done() to get the final message.

runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
final_message = runner.until_done()
print(final_message.content[0].text)

Simply await the runner to get the final message.

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "What's the weather like in Paris?" }]
});

const finalMessage = await runner;
console.log(finalMessage.content[0].text);

Use runner.run_until_finished to get all messages.

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [GetWeather.new, CalculateSum.new],
  messages: [
    {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}
  ]
)

all_messages = runner.run_until_finished
all_messages.each { |msg| puts msg.content }

Advanced usage#

Within the loop, you can fully customize the tool runner’s next request to the Messages API. The runner automatically appends tool results to the message history, so you don’t need to manually manage them. You can optionally inspect the tool result for logging or debugging, and modify the request parameters before the next API call.

Use generate_tool_call_response() to optionally inspect the tool result (the runner appends it automatically). Use set_messages_params() and append_messages() to modify the request.

runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],
)
for message in runner:
    # Optional: inspect the tool response (automatically appended by the runner)
    tool_response = runner.generate_tool_call_response()
    if tool_response:
        print(f"Tool result: {tool_response}")

    # Customize the next request
    runner.set_messages_params(
        lambda params: {
            **params,
            "max_tokens": 2048,  # Increase tokens for next request
        }
    )

    # Or add additional messages
    runner.append_messages(
        {"role": "user", "content": "Please be concise in your response."}
    )

Use generateToolResponse() to optionally inspect the tool result (the runner appends it automatically). Use setMessagesParams() and pushMessages() to modify the request.

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "What's the weather in San Francisco?" }]
});

for await (const message of runner) {
  // Optional: inspect the tool result message (automatically appended by the runner)
  const toolResultMessage = await runner.generateToolResponse();
  if (toolResultMessage) {
    console.log("Tool result:", toolResultMessage);
  }

  // Customize the next request
  runner.setMessagesParams((params) => ({
    ...params,
    max_tokens: 2048 // Increase tokens for next request
  }));

  // Or add additional messages
  runner.pushMessages({ role: "user", content: "Please be concise in your response." });
}

Use next_message for step-by-step control. Use feed_messages to inject messages and params to access parameters.

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [GetWeather.new],
  messages: [{role: "user", content: "What's the weather in San Francisco?"}]
)

# Manual step-by-step control
message = runner.next_message
puts message.content

# Inject follow-up messages
runner.feed_messages([
  {role: "user", content: "Also check Boston"}
])

# Access current parameters
puts runner.params

Debugging tool execution#

When a tool throws an exception, the tool runner catches it and returns the error to Claude as a tool result with is_error: true. By default, only the exception message is included, not the full stack trace.

To view full stack traces and debug information, set the ANTHROPIC_LOG environment variable:

# View info-level logs including tool errors
export ANTHROPIC_LOG=info

# View debug-level logs for more verbose output
export ANTHROPIC_LOG=debug

When enabled, the SDK logs full exception details (using Python’s logging module, the console in TypeScript, or Ruby’s logger), including the complete stack trace when a tool fails.

Intercepting tool errors#

By default, tool errors are passed back to Claude, which can then respond appropriately. However, you may want to detect errors and handle them differently, for example, to stop execution early or implement custom error handling.

Use the tool response method to intercept tool results and check for errors before they’re sent to Claude:

import json

runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[my_tool],
    messages=[{"role": "user", "content": "Run the tool"}],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response:
        # Check if any tool result has an error
        for block in tool_response.content:
            if block.is_error:
                # Option 1: Raise an exception to stop the loop
                raise RuntimeError(f"Tool failed: {json.dumps(block.content)}")

                # Option 2: Log and continue (let Claude handle it)
                # logger.error(f"Tool error: {json.dumps(block.content)}")

    # Process the message normally
    print(message.content)

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [myTool],
  messages: [{ role: "user", content: "Run the tool" }]
});

for await (const message of runner) {
  const toolResultMessage = await runner.generateToolResponse();

  if (toolResultMessage) {
    // Check if any tool result has an error
    for (const block of toolResultMessage.content) {
      if (block.type === "tool_result" && block.is_error) {
        // Option 1: Throw to stop the loop
        throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);

        // Option 2: Log and continue (let Claude handle it)
        // console.error(`Tool error: ${JSON.stringify(block.content)}`);
      }
    }
  }

  // Process the message normally
  console.log(message.content);
}

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [MyTool.new],
  messages: [{role: "user", content: "Run the tool"}]
)

runner.each_message do |message|
  # Get the tool response to check for errors
  # Note: The runner automatically handles tool execution and appends results
  # This is just for error checking/logging purposes
  tool_results = runner.params[:messages].last

  if tool_results && tool_results[:role] == "user"
    tool_results[:content].each do |block|
      if block[:type] == "tool_result" && block[:is_error]
        # Option 1: Raise an exception to stop the loop
        raise "Tool failed: #{block[:content]}"

        # Option 2: Log and continue (let Claude handle it)
        # logger.error("Tool error: #{block[:content]}")
      end
    end
  end

  puts message.content
end

Modifying tool results#

You can modify tool results before they’re sent back to Claude. This is useful for adding metadata like cache_control to enable prompt caching on tool results, or for transforming the tool output.

Use the tool response method to get the tool result, modify it, then add your modified version to the messages:

runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[search_documents],
    messages=[
        {
            "role": "user",
            "content": "Search for information about the climate of San Francisco",
        }
    ],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response:
        # Modify the tool result to add cache control
        for block in tool_response.content:
            if block.type == "tool_result":
                # Add cache_control to cache this tool result
                block.cache_control = {"type": "ephemeral"}

        # Append the modified response (this prevents auto-append of original)
        runner.append_messages(message, tool_response)

    print(message.content)

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [searchDocuments],
  messages: [
    { role: "user", content: "Search for information about the climate of San Francisco" }
  ]
});

for await (const message of runner) {
  const toolResultMessage = await runner.generateToolResponse();

  if (toolResultMessage) {
    // Modify the tool result to add cache control
    for (const block of toolResultMessage.content) {
      if (block.type === "tool_result") {
        // Add cache_control to cache this tool result
        block.cache_control = { type: "ephemeral" };
      }
    }

    // Push the modified message (this prevents auto-append of original)
    runner.pushMessages(message, toolResultMessage);
  }

  console.log(message.content);
}

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [SearchDocuments.new],
  messages: [{role: "user", content: "Search for information about the climate of San Francisco"}]
)

loop do
  message = runner.next_message
  break unless message

  # Access the most recent tool results from the messages array
  # The runner automatically adds tool results, but we can modify them
  tool_results_message = runner.params[:messages].last

  if tool_results_message && tool_results_message[:role] == "user"
    tool_results_message[:content].each do |block|
      if block[:type] == "tool_result"
        # Modify the tool result to add cache control
        block[:cache_control] = {type: "ephemeral"}
      end
    end
  end

  puts message.content
  break if message.stop_reason != "tool_use"
end

Adding cache_control to tool results is particularly useful when tools return large amounts of data (like document search results) that you want to cache for subsequent API calls. See Prompt caching for more details on caching strategies.

Streaming#

Enable streaming to receive events as they arrive. Each iteration yields a stream object that you can iterate for events.

Set stream=True and use get_final_message() to get the accumulated message.

runner = client.beta.messages.tool_runner(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=[calculate_sum],
    messages=[{"role": "user", "content": "What is 15 + 27?"}],
    stream=True,
)

# When streaming, the runner returns BetaMessageStream
for message_stream in runner:
    for event in message_stream:
        print("event:", event)
    print("message:", message_stream.get_final_message())

print(runner.until_done())

Set stream: true and use finalMessage() to get the accumulated message.

const runner = anthropic.beta.messages.toolRunner({
  model: "claude-opus-4-6",
  max_tokens: 1000,
  messages: [{ role: "user", content: "What is the weather in San Francisco?" }],
  tools: [getWeatherTool],
  stream: true
});

// When streaming, the runner returns BetaMessageStream
for await (const messageStream of runner) {
  for await (const event of messageStream) {
    console.log("event:", event);
  }
  console.log("message:", await messageStream.finalMessage());
}

console.log(await runner);

Use each_streaming to iterate over streaming events.

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: [CalculateSum.new],
  messages: [{role: "user", content: "What is 15 + 27?"}]
)

runner.each_streaming do |event|
  case event
  when Anthropic::Streaming::TextEvent
    print event.text
  when Anthropic::Streaming::ToolUseEvent
    puts "\nTool called: #{event.tool_name}"
  end
end

The SDK tool runner is in beta. The rest of this document covers manual tool implementation.

Controlling Claude’s output#

Forcing tool use#

In some cases, you may want Claude to use a specific tool to answer the user’s question, even if Claude thinks it can provide an answer without using a tool. You can do this by specifying the tool in the tool_choice field like so:

tool_choice = {"type": "tool", "name": "get_weather"}

When working with the tool_choice parameter, there are four possible options:

  • auto allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
  • any tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
  • tool forces Claude to always use a particular tool.
  • none prevents Claude from using any tools. This is the default value when no tools are provided.

When using prompt caching, changes to the tool_choice parameter will invalidate cached message blocks. Tool definitions and system prompts remain cached, but message content must be reprocessed.

This diagram illustrates how each option works:

Image

Note that when you have tool_choice as any or tool, the API prefills the assistant message to force a tool to be used. This means that the models will not emit a natural language response or explanation before tool_use content blocks, even if explicitly asked to do so.

When using extended thinking with tool use, tool_choice: {"type": "any"} and tool_choice: {"type": "tool", "name": "..."} are not supported and will result in an error. Only tool_choice: {"type": "auto"} (the default) and tool_choice: {"type": "none"} are compatible with extended thinking.

Testing has shown that this should not reduce performance. If you would like the model to provide natural language context or explanations while still requesting that the model use a specific tool, you can use {"type": "auto"} for tool_choice (the default) and add explicit instructions in a user message. For example: What's the weather like in London? Use the get_weather tool in your response.

Guaranteed tool calls with strict tools

Combine tool_choice: {"type": "any"} with strict tool use to guarantee both that one of your tools will be called AND that the tool inputs strictly follow your schema. Set strict: true on your tool definitions to enable schema validation.

JSON output#

Tools do not necessarily need to be client functions. You can use tools anytime you want the model to return JSON output that follows a provided schema. For example, you might use a record_summary tool with a particular schema. See Tool use with Claude for a full working example.

Model responses with tools#

When using tools, Claude will often comment on what it’s doing or respond naturally to the user before invoking tools.

For example, given the prompt “What’s the weather like in San Francisco right now, and what time is it there?”, Claude might respond with:

{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll help you check the current weather and time in San Francisco."
    },
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    }
  ]
}

This natural response style helps users understand what Claude is doing and creates a more conversational interaction. You can guide the style and content of these responses through your system prompts and by providing <examples> in your prompts.

It’s important to note that Claude may use various phrasings and approaches when explaining its actions. Your code should treat these responses like any other assistant-generated text, and not rely on specific formatting conventions.

Parallel tool use#

By default, Claude may use multiple tools to answer a user query. You can disable this behavior by:

  • Setting disable_parallel_tool_use=true when tool_choice type is auto, which ensures that Claude uses at most one tool
  • Setting disable_parallel_tool_use=true when tool_choice type is any or tool, which ensures that Claude uses exactly one tool
Complete parallel tool use example

Simpler with Tool runner: The example below shows manual parallel tool handling. For most use cases, tool runner automatically handle parallel tool execution with much less code.

Here’s a complete example showing how to properly format parallel tool calls in the message history:

import anthropic

client = anthropic.Anthropic()

# Define tools
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
        },
    },
    {
        "name": "get_time",
        "description": "Get the current time in a given timezone",
        "input_schema": {
            "type": "object",
            "properties": {
                "timezone": {
                    "type": "string",
                    "description": "The timezone, e.g. America/New_York",
                }
            },
            "required": ["timezone"],
        },
    },
]

# Initial request
response = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[
        {
            "role": "user",
            "content": "What's the weather in SF and NYC, and what time is it there?",
        }
    ],
)

# Claude's response with parallel tool calls
print("Claude wants to use tools:", response.stop_reason == "tool_use")
print(
    "Number of tool calls:", len([c for c in response.content if c.type == "tool_use"])
)

# Build the conversation with tool results
messages = [
    {
        "role": "user",
        "content": "What's the weather in SF and NYC, and what time is it there?",
    },
    {
        "role": "assistant",
        "content": response.content,  # Contains multiple tool_use blocks
    },
    {
        "role": "user",
        "content": [
            {
                "type": "tool_result",
                "tool_use_id": "toolu_01",  # Must match the ID from tool_use
                "content": "San Francisco: 68°F, partly cloudy",
            },
            {
                "type": "tool_result",
                "tool_use_id": "toolu_02",
                "content": "New York: 45°F, clear skies",
            },
            {
                "type": "tool_result",
                "tool_use_id": "toolu_03",
                "content": "San Francisco time: 2:30 PM PST",
            },
            {
                "type": "tool_result",
                "tool_use_id": "toolu_04",
                "content": "New York time: 5:30 PM EST",
            },
        ],
    },
]

# Get final response
final_response = client.messages.create(
    model="claude-opus-4-6", max_tokens=1024, tools=tools, messages=messages
)

print(final_response.content[0].text)

const anthropic = new Anthropic();

// Define tools
const tools = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  },
  {
    name: "get_time",
    description: "Get the current time in a given timezone",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "The timezone, e.g. America/New_York"
        }
      },
      required: ["timezone"]
    }
  }
];

// Initial request
const response = await anthropic.messages.create({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: tools,
  messages: [
    {
      role: "user",
      content: "What's the weather in SF and NYC, and what time is it there?"
    }
  ]
});

// Build conversation with tool results
const messages = [
  {
    role: "user",
    content: "What's the weather in SF and NYC, and what time is it there?"
  },
  {
    role: "assistant",
    content: response.content // Contains multiple tool_use blocks
  },
  {
    role: "user",
    content: [
      {
        type: "tool_result",
        tool_use_id: "toolu_01", // Must match the ID from tool_use
        content: "San Francisco: 68°F, partly cloudy"
      },
      {
        type: "tool_result",
        tool_use_id: "toolu_02",
        content: "New York: 45°F, clear skies"
      },
      {
        type: "tool_result",
        tool_use_id: "toolu_03",
        content: "San Francisco time: 2:30 PM PST"
      },
      {
        type: "tool_result",
        tool_use_id: "toolu_04",
        content: "New York time: 5:30 PM EST"
      }
    ]
  }
];

// Get final response
const finalResponse = await anthropic.messages.create({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  tools: tools,
  messages: messages
});

console.log(finalResponse.content[0].text);

The assistant message with parallel tool calls would look like this:

{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll check the weather and time for both San Francisco and New York City."
    },
    {
      "type": "tool_use",
      "id": "toolu_01",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    },
    {
      "type": "tool_use",
      "id": "toolu_02",
      "name": "get_weather",
      "input": { "location": "New York, NY" }
    },
    {
      "type": "tool_use",
      "id": "toolu_03",
      "name": "get_time",
      "input": { "timezone": "America/Los_Angeles" }
    },
    {
      "type": "tool_use",
      "id": "toolu_04",
      "name": "get_time",
      "input": { "timezone": "America/New_York" }
    }
  ]
}
Complete test script for parallel tools

Here’s a complete, runnable script to test and verify parallel tool calls are working correctly:

#!/usr/bin/env python3
"""Test script to verify parallel tool calls with the Claude API"""

import os
from anthropic import Anthropic

# Initialize client
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

# Define tools
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
        },
    },
    {
        "name": "get_time",
        "description": "Get the current time in a given timezone",
        "input_schema": {
            "type": "object",
            "properties": {
                "timezone": {
                    "type": "string",
                    "description": "The timezone, e.g. America/New_York",
                }
            },
            "required": ["timezone"],
        },
    },
]

# Test conversation with parallel tool calls
messages = [
    {
        "role": "user",
        "content": "What's the weather in SF and NYC, and what time is it there?",
    }
]

# Make initial request
print("Requesting parallel tool calls...")
response = client.messages.create(
    model="claude-opus-4-6", max_tokens=1024, messages=messages, tools=tools
)

# Check for parallel tool calls
tool_uses = [block for block in response.content if block.type == "tool_use"]
print(f"\n✓ Claude made {len(tool_uses)} tool calls")

if len(tool_uses) > 1:
    print("✓ Parallel tool calls detected!")
    for tool in tool_uses:
        print(f"  - {tool.name}: {tool.input}")
else:
    print("✗ No parallel tool calls detected")

# Simulate tool execution and format results correctly
tool_results = []
for tool_use in tool_uses:
    if tool_use.name == "get_weather":
        if "San Francisco" in str(tool_use.input):
            result = "San Francisco: 68°F, partly cloudy"
        else:
            result = "New York: 45°F, clear skies"
    else:  # get_time
        if "Los_Angeles" in str(tool_use.input):
            result = "2:30 PM PST"
        else:
            result = "5:30 PM EST"

    tool_results.append(
        {"type": "tool_result", "tool_use_id": tool_use.id, "content": result}
    )

# Continue conversation with tool results
messages.extend(
    [
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": tool_results},  # All results in one message!
    ]
)

# Get final response
print("\nGetting final response...")
final_response = client.messages.create(
    model="claude-opus-4-6", max_tokens=1024, messages=messages, tools=tools
)

print(f"\nClaude's response:\n{final_response.content[0].text}")

# Verify formatting
print("\n--- Verification ---")
print(f"✓ Tool results sent in single user message: {len(tool_results)} results")
print("✓ No text before tool results in content array")
print("✓ Conversation formatted correctly for future parallel tool use")
#!/usr/bin/env node
// Test script to verify parallel tool calls with the Claude API


const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

// Define tools
const tools = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  },
  {
    name: "get_time",
    description: "Get the current time in a given timezone",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "The timezone, e.g. America/New_York"
        }
      },
      required: ["timezone"]
    }
  }
];

async function testParallelTools() {
  // Make initial request
  console.log("Requesting parallel tool calls...");
  const response = await anthropic.messages.create({
    model: "claude-opus-4-6",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: "What's the weather in SF and NYC, and what time is it there?"
      }
    ],
    tools: tools
  });

  // Check for parallel tool calls
  const toolUses = response.content.filter((block) => block.type === "tool_use");
  console.log(`\n✓ Claude made ${toolUses.length} tool calls`);

  if (toolUses.length > 1) {
    console.log("✓ Parallel tool calls detected!");
    toolUses.forEach((tool) => {
      console.log(`  - ${tool.name}: ${JSON.stringify(tool.input)}`);
    });
  } else {
    console.log("✗ No parallel tool calls detected");
  }

  // Simulate tool execution and format results correctly
  const toolResults = toolUses.map((toolUse) => {
    let result;
    if (toolUse.name === "get_weather") {
      result = toolUse.input.location.includes("San Francisco")
        ? "San Francisco: 68°F, partly cloudy"
        : "New York: 45°F, clear skies";
    } else {
      result = toolUse.input.timezone.includes("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST";
    }

    return {
      type: "tool_result",
      tool_use_id: toolUse.id,
      content: result
    };
  });

  // Get final response with correct formatting
  console.log("\nGetting final response...");
  const finalResponse = await anthropic.messages.create({
    model: "claude-opus-4-6",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: "What's the weather in SF and NYC, and what time is it there?"
      },
      { role: "assistant", content: response.content },
      { role: "user", content: toolResults } // All results in one message!
    ],
    tools: tools
  });

  console.log(`\nClaude's response:\n${finalResponse.content[0].text}`);

  // Verify formatting
  console.log("\n--- Verification ---");
  console.log(`✓ Tool results sent in single user message: ${toolResults.length} results`);
  console.log("✓ No text before tool results in content array");
  console.log("✓ Conversation formatted correctly for future parallel tool use");
}

testParallelTools().catch(console.error);

This script demonstrates:

  • How to properly format parallel tool calls and results
  • How to verify that parallel calls are being made
  • The correct message structure that encourages future parallel tool use
  • Common mistakes to avoid (like text before tool results)

Run this script to test your implementation and ensure Claude is making parallel tool calls effectively.

Maximizing parallel tool use#

While Claude 4 models have excellent parallel tool use capabilities by default, you can increase the likelihood of parallel tool execution across all models with targeted prompting:

System prompts for parallel tool use

For Claude 4 models (Opus 4, and Sonnet 4), add this to your system prompt:

For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.

For even stronger parallel tool use (recommended if the default isn’t sufficient), use:

<use_parallel_tool_calls>
For maximum efficiency, whenever you perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like `ls` or `list_dir`, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially.
</use_parallel_tool_calls>
User message prompting

You can also encourage parallel tool use within specific user messages:

# Instead of:
"What's the weather in Paris? Also check London."

# Use:
"Check the weather in Paris and London simultaneously."

# Or be explicit:
"Please use parallel tool calls to get the weather for Paris, London, and Tokyo at the same time."

Parallel tool use with Claude Sonnet 3.7

Claude Sonnet 3.7 may be less likely to make parallel tool calls in a response, even when you have not set disable_parallel_tool_use. Upgrade to Claude 4 models, which have built-in token-efficient tool use and improved parallel tool calling.

If you’re still using Claude Sonnet 3.7, you can enable the token-efficient-tools-2025-02-19 beta header, which helps encourage Claude to use parallel tools. You can also introduce a “batch tool” that can act as a meta-tool to wrap invocations to other tools simultaneously.

See this example in the cookbook for how to use this workaround.

Handling tool use and tool result content blocks#

Simpler with Tool runner: The manual tool handling described in this section is automatically managed by tool runner. Use this section when you need custom control over tool execution.

Claude’s response differs based on whether it uses a client or server tool.

Handling results from client tools#

The response will have a stop_reason of tool_use and one or more tool_use content blocks that include:

  • id: A unique identifier for this particular tool use block. This will be used to match up the tool results later.
  • name: The name of the tool being used.
  • input: An object containing the input being passed to the tool, conforming to the tool’s input_schema.
Example API response with a `tool_use` content block
{
  "id": "msg_01Aq9w938a90dw8q",
  "model": "claude-opus-4-6",
  "stop_reason": "tool_use",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll check the current weather in San Francisco for you."
    },
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA", "unit": "celsius" }
    }
  ]
}

When you receive a tool use response for a client tool, you should:

  1. Extract the name, id, and input from the tool_use block.
  2. Run the actual tool in your codebase corresponding to that tool name, passing in the tool input.
  3. Continue the conversation by sending a new message with the role of user, and a content block containing the tool_result type and the following information:
    • tool_use_id: The id of the tool use request this is a result for.
    • content: The result of the tool, as a string (for example, "content": "15 degrees"), a list of nested content blocks (for example, "content": [{"type": "text", "text": "15 degrees"}]), or a list of document blocks (for example, "content": ["type": "document", "source": {"type": "text", "media_type": "text/plain", "data": "15 degrees"}]). These content blocks can use the text, image, or document types.
    • is_error (optional): Set to true if the tool execution resulted in an error.

Important formatting requirements:

  • Tool result blocks must immediately follow their corresponding tool use blocks in the message history. You cannot include any messages between the assistant’s tool use message and the user’s tool result message.
  • In the user message containing tool results, the tool_result blocks must come FIRST in the content array. Any text must come AFTER all tool results.

For example, this will cause a 400 error:

{
  "role": "user",
  "content": [
    { "type": "text", "text": "Here are the results:" }, // ❌ Text before tool_result
    { "type": "tool_result", "tool_use_id": "toolu_01" /* ... */ }
  ]
}

This is correct:

{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_01" /* ... */ },
    { "type": "text", "text": "What should I do next?" } // ✅ Text after tool_result
  ]
}

If you receive an error like “tool_use ids were found without tool_result blocks immediately after”, check that your tool results are formatted correctly.

Example of successful tool result
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "15 degrees"
    }
  ]
}
Example of tool result with images
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": [
        { "type": "text", "text": "15 degrees" },
        {
          "type": "image",
          "source": {
            "type": "base64",
            "media_type": "image/jpeg",
            "data": "/9j/4AAQSkZJRg..."
          }
        }
      ]
    }
  ]
}
Example of empty tool result
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9"
    }
  ]
}
Example of tool result with documents
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": [
        { "type": "text", "text": "The weather is" },
        {
          "type": "document",
          "source": {
            "type": "text",
            "media_type": "text/plain",
            "data": "15 degrees"
          }
        }
      ]
    }
  ]
}

After receiving the tool result, Claude will use that information to continue generating a response to the original user prompt.

Handling results from server tools#

Claude executes the tool internally and incorporates the results directly into its response without requiring additional user interaction.

Differences from other APIs

Unlike APIs that separate tool use or use special roles like tool or function, the Claude API integrates tools directly into the user and assistant message structure.

Messages contain arrays of text, image, tool_use, and tool_result blocks. user messages include client content and tool_result, while assistant messages contain AI-generated content and tool_use.

Handling the max_tokens stop reason#

If Claude’s response is cut off due to hitting the max_tokens limit, and the truncated response contains an incomplete tool use block, you’ll need to retry the request with a higher max_tokens value to get the full tool use.

# Check if response was truncated during tool use
if response.stop_reason == "max_tokens":
    # Check if the last content block is an incomplete tool_use
    last_block = response.content[-1]
    if last_block.type == "tool_use":
        # Send the request with higher max_tokens
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=4096,  # Increased limit
            messages=messages,
            tools=tools,
        )
// Check if response was truncated during tool use
if (response.stop_reason === "max_tokens") {
  // Check if the last content block is an incomplete tool_use
  const lastBlock = response.content[response.content.length - 1];
  if (lastBlock.type === "tool_use") {
    // Send the request with higher max_tokens
    response = await anthropic.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 4096, // Increased limit
      messages: messages,
      tools: tools
    });
  }
}

Handling the pause_turn stop reason#

When using server tools like web search, the API may return a pause_turn stop reason, indicating that the API has paused a long-running turn.

Here’s how to handle the pause_turn stop reason:

import anthropic

client = anthropic.Anthropic()

# Initial request with web search
response = client.messages.create(
    model="claude-3-7-sonnet-latest",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Search for comprehensive information about quantum computing breakthroughs in 2025",
        }
    ],
    tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
)

# Check if the response has pause_turn stop reason
if response.stop_reason == "pause_turn":
    # Continue the conversation with the paused content
    messages = [
        {
            "role": "user",
            "content": "Search for comprehensive information about quantum computing breakthroughs in 2025",
        },
        {"role": "assistant", "content": response.content},
    ]

    # Send the continuation request
    continuation = client.messages.create(
        model="claude-3-7-sonnet-latest",
        max_tokens=1024,
        messages=messages,
        tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
    )

    print(continuation)
else:
    print(response)

const anthropic = new Anthropic();

// Initial request with web search
const response = await anthropic.messages.create({
  model: "claude-3-7-sonnet-latest",
  max_tokens: 1024,
  messages: [
    {
      role: "user",
      content:
        "Search for comprehensive information about quantum computing breakthroughs in 2025"
    }
  ],
  tools: [
    {
      type: "web_search_20250305",
      name: "web_search",
      max_uses: 10
    }
  ]
});

// Check if the response has pause_turn stop reason
if (response.stop_reason === "pause_turn") {
  // Continue the conversation with the paused content
  const messages = [
    {
      role: "user",
      content:
        "Search for comprehensive information about quantum computing breakthroughs in 2025"
    },
    { role: "assistant", content: response.content }
  ];

  // Send the continuation request
  const continuation = await anthropic.messages.create({
    model: "claude-3-7-sonnet-latest",
    max_tokens: 1024,
    messages: messages,
    tools: [
      {
        type: "web_search_20250305",
        name: "web_search",
        max_uses: 10
      }
    ]
  });

  console.log(continuation);
} else {
  console.log(response);
}

When handling pause_turn:

  • Continue the conversation: Pass the paused response back as-is in a subsequent request to let Claude continue its turn
  • Modify if needed: You can optionally modify the content before continuing if you want to interrupt or redirect the conversation
  • Preserve tool state: Include the same tools in the continuation request to maintain functionality

Troubleshooting errors#

Built-in Error Handling: Tool runner provide automatic error handling for most common scenarios. This section covers manual error handling for advanced use cases.

There are a few different types of errors that can occur when using tools with Claude:

Tool execution error

If the tool itself throws an error during execution (for example, a network error when fetching weather data), you can return the error message in the content along with "is_error": true:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "ConnectionError: the weather service API is not available (HTTP 500)",
      "is_error": true
    }
  ]
}

Claude will then incorporate this error into its response to the user. For example: “I’m sorry, I was unable to retrieve the current weather because the weather service API is not available. Please try again later.”

Write instructive error messages. Instead of generic errors like "failed", include what went wrong and what Claude should try next, e.g., "Rate limit exceeded. Retry after 60 seconds." This gives Claude the context it needs to recover or adapt without guessing.

Invalid tool name

If Claude’s attempted use of a tool is invalid (for example, missing required parameters), it usually means that there wasn’t enough information for Claude to use the tool correctly. Your best bet during development is to try the request again with more-detailed description values in your tool definitions.

However, you can also continue the conversation forward with a tool_result that indicates the error, and Claude will try to use the tool again with the missing information filled in:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "Error: Missing required 'location' parameter",
      "is_error": true
    }
  ]
}

If a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user.

To eliminate invalid tool calls entirely, use strict tool use with strict: true on your tool definitions. This guarantees that tool inputs will always match your schema exactly, preventing missing parameters and type mismatches.

tags

To prevent Claude from reflecting on search quality with <search_quality_reflection> tags, add “Do not reflect on the quality of the returned search results in your response” to your prompt.

Server tool errors

When server tools encounter errors (for example, network issues with Web Search), Claude will transparently handle these errors and attempt to provide an alternative response or explanation to the user. Unlike client tools, you do not need to handle is_error results for server tools.

For web search specifically, possible error codes include:

  • too_many_requests: Rate limit exceeded
  • invalid_input: Invalid search query parameter
  • max_uses_exceeded: Maximum web search tool uses exceeded
  • query_too_long: Query exceeds maximum length
  • unavailable: An internal error occurred
Parallel tool calls not working

If Claude isn’t making parallel tool calls when expected, check these common issues:

1. Incorrect tool result formatting

The most common issue is formatting tool results incorrectly in the conversation history. This “teaches” Claude to avoid parallel calls.

Specifically for parallel tool use:

  • Wrong: Sending separate user messages for each tool result
  • Correct: All tool results must be in a single user message
// ❌ This reduces parallel tool use
[
  {"role": "assistant", "content": [tool_use_1, tool_use_2]},
  {"role": "user", "content": [tool_result_1]},
  {"role": "user", "content": [tool_result_2]}  // Separate message
]

// ✅ This maintains parallel tool use
[
  {"role": "assistant", "content": [tool_use_1, tool_use_2]},
  {"role": "user", "content": [tool_result_1, tool_result_2]}  // Single message
]

See the general formatting requirements above for other formatting rules.

2. Weak prompting

Default prompting may not be sufficient. Use stronger language:

<use_parallel_tool_calls>
For maximum efficiency, whenever you perform multiple independent operations,
invoke all relevant tools simultaneously rather than sequentially.
Prioritize calling tools in parallel whenever possible.
</use_parallel_tool_calls>

3. Measuring parallel tool usage

To verify parallel tool calls are working:

# Calculate average tools per tool-calling message
tool_call_messages = [
    msg for msg in messages if any(block.type == "tool_use" for block in msg.content)
]
total_tool_calls = sum(
    len([b for b in msg.content if b.type == "tool_use"]) for msg in tool_call_messages
)
avg_tools_per_message = total_tool_calls / len(tool_call_messages)
print(f"Average tools per message: {avg_tools_per_message}")
# Should be > 1.0 if parallel calls are working

4. Model-specific behavior

  • Claude Opus 4.6, Sonnet 4.6, Sonnet 4.5, Opus 4.5, Opus 4.1, and Sonnet 4: Excel at parallel tool use with minimal prompting
  • Claude Sonnet 3.7: May need stronger prompting or the token-efficient-tools-2025-02-19 beta header. Consider upgrading to Claude 4.
  • Claude Haiku: Less likely to use parallel tools without explicit prompting
Link last verified June 7, 2026. View original ↗
Source: Anthropic Platform Docs

Appears in Learning Paths

Link last verified: 2026-02-26