/pattern/hitl-gate/

04 · ProductionGovernance & SafetyHITLHuman ApprovalManual Review Gate

HITL Gate.
Pause before anything irreversible.

Critical steps are reviewed and approved by a human before execution. The system suspends, persists state, poses a structured question, and resumes once the human replies. Best framed as graduated autonomy — oversight set per action class by stakes (full automation for low-stakes, supervised for moderate, human-led for high-stakes) rather than a single on/off gate.

When to reach for it

  • Actions have irreversible or expensive consequences.
  • Compliance mandates human decisions.
  • Model uncertainty must be surfaced.

When it backfires

  • Low-risk actions can run fully automated.
  • Approvals are merely symbolic.
  • Latency requirements preclude manual checks.

The tradeoff

Higher control is achieved against slower operational workflows.

The mental model

A shape you can draw on a napkin.

Before any irreversible action, the agent pauses on a gate. A human inspects, approves, or rewrites.

  1. The run pauses at a defined node and its state is persisted.
  2. A human answers a structured question — approve, edit, or reject.
  3. The run resumes exactly where it stopped — on top of any other pattern.

Compare all six coordination patterns

pauseresumeAgentHumanExecute
Walk it through

A real run, step by step.

Task"Apply a refund to a flagged customer order."
1 / 5
Draftagent(order) → { refundAmount: 249.00, reason: 'duplicate_charge', method: 'original_payment' }
PauseState suspends at interrupt. Human receives: 'Approve refund of $249.00 to original payment?'
InspectHuman reviews payload. Changes amount to $199.00 and approves.
ResumeCommand(resume={ approved: true, refundAmount: 199.00 }) → graph continues
ExecuteRefund of $199.00 processed. Original $249.00 draft never touched production.
In code

The loop is already built in.

LangGraphpython
from langgraph.graph import StateGraph, END
from langgraph.types import interrupt, Command

class State(TypedDict):
    order: dict
    refund: dict | None
    approved: bool | None

def draft_refund(state):
    return {"refund": refund_llm(state["order"])}

def human_gate(state):
    # Suspends execution; resumes when human replies
    return interrupt({
        "question": "Approve refund?",
        "payload": state["refund"],
    })

def execute_refund(state):
    if state["approved"]:
        process_refund(state["refund"])
    return state

g = StateGraph(State)
g.add_node("draft", draft_refund)
g.add_node("gate", human_gate)
g.add_node("execute", execute_refund)
g.add_edge("draft", "gate")
g.add_edge("gate", "execute")
g.add_edge("execute", END)
g.set_entry_point("draft")

# Resume with: graph.invoke(None, config, stream_mode="values")
# after human calls Command(resume={"approved": True, ...})
hitl = g.compile()
Pitfalls

Two ways this pattern will hurt you.

Gate placed too early — humans drown in approvals that don't matter

Every low-risk action triggers a gate. Humans develop approval fatigue and start clicking 'approve' without reading.

Fix · Gate only on irreversible or high-cost actions. Use a risk classifier to auto-approve low-risk operations.

Approval payload omits the diff the human needs

The gate shows the final state but not what changed. The human can't tell if the refund amount is correct.

Fix · Include a clear diff in the interrupt payload: 'Proposed change: $249.00 → $199.00. Reason: duplicate charge.'

Framework support

Where HITL Gate is native.

LangGraphinterrupt() / Command(resume=...) primitivesNative
Microsoft Agent FrameworkNative
CrewAINative

Search

Search patterns, frameworks, and pages.