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

> Learn how to use programmatic tool calling with Claude to cut latency and token use. Execute multiple tool calls in one API trip, reducing costs and improving performance.

- Repository: [Anthropic/claude-cookbooks](https://github.com/anthropics/claude-cookbooks)
- Tags: how-to-guide
- Published: 2026-04-14

---

**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.

```python
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:

```python
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:

```python
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.

```python
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.

```python
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.