How to Use Programmatic Tool Calling with Claude to Reduce Latency and Token Consumption

Programmatic Tool Calling (PTC) is a Claude beta feature that allows the model to execute multiple tool calls in a single API round-trip, eliminating intermediate HTTP requests and reducing token consumption by feeding aggregated results back to the model rather than individual tool_result messages.

Programmatic tool calling with Claude represents a paradigm shift from the traditional request-response loop of AI agents. According to the anthropics/claude-cookbooks repository, this beta capability enables Claude to emit batches of tool_use blocks that your application executes programmatically before returning a consolidated response. By implementing the PTC pattern demonstrated in programmatic_tool_calling_ptc.ipynb, you can significantly decrease latency and optimize token usage in production agent workflows.

What Is Programmatic Tool Calling?

Programmatic Tool Calling (PTC) enables Claude to generate multiple tool_use blocks within a single response rather than waiting for the result of each individual call. In the standard tool use pattern, the model generates one tool call, waits for your application to execute it, sends the result back, and then decides on the next action—a cycle that repeats for every tool interaction.

With PTC, Claude analyzes the task and emits all necessary tool calls upfront. Your agent executes these calls in parallel, formats the results into a single payload, and returns them to the model. The model then processes all results together and either requests additional tools or provides a final answer. This architecture is implemented in the run_agent_with_ptc function defined at lines 25‑70 of programmatic_tool_calling_ptc.ipynb.

How Programmatic Tool Calling Reduces Latency and Token Usage

Single Round-Trip Architecture

Standard tool use requires a separate HTTP request for every tool interaction, creating a linear chain of network round-trips. PTC collapses this into a minimal set of requests—often just two: the initial request containing all tool_use blocks, and the final request returning the end_turn response.

As implemented in the cookbook, the agent sends one request with the advanced-tool-use-2025-11-20 beta flag enabled. Claude returns multiple tool_use blocks marked with caller.type: "code_execution_20250825". Your Python backend calls the underlying functions (such as get_team_members or get_expenses), aggregates the outputs, and returns them in a single tool_result array. This eliminates the model-side waiting time between tool executions.

Context Window Efficiency

Traditional tool loops append every tool_result message to the conversation history, progressively filling the context window with intermediate JSON payloads. PTC allows you to summarize or filter raw tool outputs before re-inserting them into the prompt.

The notebook demonstrates this at lines 97‑104, where raw Python return values are serialized using json.dumps() and only the essential fields are preserved. By omitting the repetitive "assistant requests tool / user provides result" message pairs from the model's context, you maintain a smaller prompt window and reduce overall token costs for multi-step reasoning tasks.

Implementing the PTC Agent Loop

Enable the Beta Flag and Configure Tools

To enable programmatic tool calling, you must activate the beta feature in your API client and modify your tool definitions to include the code_execution type. The cookbook constructs the PTC-enabled tool list at lines 887‑893 of programmatic_tool_calling_ptc.ipynb by deep-copying the standard tool definitions and adding type: "code_execution" to each entry.

import copy

# Standard tool definitions

tools = [
    {
        "name": "get_team_members",
        "description": "Retrieve members of a department",
        "input_schema": {
            "type": "object",
            "properties": {"department": {"type": "string"}},
            "required": ["department"],
        },
    },
    {
        "name": "get_expenses",
        "description": "Fetch expenses for an employee",
        "input_schema": {
            "type": "object",
            "properties": {
                "employee_id": {"type": "string"},
                "quarter": {"type": "string"},
            },
            "required": ["employee_id", "quarter"],
        },
    },
]

# Create PTC-enabled variant

ptc_tools = copy.deepcopy(tools)
for tool in ptc_tools:
    tool["type"] = "code_execution"

When calling client.beta.messages.create, pass the advanced-tool-use-2025-11-20 beta header and the modified ptc_tools list:

response = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=4000,
    tools=ptc_tools,
    messages=messages,
    betas=["advanced-tool-use-2025-11-20"],
)

Handle Container State for Stateful Workflows

For workflows that require persistent execution state across multiple API calls, the PTC beta supports container tracking. When the API returns a container object, you must forward the container.id in subsequent requests to maintain the same execution sandbox.

According to lines 52‑58 of the notebook, extract the container ID from the response and include it in the extra_body parameter:

container_id = None

# Inside the agent loop

if hasattr(response, "container") and response.container:
    container_id = response.container.id
    print(f"[Container] ID: {container_id}")

# Pass container ID in the request

response = client.beta.messages.create(
    # ... other parameters ...

    extra_body={"container": container_id} if container_id else None,
)

Execute and Aggregate Tool Results

The core logic for handling batched tool calls appears in the run_agent_with_ptc function. When response.stop_reason equals "tool_use", iterate through the content blocks to identify BetaToolUseBlock instances, execute the corresponding Python functions, and format the results.

from anthropic.types.beta import BetaToolUseBlock, BetaTextBlock
import json

tool_functions = {
    "get_team_members": get_team_members,
    "get_expenses": get_expenses,
}

# Process tool_use response

if response.stop_reason == "tool_use":
    messages.append({"role": "assistant", "content": response.content})
    
    tool_results = []
    for block in response.content:
        if isinstance(block, BetaToolUseBlock):
            name = block.name
            args = block.input
            tool_use_id = block.id
            caller_type = block.caller["type"]  # "code_execution_20250825"

            
            print(f"[{caller_type.upper()}] Invoking {name}")
            
            # Execute the actual Python function

            raw_result = tool_functions[name](**args)
            
            # Serialize result (lines 97-104 pattern)

            if isinstance(raw_result, (dict, list)):
                content = json.dumps(raw_result)
            else:
                content = str(raw_result)
            
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use_id,
                "content": content,
            })
    
    # Single aggregated response back to Claude

    messages.append({"role": "user", "content": tool_results})

Complete Working Example

Below is a minimal, runnable implementation combining the setup, tool configuration, and agent loop from programmatic_tool_calling_ptc.ipynb. This example assumes you have installed anthropic and have ANTHROPIC_API_KEY configured.

from dotenv import load_dotenv
from utils.visualize import visualize
from utils.team_expense_api import get_team_members, get_expenses
import anthropic
import copy
import json
import time

load_dotenv()
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
viz = visualize(auto_show=True)

# Standard tool definitions

tools = [
    {
        "name": "get_team_members",
        "description": "Get team members for a department",
        "input_schema": {
            "type": "object",
            "properties": {"department": {"type": "string"}},
            "required": ["department"],
        },
    },
    {
        "name": "get_expenses",
        "description": "Get expenses for an employee in a quarter",
        "input_schema": {
            "type": "object",
            "properties": {
                "employee_id": {"type": "string"},
                "quarter": {"type": "string"},
            },
            "required": ["employee_id", "quarter"],
        },
    },
]

# Create PTC-enabled tools (lines 887-893)

ptc_tools = copy.deepcopy(tools)
for tool in ptc_tools:
    tool["type"] = "code_execution"

tool_functions = {
    "get_team_members": get_team_members,
    "get_expenses": get_expenses,
}

def run_agent_with_ptc(user_message: str):
    messages = [{"role": "user", "content": user_message}]
    container_id = None
    api_calls = 0
    total_tokens = 0
    start_time = time.time()

    while True:
        response = client.beta.messages.create(
            model=MODEL,
            max_tokens=4000,
            tools=ptc_tools,
            messages=messages,
            betas=["advanced-tool-use-2025-11-20"],
            extra_body={"container": container_id} if container_id else None,
        )
        
        viz.capture(response)
        api_calls += 1
        total_tokens += response.usage.input_tokens + response.usage.output_tokens
        
        # Container tracking (lines 52-58)

        if hasattr(response, "container") and response.container:
            container_id = response.container.id
        
        if response.stop_reason == "end_turn":
            final_text = next(
                (b.text for b in response.content if isinstance(b, anthropic.types.beta.BetaTextBlock)),
                None,
            )
            elapsed = time.time() - start_time
            return final_text, total_tokens, elapsed, api_calls
        
        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            
            tool_results = []
            for block in response.content:
                if isinstance(block, anthropic.types.beta.BetaToolUseBlock):
                    result = tool_functions[block.name](**block.input)
                    content = json.dumps(result) if isinstance(result, (dict, list)) else str(result)
                    
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": content,
                    })
            
            messages.append({"role": "user", "content": tool_results})

# Execute the agent

query = "Which engineering team members exceeded their Q3 travel budget?"
answer, tokens, seconds, calls = run_agent_with_ptc(query)

print(f"\nAnswer: {answer}")
print(f"Tokens: {tokens} | Time: {seconds:.2f}s | API calls: {calls}")

Typical output shows only two API calls regardless of how many expense reports Claude needs to analyze—one to generate all tool requests and one to receive the final answer after programmatic execution.

Summary

  • Programmatic tool calling allows Claude to batch multiple tool executions into a single API round-trip, dramatically reducing latency compared to sequential tool loops.
  • Enable PTC by setting type: "code_execution" in your tool definitions and passing the advanced-tool-use-2025-11-20 beta flag to client.beta.messages.create.
  • Token savings are achieved by omitting intermediate tool_result messages from the model's context window; only aggregated results are returned to Claude.
  • Use container tracking via response.container.id to maintain stateful execution environments across multiple agent iterations.
  • The reference implementation in programmatic_tool_calling_ptc.ipynb demonstrates parallel execution of get_team_members and get_expenses calls followed by consolidated result formatting.

Frequently Asked Questions

What is the difference between standard tool use and programmatic tool calling?

Standard tool use follows a turn-by-turn pattern where Claude generates one tool_use block, waits for the result, and then decides on the next action. Programmatic tool calling allows Claude to generate multiple tool_use blocks simultaneously, which your application executes in parallel before feeding all results back in a single payload. This eliminates the network latency of intermediate HTTP round-trips and reduces token usage by keeping intermediate results out of the model's context window.

How do I enable programmatic tool calling in the Claude API?

You must use the Anthropic Python SDK's beta interface and pass two specific configurations. First, add type: "code_execution" to each tool definition in your tools list (as shown at lines 887‑893 of the cookbook). Second, include betas=["advanced-tool-use-2025-11-20"] in your client.beta.messages.create call. The model will then return tool_use blocks marked with caller.type: "code_execution_20250825" that indicate they are part of the PTC batch.

Can I use programmatic tool calling with async Python?

Yes. While the cookbook examples use synchronous Python, the pattern supports async implementations. You can execute the collected tool_use blocks concurrently using asyncio.gather() or thread pools before aggregating the results. The critical requirement is that all tool results must be collected and formatted into a single tool_result array before the next API call to Claude, regardless of whether the execution is synchronous or asynchronous.

Does programmatic tool calling support stateful execution?

Yes. When working with stateful workflows that span multiple Claude interactions, the API returns a container object containing a unique id. You must capture this ID from the response (as implemented at lines 52‑58 of the notebook) and pass it via extra_body={"container": container_id} in subsequent requests. This ensures that variables, imports, or file system state persist across multiple turns of the conversation without re-initialization.

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 →