How to Implement `user.custom_tool_result` for Tool Call Responses in Anthropic Managed Agents

Emit a user.custom_tool_result event containing the tool output and the original custom_tool_use_id to return custom tool execution results back to the managed agent's event stream.

The anthropics/cwc-workshops repository demonstrates the complete lifecycle of custom tool integration in Claude-managed agents. When a model invokes a custom tool via agent.custom_tool_use, your host code must execute the corresponding Python function and bridge the result back through the user.custom_tool_result event type. This pattern ensures deterministic request-response pairing and maintains transcript continuity for model reasoning.

Understanding the user.custom_tool_result Event Loop

The event loop in ship-your-first-managed-agent/provided.py implements a three-stage handshake for custom tool execution. This mechanism separates sandboxed tool calls (handled internally) from custom tools that require host-side Python execution.

Detecting Custom Tool Requests

When the event stream returns an agent.custom_tool_use event, the system distinguishes it from standard sandbox tools. The event contains a unique identifier, tool name, and input arguments that your handler must process.

In ship-your-first-managed-agent/provided.py (lines 72-73), the detection logic branches based on event type:

elif ev.type == "agent.custom_tool_use":
    # Custom tool detected - requires host-side execution

    tool_boxes[ev.id] = box  # Store placeholder for async result matching

The code stores a placeholder "box" in a tool_boxes dictionary using the event's id as the key. This temporary storage enables the system to match asynchronous responses when they arrive.

Executing Handlers and Emitting Results

Once detected, the host invokes your custom handle_tool function and wraps the return value in a properly structured user.custom_tool_result event. The critical requirement is preserving the custom_tool_use_id to link the response to the original request.

From ship-your-first-managed-agent/provided.py (lines 84-92):

result = handle_tool(ev.name, ev.input)
events = [{
    "type": "user.custom_tool_result",
    "custom_tool_use_id": ev.id,
    "content": result,
}]
client.beta.events.create(agent_id=agent.id, events=events)

Key implementation details:

  • The handle_tool function executes your local Python implementation
  • The custom_tool_use_id must exactly match the originating event's id
  • The content field accepts a string containing the tool output
  • Events are submitted via the Anthropic client's beta.events.create method

Consuming Results in the Event Stream

When the user.custom_tool_result event returns to the event loop, the system retrieves the stored placeholder and injects the content into the conversation transcript as an assistant message.

From ship-your-first-managed-agent/provided.py (lines 200-209):

elif ev.type == "user.custom_tool_result":
    box = tool_boxes.pop(ev.custom_tool_use_id, None)
    if box:
        transcript.append({"role": "assistant", "content": ev.content})

This insertion allows the model to reason about the tool output as part of the ongoing dialogue, effectively treating the custom tool result as if the model had generated the text itself.

Implementing Custom Tool Handlers

Custom tools are defined in ship-your-first-managed-agent/agent.py by extending the handle_tool function. The following example adds a weather lookup tool:


# ship-your-first-managed-agent/agent.py

def handle_tool(name: str, args: dict) -> str:
    if name == "weather":
        city = args.get("city", "San Francisco")
        # Production implementations would call an external API

        return f"The weather in {city} is sunny, 72°F."
    return f"unknown tool {name}"

The existing event loop in provided.py automatically routes the returned string through user.custom_tool_result. No additional modifications are required to the event emission logic when adding new tools—only the handler implementation needs extension.

Complete Implementation Example

The following script demonstrates the full round-trip from tool request to result consumption:

from anthropic import Anthropic

# Assume client and AGENT_ID are initialized

# 1. Model requests custom tool execution

client.beta.events.create(
    agent_id=AGENT_ID,
    events=[{
        "type": "agent.custom_tool_use",
        "id": "tool-123",
        "name": "weather",
        "input": {"city": "Berlin"},
    }],
)

# 2. Host receives the event, executes handle_tool("weather", {"city": "Berlin"})

#    and automatically emits user.custom_tool_result via provided.py logic

# 3. Subsequent event stream reads include the result:

#    {"type": "user.custom_tool_result", "custom_tool_use_id": "tool-123", "content": "..."}

Manual Event Emission for Edge Cases

While provided.py handles automatic emission in standard workflows, you may need to manually emit results when operating outside the built-in loop. Maintain the exact schema structure:

result = "Tool execution output here"

client.beta.events.create(
    agent_id=AGENT_ID,
    events=[{
        "type": "user.custom_tool_result",
        "custom_tool_use_id": "tool-123",  # Must match original request ID

        "content": result,
    }],
)

Critical requirements for manual emission:

  • The custom_tool_use_id must match the corresponding agent.custom_tool_use event's id
  • The content field must be a string
  • The event type must be exactly user.custom_tool_result

Summary

  • user.custom_tool_result bridges custom tool execution back to the managed agent's conversation stream
  • Deterministic pairing relies on matching custom_tool_use_id values between request and response events
  • Implementation flow: Detect agent.custom_tool_use → Execute handle_tool → Emit user.custom_tool_result → Consume result in transcript
  • Source files: Core logic resides in ship-your-first-managed-agent/provided.py, while tool definitions belong in ship-your-first-managed-agent/agent.py
  • Extensibility: Adding tools requires only extending the handle_tool function without modifying event loop mechanics

Frequently Asked Questions

What happens if the custom_tool_use_id doesn't match?

If the custom_tool_use_id in your user.custom_tool_result event does not match any pending tool ID stored in tool_boxes, the event loop cannot associate the result with the original request. According to the provided.py implementation (lines 200-209), the code attempts to pop from tool_boxes with a default of None, and if no box is found, the result is silently ignored rather than inserted into the transcript.

Can custom tools return structured data like JSON?

Yes, but you must serialize structured data to a string before emitting the user.custom_tool_result event. The content field accepts string values only. Parse the JSON in your handle_tool function, then return json.dumps(result) to ensure valid transmission. The model will receive the JSON string and can parse it contextually within the conversation.

How does this differ from regular sandbox tool_use events?

Standard agent.tool_use events execute within Anthropic's managed sandbox environment and return results automatically without host intervention. In contrast, agent.custom_tool_use events require your host code to execute Python functions locally and explicitly emit user.custom_tool_result events to return data. The custom tool pattern exists specifically for operations that require local system access or proprietary business logic.

Where is the handle_tool function defined?

The handle_tool function is defined in ship-your-first-managed-agent/agent.py (and ship-your-first-managed-agent/agent_complete.py for the full example). This function acts as the dispatch layer that maps tool names to your Python implementations. The provided.py file imports and calls this function when processing agent.custom_tool_use events, but the actual tool logic resides in your agent implementation file.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →