Human-in-the-Loop Approvals: Pausing the Agent Is the Easy Part Approval gates in agent frameworks such as the OpenAI Agents SDK for TypeScript and Vercel's AI SDK, which exposes a `needsApproval` option on tools, pause a run and return `interruptions` plus a resumable `state` that a developer approves or rejects before passing the same state back to `run`, but the article argues the pause is the easy part of human-in-the-loop review. The piece recommends rendering the actual tool name and arguments with an impact string computed by application code rather than the model's own explanation, since a prompt showing "The agent wants to cancel an order. Approve?" forty times a day trains users to click Approve without reading. It also advises gating actions by blast radius — logging reversible, cheap actions without a gate, gating reversible-but-visible actions only above a threshold, and always gating irreversible or external actions such as money movement and deletes — with that policy held in code rather than the system prompt. Every agent framework now ships an approval gate. Mark a tool as sensitive, and when the model tries to call it, the run pauses and waits for a person to say yes or no. It takes one line to turn on, and it's easy to come away thinking the safety question is settled. It isn't. An approval prompt that shows "The agent wants to cancel an order. Approve?" forty times a day trains your users to click Approve without reading, which is worse than no prompt, because now there's a record saying a human signed off. The pause is the easy part. The hard parts are deciding what gets gated, what the reviewer actually sees, and what happens between "approved" and "executed." The mechanics take five minutes Here's the OpenAI Agents SDK for TypeScript. Vercel's AI SDK has the same idea through a needsApproval option on tools, so the pattern carries over. js import { Agent, run, tool } from "@openai/agents"; import { z } from "zod"; const cancelOrder = tool { name: "cancel order", description: "Cancel a customer order.", parameters: z.object { orderId: z.number } , needsApproval: true, async execute { orderId } { return Cancelled order ${orderId} ; }, } ; const agent = new Agent { name: "Support agent", instructions: "Use cancel order to handle cancellation requests.", tools: cancelOrder , } ; let result = await run agent, "Cancel order 123." ; if result.interruptions?.length { const state = result.state; for const interruption of result.interruptions { // In a real app, this is where a person or a policy decides. state.approve interruption ; } result = await run agent, state ; } When the model calls cancel order , the tool doesn't execute. The run returns interruptions and a resumable state . You approve or reject each item, then pass the same state back to run to continue the same run instead of starting a new turn. If the review could take hours, the docs say to serialize the state, store it, and resume when the decision arrives. needsApproval can also be a function that returns a boolean, so you can gate only some calls. That loop is where most tutorials stop. Here's what it leaves out. Show the arguments, not the model's story The most tempting approval UI is the model's own explanation: "I'm going to refund the customer because the item arrived damaged." It reads nicely, and you should never rely on it. That sentence was written by the same model whose action you're checking. If an attacker slipped instructions in through a support ticket or a retrieved web page, the explanation will be just as convincing as the action is wrong. Render the actual tool name and the actual arguments, resolved into something a person can check: type ApprovalCard = { tool: string; args: Record