Code-as-action beats JSON tool calls once tasks compose
A JSON tool call is one action per round-trip: the model emits a call, the harness executes it, the result comes back, repeat. Executable code is a program per round-trip: loops, conditionals, three tool invocations and an aggregation in a single step. That difference is invisible on simple tasks and decisive on composite ones — which is why my position is that once tasks compose, code-as-action wins, and the burden of proof sits with JSON.
The empirical case has been solid since CodeAct (ICML 2024): expressing agent actions as executable Python instead of JSON improved success rates by up to 20 percent across seventeen models, and code agents use roughly half the steps of ReAct-style JSON agents on GAIA. The mechanics behind those numbers matter more than the numbers. Composability: one code block chains tools with control flow where JSON spends a full model round-trip per call. Object management: a variable can hold a DataFrame or an image between calls — things a JSON envelope can't serialize, so JSON agents lossily re-describe them into text at every boundary. Training alignment: models have seen oceans of Python and only a trickle of tool-call schemas, so they're simply better at the former.
The subtler advantage is the feedback loop. A code session's state — variables, imports, defined functions — persists across turns, so the agent accumulates a workspace rather than a transcript. And execution output is a qualitatively richer observation than a tool result: a traceback carries the error type, the line, the call stack — structure the agent can act on, which is what makes self-debugging work without a human in the loop. Code agents can even define new functions mid-session, growing their own tool set. A JSON agent's world is the fixed schema it was handed; a code agent's world compounds.
The price is a real threat model. Code-as-action's security landscape spans sandbox escape, resource exhaustion, and data exfiltration — an agent that writes programs needs a sandbox engineered like one, sized to the blast radius, not an exec() with a warning comment. And the capability curve cuts both ways: the code advantage widens as models get stronger, but below a code-competence threshold it inverts — semantic errors produce no traceback, so the self-debugging loop that justifies the paradigm silently stops working, and failure modes like variable hallucination (confabulating a value instead of printing it) creep in.
The concession, and it's substantial: JSON tool calling keeps genuine advantages that have nothing to do with model capability. A fixed schema is an enumerable action space — auditable before execution, constrainable with strict-mode grammars, reviewable by a human who can read "transfer_funds(amount=500)" faster than a script. For narrow predetermined workflows, for high-stakes side-effecting actions, and for compliance regimes that must log discrete intentions rather than programs, the predictability is the point. The schema is a permission boundary; code dissolves it.
So the design rule is the agency spectrum applied to action spaces: match expressiveness to compositionality. Tasks that are genuinely one-call-at-a-time — lookups, single mutations, routed intents — should stay JSON and keep its audit properties, a boundary that is API design under uncertainty either way. Tasks that compose — data analysis, multi-source research, anything with intermediate objects — should get code and a sandbox worthy of it. What doesn't survive is the default I still see everywhere: JSON tool calls for everything, with the composition cost paid invisibly in round-trips, token spend, and lossy re-serialization at every step.