Code as action: when the agent writes a program instead of picking a tool.
Anthropic reported a Google Drive-to-Salesforce workflow dropping from 150,000 tokens to 2,000 — a 98.7% cut — by having the model write code that calls tools rather than emit one tool call per step; Cloudflare compressed 2,500 API endpoints from roughly 244,000 tokens of schema down to about 1,000 by exposing two tools and a sandbox. Those numbers are real and they are not free: the thing you spend is the action log. A list of tool calls is something a policy engine can gate, an approver can read and an auditor can reconstruct; a program is not. That trade — not the token count — is what decides where this pattern belongs.
What actually changes: intermediate results stop entering the context.
The saving is often described as smaller tool definitions. That is the smaller half. The structural change is that data the model never needed to read stops passing through the model.
- The classic loop pays for every intermediate value twice. Each tool result is appended to the transcript, and the whole transcript is re-sent on the next step. A 40,000-token spreadsheet fetched at step 3 is still being paid for at step 12 — the quadratic growth agent cost control describes.
- In a program, the value lives in a variable. Fetch the sheet, filter it, aggregate it, write the result — and only the result crosses back into context. The model authored the transformation without ever seeing the rows, which is both the saving and the risk.
- Tool definitions become discoverable rather than resident. Instead of every schema occupying context on every turn, the tools are files or modules the agent lists and reads on demand. This is why the Cloudflare number is so extreme: 2,500 endpoints as resident schemas is impossible; as a searchable filesystem it is two tools.
- Round trips collapse from n to one. Twenty dependent tool calls are twenty inference passes, each with its own prefill over a growing prompt. A loop expressed in code executes at machine speed inside one turn — a latency win that usually dwarfs the token win, for the reasons in prefill, decode and the KV cache.
- Control flow becomes expressible. Retries, conditionals, and loops whose bounds depend on data are awkward to drive through a turn-by-turn loop and trivial in a program. A model that can write
fordoes not need you to have anticipated the iteration count.
The concise version: tool calling puts the model in the data path; code as action takes it out. Everything good and everything dangerous about the pattern follows from that one sentence.
Where the win is real, and where it is a rounding error.
The published numbers come from the best cases, and they are honest about it. Applying the pattern where the shape does not fit adds a sandbox, an interpreter and a new class of failure in exchange for nothing.
- Real: large intermediate data the model does not need to read. Filtering a result set, joining two sources, aggregating a log, transforming a document. The bigger the gap between bytes fetched and bytes needed for the decision, the bigger the win.
- Real: fan-out over a collection. "For each of the 60 open issues, fetch the linked PR and check whether tests ran." As tool calls that is 120 round trips; as a program it is a loop, and the model reads only the summary.
- Real: a tool surface too large to inline. Anything past a few dozen tools is already degrading selection accuracy, as tool discovery and docs argues. Progressive disclosure through a filesystem is a better answer than a bigger prompt.
- Illusory: three calls with small results. The interpreter round trip, the code the model must write, and the error handling all cost more than the three JSON blocks they replace.
- Illusory: work where the model must inspect the data to decide. If the next action depends on reading the rows, the rows have to enter context, and you have kept the cost while adding a sandbox.
- Negative: single consequential actions. "Issue the refund" is one call with one argument. Wrapping it in a program adds no efficiency and removes the approval surface, which is the subject of the next step.
The cost nobody prices: your action log becomes a program.
This is the argument of the page. Three controls that production agent systems depend on are all built on the same assumption — that actions arrive one at a time, as structured objects, before they execute. Code as action breaks that assumption for all three simultaneously.
- Policy enforcement keys on tool calls. A rule like "refunds over £500 require approval" is implemented as an interceptor between the model's proposed call and the execution of it. When the model emits a program, the proposed call does not exist as an object until the program is already running — so the interception point has moved inside the sandbox, and policy as code has to be re-implemented there or it is not enforced at all.
- Approval gates lose their subject. Showing a user "the agent wants to call
send_emailwith these arguments" is reviewable. Showing them forty lines of Python and asking whether to run it is not review, it is a code audit performed by someone who did not want one — and it fails the review-cost test in agent UX patterns. - The audit trail records the wrong granularity. "Executed script #4471" tells an investigator nothing about which records were touched. Reconstructing that requires the script, its inputs, and a deterministic replay you probably do not have. The decision receipt is now a program plus an environment.
- Blast radius stops being enumerable in advance. With per-call tools you can bound the run by counting: at most three writes, to these two systems. A loop can call the same write tool a thousand times, and the bound has to become a runtime quota rather than a plan review.
Say it precisely, because the sloppy version leads to the wrong fix: the pattern does not make the agent less safe, it moves the enforcement point from your orchestration layer into the code runtime. If you move the controls with it, you lose nothing. If you do not — and the default implementations do not — you have quietly deleted three controls to save tokens.
The runtime is the new enforcement boundary — build it that way.
The fix is not to give up the pattern. It is to treat the interpreter as a security boundary with a policy of its own, rather than as a convenience that happens to run code.
- The bindings are the capability grant. The functions you inject into the interpreter's global scope are exactly the tools the program can call — nothing else exists. That is a cleaner capability model than a tool list in a prompt, because it is enforced by the runtime rather than requested of the model.
- Log from inside, at the binding. Each injected function records its own call — arguments, result size, duration, task ID — so the trace still contains a list of tool calls even though the model never emitted one. This single decision recovers most of what STEP 3 took away, and it costs a wrapper.
- Keep effectful tools out of the program. The strongest version of this pattern is asymmetric: reads, transforms and queries run inside the sandbox; writes, payments, messages and anything irreversible return to the model turn as ordinary tool calls that your existing gates intercept. You get the token win on the 95% of calls that are reads and keep the review surface on the 5% that matter.
- Quota the runtime, do not trust the program. Wall-clock, memory, number of calls per binding, and a per-run write budget — enforced by the interpreter, fail-closed. A model-authored loop with an off-by-one is a much likelier incident than a malicious program.
- The sandbox needs the same five decisions as any other. Filesystem, egress, credentials, compute, lifetime — sandboxing and code execution covers them, and egress is the one that matters most here, because a program that can reach the network can exfiltrate everything it just filtered without ever surfacing it to the model.
- Deterministic replay is worth paying for. Store the program, the binding versions and the inputs. Being able to re-run a script against recorded tool responses turns an unreadable audit artifact back into a readable one, and it is the only practical answer to "what exactly did run 4471 do?"
New failure modes, and why they are quieter than tool-call failures.
Per-call tool use fails loudly: a bad argument returns an error the model reads and reacts to on the next turn. Programs fail in ways that produce a plausible answer.
- An exception loses the whole batch. One malformed record on iteration 43 of 60 aborts the script, and unless the model wrote per-item error handling — it usually did not on the first attempt — the other 59 results are gone too. Per-call loops degrade gracefully here; programs do not.
- Silent partial success is the dangerous shape. A program that caught its exceptions and continued returns a summary that looks complete. "Processed 60 records" is a claim the model made about its own code, not an observation, and nothing in the transcript contradicts it.
- The model transforms data it never saw. Filtering by a column it assumed exists, parsing a date format it guessed, deduplicating on a field that is not unique. In the per-call pattern the data was in context and errors of this kind were visible; here the wrong answer is computed correctly.
- Debugging crosses a boundary. The failure is in generated code running in a sandbox with injected bindings, so a stack trace is only useful if it survives back into the transcript in a form the model can act on — the same requirement tool error messages sets, now applied to runtime exceptions.
- Non-determinism compounds. Two runs of the same task produce different programs with different edge-case handling. The variance you already have from sampling now expresses itself as structurally different execution paths, which is worth remembering when you set eval sample sizes.
The decision rule.
Everything above reduces to one question asked per tool, not per system — the pattern is not an architecture choice you make once.
- Does each individual effect need to be authorised? If yes, that tool stays on the model turn where the gate is. If no, it can live in the sandbox.
- Is there a large gap between bytes moved and bytes needed? If yes, the pattern pays. If the model has to read the data to decide, it does not.
- Is the work iterative with a data-dependent shape? Loops and fan-out are where a program beats a turn-by-turn loop by a margin that is not close.
- Can you afford a code sandbox with real egress control? If not, you cannot afford this pattern, because the alternative is a program with your credentials and an open network.
- Would a reviewer be shown the program or the effects? If your interface has to render this for a human, design the summary before you build the runtime — retrofitting legibility onto generated code does not work.
Start asymmetric: move only your read and transform tools behind a code runtime, keep every write on the model turn where your approval gates already live, and wrap each injected binding so it logs a normal tool-call span. You will capture most of the 98.7% — reads are where the bytes are — while your policy engine, your approval UI and your audit trail keep working unchanged. The token saving comes from taking the model out of the data path; the danger comes from taking your controls out with it, and those two are separable if you separate them on purpose.
Related: tool granularity for the sizing decision this changes, advanced tool orchestration patterns for the alternatives, MCP tool design for the server side of the schema-bloat problem, and cost control at the loop level for what else moves the same number.