How to Implement Custom Tool Calls that Require Action and Wait for Server Response in Anthropic's CWC Workshops

The CWC-Workshops repository demonstrates how to implement custom tool calls that pause agent execution with a requires_action stop-reason, allowing a server-side orchestrator to perform asynchronous work and resume the session by returning a user.custom_tool_result matched to the original tool-use ID.

In the anthropics/cwc-workshops repository, the Research Desk workshop provides a production-ready pattern for handling custom tool calls that require_action and wait for external server processing. This architecture enables AI agents to initiate complex, long-running operations—such as fanning out analysis tasks to multiple sub-agents—while the main session remains idle until the orchestrator completes the work and signals continuation.

Understanding the requires_action Flow

The platform implements a specific lifecycle for asynchronous custom tools. When an agent invokes a custom tool, the session enters a paused state that requires external resolution before the conversation can continue.

The execution flow follows these steps:

  1. Agent Invocation: The head agent calls a custom tool (e.g., dispatch_analysts), triggering an agent.custom_tool_use event and immediately setting stop_reason.type to "requires_action" in research-desk/src/lib/orchestrator.ts.

  2. Orchestrator Detection: The server-side orchestrator listens for this event type and fans out the requested work (such as creating sub-sessions per ticker symbol).

  3. Result Delivery: Upon completion, the orchestrator sends a user.custom_tool_result payload containing the same id as the original tool-use event to satisfy the pending action.

  4. Session Resumption: The agent receives the result, the requires_action state clears automatically, and the head continues processing (for example, synthesizing a ranked report from analyst outputs).

Declaring Custom Tools in Prompt Files

Before the orchestrator can handle a tool, you must declare it in the prompts directory. Tool definitions reside in research-desk/prompts/*.md files and describe the contract between agent and server.


# dispatch_analysts

This tool asks the server to run an analyst for each ticker you provide. 
The agent will pause until the server returns a `user.custom_tool_result`.

Each markdown file in this directory defines one custom tool. The filename typically matches the tool name used in the orchestrator logic, creating a clear mapping between the agent's available functions and server-side implementations.

Handling requires_action in the Orchestrator

The core logic for managing custom tool calls that require_action lives in research-desk/src/lib/orchestrator.ts. This file contains the event handler that detects paused sessions and coordinates the asynchronous work.

// research-desk/src/lib/orchestrator.ts
export async function handleEvent(event: StreamEvent) {
  if (event.type === "agent.custom_tool_use") {
    switch (event.tool_name) {
      case "dispatch_analysts":
        const analystResults = await fanOutAnalysts(event.args.tickers);
        await sendToolResult({
          id: event.id,
          result: analystResults,
        });
        break;
      // Add additional custom tools here
    }
  }
}

The orchestrator checks event.stop_reason?.type !== "requires_action" in research-desk/src/lib/sessions.ts before proceeding with normal operations, ensuring that sessions with pending actions are properly quarantined until resolved.

Sending Results Back to Resume the Agent

To unblock a paused session, you must send a user.custom_tool_result with the exact id from the original agent.custom_tool_use event. The helper function typically posts to your platform's API endpoint:

async function sendToolResult(payload: {
  id: string;
  result: any;
}) {
  await fetch("/api/tool-result", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
}

Once this payload reaches the platform, the session automatically resumes because the requires_action condition is satisfied. The agent then receives the result data as if it were a synchronous tool return value.

Client-Side Tool Invocation

From the agent's perspective, invoking a custom tool that requires server action follows standard tool-use patterns. The key difference is that the session state indicates idleness rather than completion:

// Example client-side usage (head agent)
await agent.runTool("dispatch_analysts", { tickers: ["NVDA", "AMD", "MU"] });
// Session enters requires_action state → orchestrator processes → result arrives → conversation continues

The runTool method initiates the call, but execution pauses immediately after streaming the agent.custom_tool_use event. The agent remains in this idle state—freeing compute resources—until your orchestrator posts the matching result.

Why requires_action Matters for Asynchronous Operations

Using custom tool calls that require_action provides three critical advantages for complex AI workflows:

  • Resource Efficiency: Rather than busy-waiting or maintaining open connections during long-running operations, the platform holds the session idle, freeing resources while the server performs work.

  • Clear Contract: The requires_action state creates an unambiguous protocol where the server must return a user.custom_tool_result with the matching event id to unblock the agent.

  • Scalable Fan-Out: As demonstrated in the Research Desk workshop, this pattern supports fanning out work to multiple sub-agents or external services without blocking the main conversation thread.

Summary

  • Custom tool calls that require_action pause agent execution and wait for server-side orchestration in the CWC-Workshops framework.
  • Declare tools in research-desk/prompts/*.md files to define the agent-facing interface.
  • Implement handlers in research-desk/src/lib/orchestrator.ts to process agent.custom_tool_use events and manage asynchronous work.
  • Return results via user.custom_tool_result payloads matched by event id to automatically resume paused sessions.
  • Check stop_reason.type in research-desk/src/lib/sessions.ts to handle session state transitions correctly.
  • This pattern enables scalable, resource-efficient workflows where agents coordinate with external systems without blocking.

Frequently Asked Questions

What is the requires_action stop reason in CWC-Workshops?

The requires_action stop reason is a session state that indicates the agent has invoked a custom tool requiring external server processing. When this state appears in event.stop_reason.type, the agent cannot proceed until the orchestrator sends a matching user.custom_tool_result. This mechanism creates a clean separation between the agent's decision-making and long-running server operations.

How does the orchestrator match results to specific tool calls?

The orchestrator matches results using the event id field. When handling an agent.custom_tool_use event in research-desk/src/lib/orchestrator.ts, the code captures event.id and includes that same identifier when calling sendToolResult(). The platform uses this ID to correlate the user.custom_tool_result with the pending action, ensuring the correct session resumes with the correct data.

Can I implement multiple custom tools in the same orchestrator?

Yes. The switch statement in handleEvent() naturally supports multiple tools. Add additional cases for each custom tool name, ensuring each returns a user.custom_tool_result with the appropriate payload structure. Each tool declaration in research-desk/prompts/*.md corresponds to one case in the orchestrator's event handler.

Where should I check for requires_action states in my code?

Check requires_action states in your session management utilities, specifically in research-desk/src/lib/sessions.ts. The code should verify that event.stop_reason?.type !== "requires_action" before attempting to process normal conversation events. This check prevents premature processing of events while the session waits for external tool results.

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 →