Skip to main content

Tools a human has to approve

Mark the tools an agent should not use unattended. The run parks on a reviewer before the tool runs, holding no worker while it waits:
requiresApproval: true raises the gate and says nothing else about it. Say more with approval, and the same annotations a hand-written ctx.step.approval takes apply to the tool’s gate:
Declare a risk on anything you would not want cleared by a credential. A gate that states no risk is stored at the default, medium, and the human-decision floor reads the stored risk - so a floor of high cannot refuse a gate that never claimed to be high.
The reviewer sees the input the model proposed and can change it before approving - the tool then runs with their input, not the model’s. A denial is not an error: it comes back to the model as that tool’s result, so the agent can say what it could not do instead of the run failing. Each gate is its own durable step, so resuming the run replays the agent without asking anyone twice. Both the approval and the tool call show up under their turn in the console, so a parked agent reads as parked rather than as a tool that never finished.

Gating some calls and not others

requiresApproval is one bit for the whole tool, so a refund tool marked true wakes someone for a 2refundandonemarked‘false‘wakesnobodyfora2 refund and one marked `false` wakes nobody for a 50,000 one. approval.if and approval.minRisk are the third answer: a condition on this call, written as data and evaluated by Duraton.
The expression reads four names and no others - tool, args, risk and iteration - and every JSON number in args reaches it as a double. A rule that cannot be evaluated raises the gate rather than skipping it, and one that declines completes the step as decidedBy: "system:rule" with no approval for anyone to answer. The full environment, the failure modes and when this beats a hand-written if are on Approvals.

Which rule applies to a tool

A rule can be declared in two places: on the tool, and on the agent as the default for every tool that has not answered for itself.
The two levels resolve in one order, and requiresApproval is tri-state on purpose - absent, true and false are three different answers rather than a boolean with a default: Two rows are worth reading twice. requiresApproval: true on a tool the agent has a default for takes that default’s annotations rather than erasing them: true asks for a gate, it does not claim there is nothing to say about one. And a tool’s own approval replaces the default rather than merging with it - a rule is one policy, so a tool that states risk and no timeout has no timeout, whatever the default said.
A tool with no opinion at all inherits the default. That is the point: requiresApproval: false is how a tool says it never waits for anyone, and saying nothing is not the same as saying that.
The default is also what covers an MCP server’s tools that the server’s own predicate says nothing about. Those tools are attached here and discovered at runtime, so there is no declaration above to annotate and no list to enumerate ahead of time.

Capping the decisions an agent asks for

maxApprovals is a ceiling on how many human decisions one agent may ask for. When a turn’s tool calls would take it past the ceiling, the agent stops instead of running them, and returns stopReason: "approval-budget":
Omitted, there is no ceiling. Set one where an agent could plausibly keep reaching for a gated tool: maxIterations bounds how long it runs, and this bounds how much of a person’s attention it can spend doing so. Handle the stop like any other terminal reason - the agent returns rather than throwing, so the workflow decides what a run that ran out of sign-off does next.

Showing the model less than you record

A tool that returns a thousand rows costs a thousand rows of context on every turn after it. Give it a toModelOutput and the model reads the summary while the durable step keeps the whole thing:
The projection is not a truncation you can never undo. The tool’s step still holds all thousand orders, so the run’s history, a replay, and anything reading the timeline all see the full result - only the model’s context was spent on the summary.
toModelOutput must be pure. A replay re-projects the result the tool already returned rather than calling the tool again, so a projection that reads the clock or a counter makes a replayed turn disagree with the original.
It runs only on a result your tool actually produced. A denial from an approval gate is the loop telling the model about its own gate, so it reaches the model unprojected.

Checking the model’s tool arguments

The arguments a tool is called with are written by the model. Pass a guardrail and they are checked against the tool’s own inputSchema before the handler ever sees them; a refusal comes back to the model as that tool’s result, so it can correct itself.
The verdict is its own durable step, so a replay reads what was decided instead of deciding again. Guardrails covers the placements, the actions, and how to write your own.