Command Palette

Search for a command to run...

JSON vs Programmatic Tool Calling with Claude

🇫🇷FR

Three approaches to connect tools to Claude: JSON schema with manual loop, Tool Runner SDK, and programmatic tool calling. Comparison, code examples, and practical guidance.

11 min read
claude-apitool-callingsdkanthropicagentstypescriptpython
Comparative diagram of three Claude tool calling approaches: JSON schema, Tool Runner SDK, and programmatic tool calling

When building an application with the Claude API, connecting tools is the first step toward a functional agent. The model can call functions, query databases, send messages. But how you wire those tools drastically changes performance, cost, and code complexity.

In 2026, the Claude API offers three distinct approaches. This article compares them with code, numbers, and practical experience from using all three.

The problem: round trips

Traditional tool calling works in a loop: the model requests a tool, your server executes it, sends back the result, the model reasons over the result, requests another tool, and so on. Each iteration is a full round trip with the model.

Your serverdispatch loopmessagestool_useClaude APIfull inference passx10 tools = x10 inferencestokens accumulate in contextlatency scales linearlyEvery tool call requires a full model inference pass

The problem becomes concrete as the number of tools grows. Checking expenses for 20 employees? 20 round trips. Every intermediate result piles into the context. According to Anthropic’s measurements, tool definitions alone can consume over 134,000 tokens before any conversation in a multi-server setup.

Approach 1: JSON schema + manual loop

This is the original approach. You define each tool with a JSON schema, send the request to the API, parse the tool_use response, execute the tool, and send back the result.

import anthropic, json

client = anthropic.Anthropic()

tools = [{
    "name": "query_database",
    "description": "Execute a SQL query. Returns rows as JSON.",
    "input_schema": {
        "type": "object",
        "properties": {
            "sql": {"type": "string", "description": "SQL query"}
        },
        "required": ["sql"]
    }
}]

messages = [{"role": "user", "content": "What's the revenue by region?"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )

    if response.stop_reason == "end_turn":
        break

    for block in response.content:
        if block.type == "tool_use":
            result = execute_sql(block.input["sql"])
            messages.append({"role": "assistant", "content": response.content})
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result)
                }]
            })

The while True loop is the critical part. You handle the request/response cycle, parse tool_use blocks, accumulate messages, and manage error cases yourself.

Pros: full control over every step. You can log, filter results, inject business logic between calls.

Cons: lots of boilerplate. Every tool result passes through the model, even if it’s just an intermediate value. With 10 tool calls, that’s 10 full inference passes.

Approach 2: Tool Runner SDK

The Tool Runner is a helper in the official Anthropic SDKs (Python, TypeScript, Go, Java, C#, PHP, Ruby). It automates the agentic loop: tool definition, execution, conversation state management, and type validation.

In Python, the @beta_tool decorator generates the JSON schema from type hints and the docstring:

from anthropic import Anthropic, beta_tool

client = Anthropic()

@beta_tool
def query_database(sql: str) -> str:
    """Execute a SQL query on the sales database.

    Args:
        sql: SQL query to execute
    """
    rows = db.execute(sql)
    return json.dumps(rows)

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-5",
    max_tokens=4096,
    tools=[query_database],
    messages=[{"role": "user", "content": "What's the revenue by region?"}],
)

for message in runner:
    print(message)

In TypeScript, two options: betaZodTool with Zod validation (recommended), or betaTool with a plain JSON schema:

import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const queryDatabase = betaZodTool({
  name: "query_database",
  description: "Execute a SQL query on the sales database",
  inputSchema: z.object({
    sql: z.string().describe("SQL query to execute"),
  }),
  run: async (input) => {
    const rows = await db.execute(input.sql);
    return JSON.stringify(rows);
  },
});

const result = await client.beta.messages.toolRunner({
  model: "claude-sonnet-5",
  max_tokens: 4096,
  tools: [queryDatabase],
  messages: [{ role: "user", content: "What's the revenue by region?" }],
});

The Tool Runner handles the loop automatically: when Claude requests a tool, the runner executes it and sends back the result. No more while True, no manual tool_use block parsing.

Pros: less code, type safety with Zod or Python type hints, built-in error handling.

Cons: same execution model as the manual approach under the hood. Each tool call is still a round trip with the model. The gain is in DX, not performance.

Approach 3: programmatic tool calling

