Skip to main content
>_ supraj.dev

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:

  1. LangGraph surfaces the interrupt payload to the caller.
  2. graph state is preserved through the configured checkpointer;
  3. execution pauses;
  4. the application can return control to the user/UI;
  5. 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

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.