Module 4: State & Graph Workflows · 5.25h
01 · UNDERSTAND
Day 22 Theory — Human-in-the-Loop, Interrupts and Idempotent Side Effects
Why humans belong inside some workflows
Autonomy is not the objective. Correct, safe task completion is.
Some actions should pause for a human because they are high impact, ambiguous, policy-sensitive, or legally significant.
Human-in-the-loop (HITL) means the workflow can deliberately stop, expose context to a person, receive a decision, and resume.
Interrupt as a workflow state
An interrupt is not a crash.
It is an intentional pause.
agent -> proposes sensitive action
↓
INTERRUPT
↓
human review
├─ approve -> execute
└─ reject -> alternate path
The workflow state must survive while waiting.
What the reviewer should see
A useful approval request includes the information needed to make a decision:
- proposed action,
- target resource,
- reason/context,
- expected effect,
- important evidence,
- relevant policy.
Do not ask humans to approve opaque actions such as “Agent wants to continue.”
Approval is authorization context, not model text
A model saying “approved” must not satisfy a human approval gate.
The runtime should receive a trusted approval event tied to an authenticated reviewer and workflow instance.
Resume safely
On resume, validate that:
- the approval belongs to this workflow,
- the action has not already been executed,
- the target is still valid,
- the workflow has not expired,
- the approver still has required permissions if policy requires re-checking.
Idempotent side effects
HITL workflows are long-lived and therefore especially exposed to retries and duplicate resumes.
Use operation IDs or idempotency keys so “approve” events cannot accidentally execute the same write twice.
Rejection is a first-class path
A rejected request should not become an exception that leaves the workflow confused.
Model the alternate path explicitly: explain the rejection, request more information, or escalate.
Service Desk connection
Today we place a real safety boundary around actions such as privileged resets or high-impact changes.
The principle is:
Human approval is an authenticated runtime event tied to a specific proposed action, and resuming after approval must be safe against duplicate execution.
02 · APPLY
Lesson goal
Today we make a stateful agent pause safely for a human decision and resume later without repeating a side effect.
By the end of the lesson you should be able to:
- explain why human approval is a workflow state rather than a blocking
input()call, - use LangGraph's
interrupt(...)mental model for pause/resume, - explain why durable checkpointing is required for long-lived approvals,
- resume a workflow with
Command(resume=...), - distinguish approve, reject and edit decisions,
- design idempotency around side effects that may be replayed,
- decide which actions should require approval,
- record an audit trail without exposing private chain-of-thought.
What changed in the Service Desk today?
Yesterday we learned how durable state lets a workflow survive a pause or failure. Today we use that capability for human approval.
A high-risk Service Desk action should no longer be:
model decides -> execute immediately
It becomes:
model proposes action
|
v
risk/policy check
|
+-- low risk ------> execute
|
+-- approval ------> interrupt
|
v
human decision
|
v
resume
|
v
execute exactly once
The human is part of the workflow, but the worker process does not need to remain blocked while that human is thinking.
Why input() is not production HITL
A beginner implementation might do this:
approved = input("Approve password reset? y/n: ")
That works in a local script, but it couples the workflow to one live process and terminal.
What happens if:
- the approval arrives three hours later,
- the process restarts,
- approval comes from a web UI,
- another worker receives the resumed request,
- thousands of workflows are waiting at once?
Production HITL needs durable pause/resume state, not a blocked OS thread.
The LangGraph interrupt mental model
In current LangGraph, a node can call interrupt(...) when external input is required.
Conceptually:
from langgraph.types import interrupt
def approval_node(state):
decision = interrupt({
"action": state["proposed_action"],
"reason": state["risk_reason"],
})
return {"approval_decision": decision}
When execution reaches the interrupt:
- LangGraph surfaces the interrupt payload to the caller.
- graph state is preserved through the configured checkpointer;
- execution pauses;
- the application can return control to the user/UI;
- later, the graph resumes with human input.
The interrupt is not a sleep loop and not a permanent network connection.
Checkpointing is what makes the pause durable
An interrupt without durable state would be fragile.
The workflow needs enough persisted state to answer:
- which ticket is waiting,
- which node paused,
- what action was proposed,
- which thread/workflow ID should resume,
- what has already executed.
A typical graph is compiled with a checkpointer and invoked with a stable thread_id or equivalent workflow identity supported by the current LangGraph configuration.
Conceptually:
invoke graph with thread_id = ticket-INC-42
|
v
interrupt
|
v
checkpoint store preserves state
|
hours later
|
v
resume same workflow identity
Do not confuse a user ID with a workflow/thread ID. One user can have many independent workflows.
Resuming with a human decision
Current LangGraph HITL flows resume interrupted work using Command(resume=...).
Conceptually:
from langgraph.types import Command
result = graph.invoke(
Command(resume={
"decision": "approve",
"reviewer": "manager-17",
}),
config=config,
)
The exact resume payload is an application contract. Define it explicitly rather than passing an unstructured string through the graph.
For example:
class ApprovalDecision(TypedDict):
decision: Literal["approve", "reject", "edit"]
reviewer: str
note: str
edited_arguments: dict | None
Approve, reject and edit are different outcomes
A useful approval gate supports more than True/False.
Approve
Execute the proposed action as reviewed.
Reject
Do not execute the side effect. Continue to a safe final state such as:
"The requested action was not approved."
Edit
A reviewer may change arguments before execution.
Example:
Proposed:
reset user account AND revoke all sessions
Reviewer edits:
reset account only
The edited action must be revalidated and re-authorized. Human input is trusted only according to the reviewer's actual permissions.
The most important replay problem
Many graph runtimes resume by re-entering code around the point that interrupted.
This creates a dangerous question:
What code ran before the interrupt, and can it safely run again?
Consider this bad node:
def risky_node(state):
ticket_api.add_comment("Reset requested") # side effect
decision = interrupt("Approve reset?")
if decision == "approve":
reset_password()
If the node is replayed around resumption, the comment may be written more than once.
The safe design principle is:
Do not perform a non-idempotent side effect before a pause unless replay is explicitly safe.
Prefer separating proposal/approval from execution.
Safer graph structure
propose_action
|
v
approval_gate --interrupt--> human
|
v
execute_action
|
v
record_result
Now the node that produces the external mutation is downstream of the approval decision.
Even then, idempotency is still important because retries, crashes or uncertain responses can cause re-execution.
Idempotency in plain English
An operation is idempotent when repeating the same intended operation does not create an additional unintended effect.
For a password-reset workflow, we might assign an idempotency key such as:
reset:INC-42:user-123:proposal-v2
Before executing:
Have we already completed this idempotency key?
|
+-- yes -> return recorded result
|
+-- no -> execute -> persist result atomically
The key must represent the business operation, not merely one HTTP request attempt.
Why “check then execute” can still race
This is not enough under concurrency:
if not store.exists(key):
perform_side_effect()
store.save(key)
Two workers can both observe “not exists” and both execute.
Real idempotency needs an atomic claim/write strategy appropriate to the storage system or downstream API.
Possible approaches include:
- a database uniqueness constraint,
- transactional insert/lock,
- provider-supported idempotency keys,
- durable execution record with atomic state transition.
The exact mechanism depends on the side effect.
Approval does not replace authorization
A manager clicking “Approve” does not automatically make an action valid.
Before execution, still check:
requester authenticated?
reviewer authenticated?
reviewer allowed to approve this risk class?
target belongs to correct tenant?
action arguments still valid?
policy still allows it now?
Approval is one policy input. Authorization remains deterministic application logic.
Risk-tiered HITL
Requiring a human for every agent action creates approval fatigue and destroys the value of automation.
A practical model may classify actions by risk.
Example:
| Action | Example policy |
|---|---|
| Read ticket status | automatic |
| Search approved KB | automatic |
| Restart low-risk sandbox service | policy dependent |
| Reset another user's credential | approval required |
| Delete production data | strong approval / possibly prohibited |
The exact tiers are organization-specific. Do not copy these examples as universal policy.
The design goal is:
Humans review actions where human judgment or accountability meaningfully reduces risk.
Audit trail
A strong HITL record should make the decision reconstructable later.
Useful fields can include:
workflow/thread ID
proposal ID
requested action
validated arguments
risk classification
requester identity
reviewer identity
decision
review note
edited arguments if any
idempotency key
execution result
timestamps
Do not store private model chain-of-thought as an “audit trail.” Store observable proposals, policy decisions and execution evidence.
Failure scenarios
Scenario 1 — process restarts while waiting
Expected behavior:
- persisted checkpoint survives,
- no worker thread has to remain alive,
- workflow can resume using the same durable identity.
Scenario 2 — user clicks Approve twice
Expected behavior:
- same business operation is not executed twice,
- duplicate resume/action requests are safely detected or deduplicated.
Scenario 3 — approval arrives after policy changes
Expected behavior:
- execution re-checks current authorization/policy where required,
- old approval does not blindly bypass a new security rule.
Scenario 4 — reviewer edits arguments
Expected behavior:
- edited arguments are validated,
- authorization is recomputed for the edited action,
- audit trail records what changed.
Practical lab
Work in:
service-desk-day-22/
The repository's Day 22 goal is to demonstrate:
- interrupt/pause-resume lifecycle,
- approve/reject/edit decisions,
- risk-tiered approval,
- idempotent side-effect protection,
- audit evidence.
Task A — trace the approval lifecycle
Starting from a high-risk proposed action, write the lifecycle as states:
PROPOSED -> WAITING_FOR_APPROVAL -> APPROVED/REJECTED -> EXECUTED/STOPPED
Identify which transitions must be persisted.
Task B — inspect the interrupt boundary
Locate the point where the workflow pauses. Verify that the pause occurs before the protected external mutation.
Task C — test resume variants
Cover at least:
- approve,
- reject,
- edit,
- invalid reviewer decision.
Task D — test duplicate resume/execution
Send the same approved business operation twice and assert that the external mutation occurs no more than once.
Task E — run the focused suite
cd service-desk-day-22
PYTHONPATH=. pytest tests/test_hitl.py -v
Do not only record “tests pass.” For the idempotency tests, explain what duplicate event is being simulated and which invariant prevents a second side effect.
Knowledge check
1. Why is input() not a production HITL mechanism?
Because it blocks one live process/terminal and does not provide durable, distributed pause/resume semantics.
2. What is the purpose of interrupt(...)?
It pauses graph execution at an explicit workflow point and surfaces a payload that the application can use to obtain external/human input.
3. What resumes an interrupted LangGraph workflow?
A resume command such as Command(resume=...) is sent using the same durable workflow configuration/thread identity.
4. Why must side effects be designed for replay?
Crashes, retries or resume semantics can cause execution code to be encountered again. Without idempotency, a ticket update, notification or reset can happen more than once.
5. Does approval imply authorization?
No. The reviewer and requester still need deterministic authorization checks appropriate to the action and current policy.
Scenario
The Service Desk proposes disabling an employee account. The manager approves it. The account API processes the request successfully, but the worker crashes before storing the final graph state. On recovery the execution node runs again.
What prevents two disable operations or duplicate downstream notifications?
Answer: The business action must use durable idempotency/deduplication. The repeated execution should resolve to the already-recorded outcome rather than creating another side effect. Human approval alone does not solve replay safety.
Key takeaways
- HITL is a durable workflow state, not a blocking prompt.
interrupt(...)represents the pause;Command(resume=...)carries the external decision back into the workflow.- A durable checkpointer/workflow identity allows approvals to outlive a process.
- Put irreversible effects after the approval boundary where possible.
- Idempotency is required because approved work can still be retried or replayed.
- Approval complements authorization; it does not replace it.
- Audit observable proposals, decisions and effects—not private reasoning.
Official references
- LangGraph Interrupts / Human-in-the-Loop: https://docs.langchain.com/oss/python/langgraph/interrupts
- LangGraph Persistence: https://docs.langchain.com/oss/python/langgraph/persistence
- Stripe Idempotent Requests (worked industry pattern): https://docs.stripe.com/api/idempotent_requests
03 · EXPLAIN
Interview checkpoint
Explain one design decision from this lesson, the alternative you rejected, and the failure mode or evidence that justified your choice.