If you want the fewest lines of code, use the Vercel AI SDK: one needsApproval: true on a tool and one hook call. If you want the pause to survive a redeploy without extra work, use LangGraph, because its interrupt is written to a checkpointer by default. If you want an explicit, serializable state object you can stash in your own database and resume from anywhere, use the OpenAI Agents SDK. All three ship this as a first-class feature now — so the real decision isn't the API. It's where the pause lives.

We already covered why tool approval became a framework default. This is the code companion: the actual minimal gate in each, then the one trade-off that decides your choice.

LangGraph: middleware + a checkpointer#

LangChain's agents wrap approval in HumanInTheLoopMiddleware. You list which tools pause; the interrupt is persisted to the checkpointer, so resumption is a first-class operation.

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command

agent = create_agent(
    model="openai:gpt-5",
    tools=[send_email, read_data],
    middleware=[HumanInTheLoopMiddleware(interrupt_on={"send_email": True})],
    checkpointer=InMemorySaver(),  # swap for Postgres/SQLite in prod
)

config = {"configurable": {"thread_id": "abc"}}
result = agent.invoke({"messages": [...]}, config)   # pauses on send_email
# ...human reviews...
agent.invoke(Command(resume=[{"type": "approve"}]), config)  # continues

The decision can be approve, edit, reject, or respond. Swap InMemorySaver for a Postgres checkpointer and the pause outlives your process for free — the same thread_id picks up exactly where it stopped.

Vercel AI SDK: one flag on the tool#

The AI SDK collapses the whole thing into a tool property. Mark the tool, keep its execute, and the SDK stops before running it.

import { tool } from 'ai';
import { z } from 'zod';

const deploy = tool({
  description: 'Deploy to an environment',
  inputSchema: z.object({ target: z.string() }),
  needsApproval: true,               // or: ({ input }) => input.target === 'production'
  execute: async ({ target }) => runDeploy(target),
});

On the frontend, the tool part enters the approval-requested state; you answer with addToolApprovalResponse from useChat, and the agent loop resumes automatically. Deny it and the model receives the denial and can react. It is the least ceremony of the three — but by default the pending approval lives in the message stream, in memory. AI SDK 7 adds @ai-sdk/workflow and WorkflowAgent for runs that survive a restart; without it, a deploy mid-approval starts over.

OpenAI Agents SDK: interruptions and a RunState#

The OpenAI SDK surfaces pauses as interruptions and hands you a RunState you own.

from agents import Agent, Runner, function_tool

@function_tool(needs_approval=True)   # or an async fn deciding per call
def transfer_funds(amount: int) -> str:
    return do_transfer(amount)

result = await Runner.run(agent, "wire $5000")
if result.interruptions:              # list of ToolApprovalItem
    state = result.to_state()
    for item in result.interruptions:
        state.approve(item)           # or state.reject(item)
    result = await Runner.run(agent, state)   # resume

The unlock is state.to_json() / RunState.from_json(...): serialize to a database, restore when the approver responds hours later, resume. The JS SDK mirrors this with result.interruptions, state.approve(interruption), RunState.fromString, and run(agent, state). For MCP servers, approval is configured with require_approval on the server rather than per tool.

The choice that actually matters#

Ergonomically it's a near-tie. The real fork is durability. LangGraph is durable by default the moment you attach a real checkpointer. The OpenAI SDK is durable when you serialize its RunState. The Vercel SDK is durable only if you opt into the v7 workflow runtime. This is not a footnote:

An in-memory pause that dies on redeploy is a footgun aimed at exactly the irreversible tools — payments, prod writes, sending mail — that you gated in the first place.

My recommendation: if you're shipping a Next.js product and the gated action is reversible, the Vercel AI SDK is the right amount of code — we walked through that full flow here. But if the action can't be undone and the pause must survive a deploy, reach for LangGraph's checkpointer or serialize OpenAI's RunState to your DB. Don't let least-code decide a question that's really about persistence.

Avoiding approval fatigue: inspect the arguments#

A blanket true trains users to click approve without reading — the security value evaporates. Every framework lets the gate be a predicate over the tool's arguments, so gate by category and inspect the args:

// Vercel AI SDK — auto-run small, pause the dangerous shape
needsApproval: ({ input }) =>
  input.category === 'payment' && input.amount > 1000
  || input.target === 'production',

LangGraph expresses the same idea with a when predicate that receives a ToolCallRequest (conditional interrupts require langchain>=1.3.3); the OpenAI SDK accepts an async function in place of needs_approval=True. The pattern is identical everywhere: routine calls flow through, and only the risky ones surface — which is the whole point of a human in the loop.