Every morning somebody on the team runs the same check. Thirty tables in the staging schema, and the only thing anyone wants to know is which ones did not get loaded overnight. It is a five minute job, it is boring, and it is exactly the sort of thing you would hand to an agent. So you do. And then you watch it take thirty separate trips to the model, one table at a time, dragging every result set through the context window on the way. The answer was three table names. You paid for thirty round trips to get them.
The LLMCompiler paper put a name on this in 2024. Treat the tool calls like a compiler treats instructions: work out what depends on what, then run everything else at the same time. Two years later the idea is in the API, and it arrived in a shape nobody quite predicted. There is no plan format to parse. Claude just writes the Python.
1. One field changes everything
Add allowed_callers to a tool and switch on code execution. Your tool stops being something the model asks for one call at a time and becomes an async function sitting inside a sandbox, taking a dict, returning a string. From there, loops and conditionals and asyncio.gather come free. They are just Python.
import anthropic client = anthropic.Anthropic() TOOLS = [ {"type": "code_execution_20260120", "name": "code_execution"}, { "name": "run_sql", "description": ("Run a read-only query on the warehouse. " "Returns a JSON list of row objects."), "input_schema": { "type": "object", "properties": {"sql": {"type": "string"}}, "required": ["sql"], }, "allowed_callers": ["code_execution_20260120"], }, ] resp = client.messages.create( model="claude-sonnet-5", max_tokens=4096, tools=TOOLS, messages=[{"role": "user", "content": "For every table in the stage schema, get the latest " "load timestamp and list only those older than 6 hours."}], )
Sonnet 5 is the right call here. Plan, fan out, summarize, nothing that needs the expensive model, which is the same reasoning behind matching the model to the task rather than the app. The floor is the code execution version, not the tier, so any Sonnet from 4.5 forward will do.
2. Somebody else writes the loop
Back comes a server_tool_use block with the generated script inside it. The first time you read one of these it is a little uncanny, because it is roughly the code you would have written yourself on a Tuesday afternoon.
# written in the container, not by you import json, asyncio tables = json.loads(await run_sql({"sql": LIST_SQL})) rows = await asyncio.gather(*[ run_sql({"sql": f"select max(load_ts) ts from {t['name']}"}) for t in tables ]) stale = [(t["name"], r) for t, r in zip(tables, rows) if hours_since(r) > 6] print(stale)
One query to find the tables, then all thirty timestamps at once, then a filter. Three names get printed. Three names is all the model ever sees. The other twenty seven results lived and died in the sandbox, which is the whole point.
3. You still answer the phone
Your database is not in that container, so every time the script calls the tool, everything stops and waits for you. The response shows up with stop_reason of tool_use, a container id, and one block per pending call. Run the query, hand back the rows, the script picks up mid-line.
while resp.stop_reason == "tool_use": results = [ {"type": "tool_result", "tool_use_id": b.id, "content": execute(b.name, b.input)} for b in resp.content if b.type == "tool_use" ] # tool_result blocks only on this turn, nothing else messages += [{"role": "assistant", "content": resp.content}, {"role": "user", "content": results}] resp = client.messages.create( model="claude-sonnet-5", max_tokens=4096, container=resp.container.id, # required while calls pend tools=TOOLS, messages=messages, )
Two things will trip you on the first attempt. That user turn takes tool results and absolutely nothing else, not even a stray line of text. And the full tools array has to ride along on every continuation, code execution included, otherwise the paused script has nothing to wake up into.
4. Is it worth it
Depends entirely on the shape of your work, and the documentation is refreshingly blunt about it. On a 75-tool agent benchmark, about 38 percent fewer billed input tokens with accuracy unchanged. Across production traffic declaring 10 to 49 tools, savings of 20 to 40 percent. And on a benchmark where each turn made one or two sequential calls, it cost 8 percent more. Fan-out wins. Short chains lose.
Before you point this at production
Keep an eye on the clock. A pending call gives up after about four minutes and throws a timeout inside the running script, and idle containers get reclaimed after about five, so a query that takes ten minutes needs chunking, not optimism. Availability is uneven at the moment: Claude API, Claude Platform on AWS and Microsoft Foundry yes, Bedrock and Google Cloud not yet. And the obvious one, which is worth saying anyway. Every query in that fan-out is generated text. Give the tool a read-only connection and let the database enforce it, because allowed_callers is a hint to Claude about how to use the tool, not a wall around it.