cd /news/ai-agents/json-vs-programmatic-tool-calling-wi… · home topics ai-agents article
[ARTICLE · art-127048] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

JSON vs Programmatic Tool Calling with Claude

A developer compared three approaches to connecting tools in the Claude API: traditional JSON-schema tool calling, the SDK's Tool Runner helper, and programmatic tool calling. The comparison found that traditional tool calling requires a full inference round trip per tool call and that tool definitions alone can consume over 134,000 tokens in multi-server setups, according to Anthropic's measurements. The Tool Runner automates the agentic loop, handling tool definition, execution, conversation state, and type validation across Anthropic's official SDKs.

by read7 min views3 publishedSep 11, 2026

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.

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.

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.

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.

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.

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:

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.

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.

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:

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:

Value Behavior
["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.

Criterion JSON + loop Tool Runner SDK Programmatic
Inferences per N tools N N 1 + returns
Intermediate tokens In context In context In container
Code complexity High Low Medium
Per-step control Full Limited Via generated code
Latency N x inference N x inference 1 inference + exec
Type safety Manual Zod / type hints N/A (generated code)
Availability GA, all clouds Beta, all clouds Beta, direct API

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

── more in #ai-agents 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/json-vs-programmatic…] indexed:0 read:7min 2026-09-11 ·