Return Tool Results

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 87 of 200

What a Tool Result Is

After your code runs a tool (Lesson 86), Claude needs to know what happened. You tell it by sending a tool_result content block in the next user message:

{
    "type": "tool_result",
    "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
    "content": "31°C, humid, light rain expected at 5 PM",
}
FieldRequiredMeaning
typeYesAlways "tool_result"
tool_use_idYesThe id of the tool_use block this result answers
contentUsuallyThe result: a string, or a list of content blocks
is_errorOptionalTrue when the tool failed; content then explains the problem

The tool_use_id is what connects a result to its call. Claude may have requested several tools, and each result must say which call it belongs to.


The Message Structure

A complete tool round trip adds two messages to the conversation:

messages = [
    {"role": "user", "content": "Is it a good day for a walk in Chennai?"},

    # 1. Claude's response, exactly as received (may include thinking, text, and tool_use blocks)
    {"role": "assistant", "content": response.content},

    # 2. A user message containing the tool results
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "toolu_01A0...", "content": "31°C, humid, light rain at 5 PM"},
    ]},
]

Then you send the whole list in the next request, with the same tools (Lesson 88).


Formatting Rules

The API enforces strict rules for tool results. Breaking them causes 400 errors.

RuleExplanation
Results come immediately after the callThe user message with the results must directly follow the assistant message that contains the tool_use blocks. No other messages in between.
Every call gets a resultEach tool_use block needs exactly one tool_result with its id.
All results go in one messageWhen Claude made several calls, return all their results together in a single user message.
Results come firstIn that user message, all tool_result blocks must come before any text blocks.
The assistant message is unchangedSend response.content back exactly as received, including any thinking blocks (Lesson 16).

All Results in One Message

According to the parallel tool use documentation, sending each result in a separate user message is the most common formatting mistake. It also "teaches" Claude to avoid parallel calls in later turns.

# Wrong: one user message per result
{"role": "user", "content": [result_1]},
{"role": "user", "content": [result_2]},

# Correct: one user message with all results
{"role": "user", "content": [result_1, result_2]},

Text After Results

You may add text after the results in the same message, for example new information from the user:

{"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01...", "content": "31°C, light rain at 5 PM"},
    {"type": "text", "text": "Also, I prefer walking in the morning."},
]}

Putting text before a tool_result block causes a 400 error.


What to Put in content

A String

The simplest and most common form:

{"type": "tool_result", "tool_use_id": call_id, "content": "31°C, humid, light rain expected at 5 PM"}

Structured Data as JSON Text

For structured results, send compact JSON as a string (Lesson 86). Claude reads JSON well:

{"type": "tool_result", "tool_use_id": call_id,
 "content": '[{"slug": "django-101", "title": "Django for Beginners", "weeks": 6}]'}

A List of Content Blocks

content can also be a list of blocks, such as several text blocks, or images, for example a chart generated by a tool or a screenshot (images are covered in Lessons 111–113):

