← Daily

Concept · No. 021 · November 5, 2026

Split risky agent tools into a plan step and an apply step

A tool that mutates the world in one call asks the model to be right before it has seen the consequences. Splitting it into plan and apply gives the agent, and any human gate, a concrete diff to judge, plus a way to catch stale state.

The idea

Most agent tools are shaped like API endpoints: update_records, send_invoice, delete_branch. The model reasons, calls the tool once, and the change is live. If its reasoning was off by one filter clause, the first evidence is the damage.

The plan and apply pattern, familiar from infrastructure tools like Terraform, splits that call in two. plan_* computes the exact change without performing it and returns a readable diff and a plan_id. apply_plan(plan_id) executes that stored plan and nothing else.

This gives you three things. The agent sees the blast radius before committing. A policy or a human can approve a concrete change instead of a description of intent. And because the plan records which version of the state it was computed from, apply can refuse to run if that state has since moved.

An example

An agent is asked to reassign open deals from a departing sales rep to two colleagues.

  • It calls plan_reassign_deals(from_owner, rule). The tool queries the CRM, computes every change, and returns a summary first: how many deals, grouped by stage, then the before and after owner for each, then a plan_id with an expiry.
  • The summary shows a group of closed-won deals. The request said open deals, so the filter was wrong. The agent discards the plan and calls plan again with a corrected rule.
  • The new plan is larger than the policy threshold for unattended bulk writes, so apply_plan returns a pending status and posts the summary to Slack for approval.
  • After approval, apply walks the stored changes. Each record carries the version it had at plan time. Two deals were edited by a person in the meantime, so apply skips them and returns a structured error naming both, and the agent can replan just those.

Where it breaks

The most common bug is an apply step that re-derives the change from the original arguments instead of executing the stored plan. Then the approved diff and the executed diff can differ, and the gate is theatre.

Plans go stale. Without version checks and an expiry, a plan approved in the morning can overwrite an afternoon of human edits.

Some effects cannot be previewed. A plan for sending an email can show the content and recipients, but not how a customer reacts. The split still helps, but it is not a safety net for irreversible external actions.

Plans that are too long to read get rubber-stamped by people and skimmed by models. And if the agent alone decides when to apply, it will learn to call plan then apply back to back, which only doubles latency.

For cheap, reversible writes, such as editing a scratch file, the extra round trip is pure cost.

In practice

I would reserve the pattern for writes that are bulk, irreversible, or cross a trust boundary, and keep everything else as a single call.

  • Store the plan server side. apply_plan takes only an id, never a plan body the model could have edited.
  • Bind each plan to state versions, such as row versions, ETags or updated_at, and give it a short expiry.
  • Put the summary first: counts, groupings and anything unusual, then the detail. Anomalies should be visible without scrolling.
  • Enforce approval rules inside apply_plan, keyed on plan contents. A rule written only in the prompt is a suggestion.
  • Make apply idempotent on plan_id, so a retried call cannot run the change twice.
  • Log plan and apply as one record, so an audit shows what was proposed, who approved it, and what actually changed.
If a tool's write cannot be undone cheaply, make the agent produce a stored, versioned plan first and let apply execute only that plan.