Approved once. Runs once. Even after crashes.
Tested approval templates for OpenAI Agents SDK, LangGraph, Google ADK and Vercel AI SDK. Pause, approve, resume, no repeats.
01 / Method
- 01
Tested on every new release
Every template runs on a clean box and reruns when its framework version changes. The run output and tested version sit beside the code.
- 02
We say what is mocked
Usually just the LLM, so you can run it without an API key. Everything else is the real SDK.
- 03
The traps, written down
Deprecated options, wrong call ids, side effects that run twice: noted per version.
02 / The approval loop
Pause. Persist. Ask a human. Resume.
The same four-beat loop sits under every approval template, whatever the framework calls it: interruptions in the OpenAI Agents SDK, interrupt() in LangGraph, tool confirmation in Google ADK.
03 / Inside a template
Code, run log, and the traps, in one place.
The OpenAI Agents SDK starter pauses risky tools for approval, checkpoints the run to a string, and resumes after a human approves or rejects. It also shows the new run-wide tool_error_formatter scrubbing secrets from crashed tools.
"""Human-in-the-loop: needs_approval -> interruption -> serialize RunState -> restore -> approve/reject -> resume."""
import asyncio
from agents import Agent, Runner, RunConfig, RunState, function_tool
from fake_model import ScriptedModel
@function_tool(needs_approval=True)
def delete_file(path: str) -> str:
"""Delete a file."""
return f"deleted {path}"
@function_tool(needs_approval=True)
def send_email(to: str) -> str:
"""Send an email."""
return f"emailed {to}"
def make_agent():
return Agent(name="Ops", instructions="ops",
model=ScriptedModel([("delete_file", {"path": "/tmp/x"}), ("send_email", {"to": "a@b.c"})]),
tools=[delete_file, send_email])
async def main():
cfg = RunConfig(tracing_disabled=True)
agent = make_agent()
r = await Runner.run(agent, "clean up and notify", run_config=cfg)
print("interruptions:", [(i.tool_name, i.arguments) for i in r.interruptions])
saved = r.to_state().to_string() # durable checkpoint (e.g. DB)### 02_approval_resume.py
interruptions: [('delete_file', '{"path": "/tmp/x"}'), ('send_email', '{"to": "a@b.c"}')]
serialized state: 12612 chars
interruptions after resume: []
final: Model saw tool outputs: call_0=deleted /tmp/x | call_1=Human declined sending email.04 / The library
The library
All templates →Pinned framework versions. "Coming soon" means not tested yet, and we label it that way.
Approval + resume & tool error formatting
Pause tool calls for human approval, checkpoint the run to a string, approve/reject, and resume. Plus the new run-wide tool_error_formatter that scrubs secrets from crashed tools.
Human-in-the-loop approval graph
A propose → approval → execute StateGraph that pauses with interrupt(), saves the pause in a checkpointer under a thread_id, and resumes with Command(resume=...) down the approve or reject path.
Idempotent approval: no double charges on resume
When a run resumes after interrupt(), LangGraph re-runs the paused node from the top. This template shows the double-charge bug and the fix: approval-only node, separate execute node, and an idempotency key derived from thread_id.
Idempotent approved side effects
The Agents SDK runs an approved tool once per resume, but side effects still repeat on a duplicate resume of the same RunState or a whole-run retry after a crash. Shows both failures (2 charges) and the fix (1 charge).
Multiple interrupts in one node
One node calls interrupt() three times and is answered one resume at a time (matched by position), plus two parallel nodes that interrupt in the same step and are resumed together with a dict keyed by Interrupt.id.
Minimal web UI to approve paused runs
A stdlib-only web page (plain HTML forms, no JavaScript) that lists paused LangGraph runs and approves or rejects them with Command(resume=...), with 409/400/404 handling.
What broke in agent frameworks this week.
Every Monday: releases that matter, what to skip, and one starter you can run today. Free.
Get the Monday email ↓
05 / Guides
Guides
LangGraph interrupt resumes twice: why the node re-runs and how to fix it
Why a LangGraph node re-runs and side effects fire twice when you resume with Command(resume=...), and three tested fixes. langgraph 1.2.15.
LangGraph multiple interrupts: how to resume each one correctly
Resume several LangGraph interrupts in one node or across parallel nodes with Command(resume=...), and avoid replay bugs. Tested on langgraph 1.2.15.
LangGraph vs CrewAI: Which Agent Framework to Use
LangGraph vs CrewAI for engineers: control flow, state, persistence, human-in-the-loop, and memory, with sources from the official docs.
AI Agent Memory: Types, Architecture and Tools
Working, episodic, semantic and procedural memory for AI agents: what each is, how to build it, and how LangGraph and CrewAI implement it.