{"type": "tool_result", "tool_use_id": call_id, "content": [
    {"type": "text", "text": "Sales chart for October:"},
    {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": chart_base64}},
]}

Writing Useful Results

GuidelineWhy
Include only what Claude needsResults become input tokens on every following request
Use stable identifiers (slugs, IDs)Claude can refer to them in later tool calls
Make empty results explicit"No courses found for 'Rust'." is clearer than []
Note truncationTell Claude when a result was shortened, so it does not assume it saw everything

Error Results

When a tool fails, still send a result, with is_error: True and an explanation:

{
    "type": "tool_result",
    "tool_use_id": call_id,
    "is_error": True,
    "content": "No weather data for 'Mumbai'. Supported cities: Chennai, Bengaluru.",
}

Claude reads the error and adapts: it may retry with corrected input, try a different tool, or explain the problem to the user. Lesson 90 covers error handling in depth.

Calls You Choose Not to Run

Sometimes you skip a call, for example when an earlier call in a sequence failed. According to the documentation, you should still return a tool_result for it, with is_error: True and a brief explanation:

{"type": "tool_result", "tool_use_id": "toolu_02", "is_error": True,
 "content": "Not executed: the preceding create_booking call failed."}

A missing result is a formatting error. An explicit "not executed" result keeps the conversation valid and tells Claude what happened.


Practical Example: Building and Checking Tool Results

This module builds the tool-result message for a response, using run_tool() from Lesson 86, and includes a checker that verifies the formatting rules before a request is sent. Catching formatting mistakes locally is much easier than debugging 400 errors.

"""tool_results.py: build tool_result messages and check them against the formatting rules."""

from types import SimpleNamespace

from demo_tools import REGISTRY
from tools_lib import ToolContext, run_tool


def build_results_message(response, ctx: ToolContext) -> dict:
    """Run every tool call in a response and return ONE user message with all results."""
    results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        outcome = run_tool(REGISTRY, block.name, dict(block.input or {}), ctx)
        result = {"type": "tool_result", "tool_use_id": block.id, "content": outcome.content}
        if outcome.is_error:
            result["is_error"] = True
        results.append(result)
    return {"role": "user", "content": results}


def field(block, name):
    return block.get(name) if isinstance(block, dict) else getattr(block, name, None)


def check_results(assistant_content, results_message: dict) -> list[str]:
    """Return a list of formatting problems; an empty list means the message is valid."""
    problems = []
    call_ids = [field(b, "id") for b in assistant_content if field(b, "type") == "tool_use"]
    blocks = results_message["content"]
    result_ids = [b["tool_use_id"] for b in blocks if b.get("type") == "tool_result"]

    if results_message["role"] != "user":
        problems.append("tool results must be in a user message")
    for missing in set(call_ids) - set(result_ids):
        problems.append(f"no result for call {missing}")
    for extra in set(result_ids) - set(call_ids):
        problems.append(f"result for unknown call {extra}")
    if len(result_ids) != len(set(result_ids)):
        problems.append("more than one result for the same call")

    seen_text = False
    for block in blocks:
        if block.get("type") == "text":
            seen_text = True
        elif block.get("type") == "tool_result" and seen_text:
            problems.append("a tool_result appears after text; results must come first")
            break
    return problems


if __name__ == "__main__":
    response = SimpleNamespace(content=[
        SimpleNamespace(type="text", text="Let me check both."),
        SimpleNamespace(type="tool_use", id="toolu_01", name="get_weather", input={"city": "Chennai"}),
        SimpleNamespace(type="tool_use", id="toolu_02", name="get_weather", input={"city": "Mumbai"}),
    ])

    message = build_results_message(response, ToolContext(user_id="meena"))
    for block in message["content"]:
        print(block)
    print("problems:", check_results(response.content, message) or "none")

    broken = {"role": "user", "content": [
        {"type": "text", "text": "Here you go:"},
        {"type": "tool_result", "tool_use_id": "toolu_01", "content": "31°C"},
    ]}
    print("broken message problems:", check_results(response.content, broken))

Output:

{'type': 'tool_result', 'tool_use_id': 'toolu_01', 'content': '31°C, humid, light rain expected at 5 PM'}
{'type': 'tool_result', 'tool_use_id': 'toolu_02', 'content': "No weather data for 'Mumbai'. Supported cities: Chennai, Bengaluru.", 'is_error': True}
problems: none
broken message problems: ['no result for call toolu_02', 'a tool_result appears after text; results must come first']

How It Works

  1. build_results_message() runs every tool call in the response with run_tool(), and collects all results into one user message. That satisfies the "every call gets a result" and "all results in one message" rules by construction.
  2. is_error is added only for failures. The Mumbai call fails with a helpful message, and still produces a result for its ID.
  3. field() reads fields from SDK block objects or dictionaries, so the checker works on live responses and stored histories (Lesson 41).
  4. check_results() verifies the rules:
    • the message has the user role;
    • every call ID has a result, and no result refers to an unknown call, using set differences;
    • no call has two results;
    • no tool_result block comes after a text block.
  5. The broken message fails two checks: toolu_02 has no result, and the text block comes before a result. Running the checker in tests or during development catches these mistakes before the API rejects the request.

Common Mistakes

Missing Results for Some Calls

What happens: a 400 error about tool_use IDs without corresponding results.

Fix: return a result for every call, including error results for calls that failed or were skipped.

Separate Messages for Each Result

What happens: malformed conversations, and fewer parallel tool calls in later turns.

Fix: put all results in one user message.

Text Before Results

What happens: a 400 error.

Fix: place all tool_result blocks first; text may follow them.

Wrong or Reused IDs

What happens: results do not match their calls, and requests fail.

Fix: copy tool_use_id from each call's id, exactly.

Raising Exceptions Instead of Returning Errors

What happens: the conversation stops with an unanswered call.

Fix: return is_error: True results with clear explanations.


Best Practices

  • Return exactly one result per call, matched by tool_use_id.
  • Put all results in one user message, before any text.
  • Send the assistant's tool-use message back unchanged.
  • Use strings or compact JSON for most results, and content blocks for images.
  • Mark failures with is_error, with explanations that help Claude recover.
  • Return "not executed" errors for calls you skip.
  • Check formatting in tests before sending.

Exercise

  1. Build a fake response with three tool calls (two valid, one for an unknown tool), and print the resulting message.
  2. Add a check to check_results() that reports when a tool_result has empty content.
  3. Modify build_results_message() to accept optional user text and append it after the results.
  4. Explain why the documentation recommends returning a "not executed" error result instead of leaving out a call you chose not to run.
Show answers for steps 3 and 4

Step 3:

def build_results_message(response, ctx: ToolContext, user_text: str | None = None) -> dict:
    results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        outcome = run_tool(REGISTRY, block.name, dict(block.input or {}), ctx)
        result = {"type": "tool_result", "tool_use_id": block.id, "content": outcome.content}
        if outcome.is_error:
            result["is_error"] = True
        results.append(result)
    if user_text:
        results.append({"type": "text", "text": user_text})     # After all results
    return {"role": "user", "content": results}

Step 4: the API requires a result for every tool_use block, so leaving one out makes the request invalid. Beyond that, an explicit result tells Claude what happened, for example that a step was skipped because an earlier step failed. Claude can then decide what to do next, such as retrying, choosing another approach, or explaining the situation to the user, instead of working with a gap in its information.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Return Tool Results and get answers drawn from it.

Signed-in readers only.