Programmatic tool calling changes the execution model. Instead of requesting one tool at a time through the API, Claude writes Python code that calls your tools directly inside a code execution container. Intermediate results stay in the container and only enter the model’s context when Claude explicitly sends them.

Your servertool_resultClaude API + code containerPython codeloop + filteringyour toolsasync Python1 single inference - Claude writes the codeN tool calls inside the containeronly the final result enters the context

To enable this approach, you need two things: include the code_execution tool in the request, and add allowed_callers to the tools Claude can call from code:

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    messages=[{
        "role": "user",
        "content": "Compare revenue across the West, East, and Central regions"
    }],
    tools=[
        {"type": "code_execution_20260120", "name": "code_execution"},
        {
            "name": "query_database",
            "description": "Execute a SQL query. Returns rows as JSON.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "sql": {"type": "string", "description": "SQL query"}
                },
                "required": ["sql"]
            },
            "allowed_callers": ["code_execution_20260120"]
        }
    ],
)

Claude then generates a Python script that calls query_database in a loop or in parallel via asyncio.gather, filters the results, and only sends back the summary:

import json, asyncio

results = {}
for region in ["West", "East", "Central"]:
    rows = json.loads(await query_database({
        "sql": f"SELECT SUM(revenue) as total FROM sales WHERE region = '{region}'"
    }))
    results[region] = rows[0]["total"]

best = max(results, key=results.get)
print(f"Highest revenue region: {best} ({results[best]} USD)")
print(f"Details: {json.dumps(results)}")

Instead of 3 model round trips, a single inference produced the code, and all 3 tool calls execute inside the container. The model only receives the final print() output.

Tools are exposed as async Python functions. The allowed_callers field controls who can call each tool:

ValueBehavior
["direct"]Standard API call (default)
["code_execution_20260120"]Call only from code execution
["direct", "code_execution_20260120"]Both modes

Anthropic recommends choosing a single mode per tool to avoid ambiguity.

Pros: massive token and latency reduction. BrowseComp and DeepSearchQA benchmarks show +11% performance and -24% input tokens. On complex research tasks, average usage drops from 43,588 to 27,297 tokens, a 37% reduction.

Cons: requires the code_execution tool (beta). Containers have a limited lifetime (~5 minutes idle). Not available on Amazon Bedrock or Google Cloud. The field is not a security boundary: allowed_callers guides Claude but does not strictly block direct calls.

Comparison

CriterionJSON + loopTool Runner SDKProgrammatic
Inferences per N toolsNN1 + returns
Intermediate tokensIn contextIn contextIn container
Code complexityHighLowMedium
Per-step controlFullLimitedVia generated code
LatencyN x inferenceN x inference1 inference + exec
Type safetyManualZod / type hintsN/A (generated code)
AvailabilityGA, all cloudsBeta, all cloudsBeta, direct API

When to use what

JSON + manual loop when you need fine-grained control between each tool call. For example: a workflow with human validation between steps, detailed logging, or conditional business logic. It’s also the only option if you target Bedrock or Google Cloud without the Tool Runner.

Tool Runner SDK for most use cases. The code is cleaner, type validation is automatic, and the behavior is identical to the manual loop under the hood. This is the approach I use by default in my TypeScript and Python projects.

Programmatic tool calling when the number of tool calls per request is high or intermediate results are large. The typical example: aggregating data from 20 sources, filtering, and only returning the summary. It’s also the most efficient approach for research tasks where Claude needs to explore and sort before concluding.

You can combine approaches. A tool marked ["direct"] will be called through the standard loop, while another marked ["code_execution_20260120"] will be orchestrated by the code. In the same project.

In practice

In CodeRift, I use the Tool Runner for review agents: each agent has 2-3 tools (read a file, search symbols, post a comment) and the number of calls is predictable. The Tool Runner eliminates boilerplate without sacrificing visibility.

For data exploration tasks in my IronFlow workflows, programmatic tool calling would be the right choice: an agent scanning 50 GitLab projects, filtering open MRs, and aggregating stats. The heavy lifting happens in the container, and only the summary enters the context.

The manual JSON approach stays for cases where the Tool Runner isn’t available (unsupported SDK, or integration with a third-party framework that manages its own loop).

The choice isn’t permanent. Start with the Tool Runner, migrate to programmatic when the token bill justifies it.