Skip to content
Blog

Interruptible Agents: Mid-Task Corrections from Users

Patterns for building interruptible agents that support mid-task corrections — state preservation, approval gates, and human-in-the-loop UX best practices.

Published on September 9, 2026

AI Assistant

Interruptible Agents: Mid-Task Corrections from Users

The agent goes off-track and you can’t stop it. It’s burning API credits, modifying files, or sending emails you never authorized. Without an interruption surface, autonomous loops produce a “let it finish or lose everything” experience that destroys user trust.

Why HITL Is a State Problem, Not a UI Problem

“Human-in-the-loop is not a UI feature. It is a state-persistence problem wearing a UI costume.” The hard problem isn’t the approval button — it’s that the agent must pause for seconds-to-days and resume the exact execution state: which tool, which arguments, which step in a multi-call plan.

Five Interruption Trigger Patterns

PatternDescriptionBest For
Action allowlist/blocklistCertain tool calls always gatedSimple, static policies
Risk scoreEach action scored for riskFlexible, needs calibration
Confidence thresholdAgent self-reports confidenceCheap, crude
Critical path checkpointsFixed gates at milestonesMost predictable for users
User-driven pauseUser can interject at any timeMaximum control

LangGraph’s interrupt() Pattern

from langgraph.types import interrupt

def approve_node(state):
    # Pause and surface value to client
    approval = interrupt({
        "action": "send_email",
        "to": state["recipient"],
        "subject": state["subject"],
        "preview": state["body"][:200],
    })
    
    if approval == "approved":
        return send_email(state)
    return {"status": "cancelled"}

Resume with Command(resume=...). Key gotcha: resume re-executes the entire node from the top, so interrupt() must be at the top or side effects must be idempotent.

OpenAI Agents SDK Approval Flow

from agents import Agent, tool

@tool(needs_approval=True)
async def cancel_order(order_id: int) -> str:
    return f"Cancelled order {order_id}"

# Dynamic approval rules
async def requires_review(_ctx, params, _call_id) -> bool:
    return "refund" in params.get("subject", "").lower()

@tool(needs_approval=requires_review)
async def send_email(subject: str, body: str) -> str:
    return f"Sent '{subject}'"

The flow: Model emits tool call → runner evaluates needs_approval → if required, execution pauses → RunResult.interruptions contains ToolApprovalItem → convert to RunState → approve/reject → resume.

Serialization for Long-Running Approvals

# Serialize state for durable storage
state_json = result.to_state().to_json()

# Store in database/queue
await db.store(f"approval:{run_id}", state_json)

# Later: recreate and resume
from agents import RunState
state = RunState.from_json(agent, stored_json)
await Runner.run(agent, state)

State Preservation: Checkpointing

Critical checkpoint fields:

FieldPurpose
runIdPrevents two workers resuming the same run
stateVersionRejects or migrates old checkpoints
statusRunning, paused, approval, cancellation, failure
nextStepFirst unconfirmed transition
pendingApprovalWhat was approved/rejected
toolReceiptsObserved results and external IDs
updatedAtDetects abandoned ownership

Two approaches:

  • Deterministic replay (Temporal/Inngest) — State = inputs + log of side-effects; re-execute, skip side-effects with logged results
  • Checkpoint snapshots (LangGraph) — Periodically serialize plan, working memory, partial outputs; restore on restart

Three Gate Patterns

Propose-then-confirm — Agent produces concrete action → human approves before execution. Best for high-stakes one-offs.

Dry-run/preview — Agent computes full effect without committing → human sees ground truth. Strictly better than propose-then-confirm when effect is computable.

Auto-approve allowlist — Actions proven safe through repetition run without prompt. Start strict, widen as evidence accumulates.

UI/UX: What to Show

The approval screen must answer three questions:

  1. What the agent is about to do (concrete action + arguments)
  2. Why (agent’s reasoning or previous steps)
  3. What could go wrong (failure modes)

Risk-based UI:

  • Low-risk: Single approve button
  • Medium-risk: Show objects + reversibility
  • High-risk: Explicit confirmation with consequences
  • Irreversible: Typed confirmation

Avoid over-gating: “If the agent asks approval on every search or file read, oversight becomes unbearable and the person approves without reading.”

Progressive Delegation

Start with conservative autonomy, expand as users build trust. Capture per-user approval patterns and persist them. New contexts start conservative; users who consistently approve a category get it moved to auto-approve.

Framework Comparison

FrameworkHITL PrimitiveWhere State Lives
OpenAI Agents SDKneeds_approval + RunResult.interruptionsRunState.to_json()
LangGraphinterrupt() + Command(resume=...)Checkpointer (required)
PydanticAIrequires_approval + DeferredToolRequestsDeferred-tool results
TemporalSignal + wait_conditionDurable Event History

The Takeaway

Interruptible agents require solving state persistence first. Use interrupt() or needs_approval for the pause mechanism, checkpoint your state durably, and build approval UIs that answer what/why/what-could-go-wrong. Start with a blocklist approach and graduate to risk-score-based gating as you accumulate production data.

💡 Treat every agent action as a potential interruption point — users should always be able to say “stop” and get a clean state.