TAI BUI
Back to curriculum
LearnPython

Human-in-the-Loop: Propose-Then-Commit

The 2026 consensus on HITL is specific, and it is not "the agent asks, the user clicks Approve." It is propose-then-commit. The proposed action is persisted to a durable store with an idempotency key, then surfaced to a reviewer with intent, data lineage, permissions touched, blast radius, and a rollback plan. It is committed only after positive acknowledgement, and verified after execution to confirm the side effect happened. LangGraph's `interrupt()` plus PostgreSQL checkpointing, Microsoft Agent Framework's `RequestInfoEvent`, and Cloudflare's `waitForApproval()` all implement the same shape. The canonical failure mode is the rubber-stamp approval, where "Approve?" gets clicked without review. The documented mitigation is challenge-and-response with an explicit checklist.

TIME: ~60 minutesPREREQUISITES: Phase 15 · 12 (Durable execution), Phase 15 · 14 (Tripwires)

Learning Objectives

  • Describe the four phases of propose-then-commit (propose, surface, commit, verify) and what each one persists or checks.
  • Explain how an idempotency key stops a retry after a transient failure from double-executing an approved action.
  • Design a challenge-and-response checklist that counters rubber-stamp approval for a specific action.
  • Classify an action as consequential, reversible, or read-only, and decide whether it needs propose-then-commit.
  • Run `main.py` and point out what its toy `verify()` and `rollback` field skip compared with a real system.

The Problem

An agent takes an action and the user has to decide whether to approve it. If the decision is instant, it probably wasn't a review. If the decision is structured, it is slower but you can trust it. The engineering question is how to make the structured review the path of least resistance.

The 2023-era HITL pattern was a synchronous prompt: "Agent wants to send email to X with body Y. Approve?" The user clicks Approve, and everyone feels the system is safe. In practice this surface gets rubber-stamped. Users approve fast, approvals predict little, and when the agent goes wrong the audit trail shows a long history of approvals the user cannot recall.

The 2026 pattern, propose-then-commit, moves HITL onto a durable substrate, attaches structured metadata, and requires a positive commit. Every managed agent SDK ships a version: LangGraph interrupt(), Microsoft Agent Framework RequestInfoEvent, Cloudflare waitForApproval(). The API names differ and the shape stays the same.

Warm-up0/2
1. A durable-execution workflow pauses to wait for an external event and the worker process restarts before it arrives. What lets the workflow resume where it left off?Warm-up
Three boxes in a row: a workflow runs on a worker process, it pauses to wait for an event, and the worker restarts before the event arrives. A dashed box below asks where the workflow resumes once the event arrives.
Typed schematic, not captured output.
2. A tripwire is configured on an agent run. What is its job?Warm-up
An illustrative single agent run drawn as five steps in a row, with a dashed red marker labelled tripwire configured between step 3 and step 4. A dashed box asks what the tripwire's job is.
Typed schematic, not captured output.
Check: The Problem0/2
1. LangGraph's `interrupt()`, Cloudflare's `waitForApproval()` and Microsoft's `RequestInfoEvent` have different names. What do they share?Define
Three panels side by side, one per framework: LangGraph interrupt(), Cloudflare waitForApproval() and Microsoft RequestInfoEvent. Each has a question mark under what it does. A line asks what the three share.
Typed schematic, not captured output.
2. Why did the 2023-era synchronous "Approve?" prompt fail as a safety control?Explain
A mock 2023-style approval prompt reading Agent wants to send email to X with body Y, Approve?, with Approve and Reject buttons. A line below asks why this failed as a safety control.
Prompt wording quoted from the lesson; UI mock-up, typed.

The Concept

The propose-then-commit state machine

  1. Propose. Agent produces a proposed action. Persisted to a durable store (PostgreSQL, Redis, Durable Object). Includes:
    • intent (why is the agent doing this)
    • data lineage (what source led to this proposal)
    • permissions touched (which scopes / files / endpoints)
    • blast radius (what is the worst case)
    • rollback plan (if committed, how do we undo it)
    • idempotency key (unique per proposal; resubmission returns the same record)
  2. Surface. Reviewer sees the proposal with all metadata. The reviewer is a person, not the agent reviewing itself.
  3. Commit. Positive acknowledgement. The action executes.
  4. Verify. After execution, the side effect is read back and confirmed. If the verify step fails, the system is in a known bad state and alerting engages.

The idempotency key

Without an idempotency key, a retry after a transient failure can double-execute an approved action. Concrete example: user approves "transfer $100 from A to B." Network blips. Workflow retries. The user has approved once but the transfer executes twice. The idempotency key ties the approval to a single, unique side effect; the second execution is a no-op.

This is the same idempotency pattern Stripe and AWS APIs use. Reusing it for agent approvals is explicit in the Microsoft Agent Framework docs.

Durability: why approvals outlast processes

The approval waiting room is a piece of state the agent does not own. The workflow is paused (Lesson 12). When the approval arrives, the workflow resumes from exactly that point. This is why LangGraph pairs interrupt() with PostgreSQL checkpointing and not just in-memory state: an approval two days later still finds the workflow intact.

Rubber-stamp approvals and the challenge-and-response mitigation

The default UI for HITL ("Approve" / "Reject" buttons) produces fast approvals with no genuine review. Documented mitigation: a challenge-and-response checklist that requires positive answers to specific questions before the Approve button is enabled. Concrete shape:

  • "Do you understand what resource this touches? [ ]"
  • "Have you verified the blast radius is acceptable? [ ]"
  • "Do you have a rollback plan if this fails? [ ]"

This isn't bureaucracy for its own sake. It is a forcing function. The reviewer who cannot tick the boxes either asks for clarification (escalation) or declines (safe default). The Anthropic agent-safety research explicitly cites checklist-driven HITL as a mitigation for rubber-stamp approval patterns.

What counts as consequential

Not every action needs propose-then-commit. The 2026 guidance:

  • Consequential actions (always HITL): irreversible writes, financial transactions, outbound communication, production database changes, destructive file-system operations.
  • Reversible actions (sometimes HITL): edits to local files, staging-env changes, reversible writes with clear rollback.
  • Reads and inspections (never HITL): reading a file, listing resources, calling a read-only API.

Post-action verification

"The commit ran" is not the same as "the side effect happened." Network-partition and race conditions can produce a workflow that thinks it succeeded while the backend did not persist. The verify step re-reads the target resource after commit to confirm. This is the same pattern as database transactions with RETURNING clauses or AWS GetObject after PutObject.

EU AI Act Article 14

Article 14 mandates effective human oversight for high-risk AI systems in the EU. "Effective" is not decorative, and the regulatory language specifically excludes rubber-stamp patterns. Propose-then-commit with challenge-and-response is the shape that survives Article 14 scrutiny in the Microsoft Agent Governance Toolkit compliance docs.

Check: The Concept0/23
1. What is propose-then-commit?Define
A card shows an action an agent wants to take: email.send to team@example.com with subject release and 37 recipients. A dashed box below asks how propose-then-commit handles it.
Values come from main.py Demo 1; the card is typed.
2. In a proposal's metadata, what is data lineage?Define
A proposal card for an email.send action showing the intent and the lineage field, with the lineage value shown as a question mark.
Field values from main.py Demo 1; the lineage value is hidden.
3. What does blast radius mean in a proposal?Define
A proposal card for an email.send action to team@example.com showing the intent and the blast radius field, with the blast radius value shown as a question mark.
Field values from main.py Demo 1; the blast radius value is hidden.
4. What is a rubber-stamp approval?Define
A mock approval window titled Agent wants to send an email, showing the recipient, the subject, and two buttons labelled Approve and Reject. A line below says it is the approval screen a reviewer sees.
UI mock-up, typed. Recipient and subject come from main.py Demo 1.
5. What is challenge-and-response in a human approval flow?Define
A reviewer's approval screen with the proposal email.send to team@example.com, a dashed box labelled challenge-and-response step, and an Approve button. A line below asks what that step asks of the reviewer.
Typed schematic, not captured output.
6. What is RequestInfoEvent?Define
A workflow drawn as three boxes: an agent step, a box labelled RequestInfoEvent, and a box where the workflow continues. A dashed box below asks what RequestInfoEvent is in this workflow.
Typed schematic, not captured output.
7. Which of these is NOT one of the proposal metadata fields listed in the Propose step?Define
A panel titled Record written by the Propose step with six slots, field 1 to field 6, each showing a question mark. A line below asks which kind of value is not one of the fields.
Typed schematic, not captured output.
8. What is the purpose of the idempotency key in the propose-then-commit state machine?Define
A stored proposal record with thread t-001, action email.send, a payload with recipient and subject, status waiting, and an idempotency key value, whose purpose is marked with a question mark.
Values taken from a real run of main.py Demo 1.
9. Which of these should NEVER require propose-then-commit HITL?Define
Three empty tier panels labelled always HITL, sometimes HITL and never HITL, each with a question mark. A line asks which kind of action belongs in the never tier.
Typed schematic, not captured output.
10. Which phase writes the proposal to the durable store, together with its metadata and idempotency key?Define
Four unnamed stages in a row labelled stage 1 to stage 4, and below them a durable store panel holding a record, its metadata and an idempotency key, with the stage at which it is written shown as a question mark.
Typed schematic, not captured output.
11. A user approves a $100 transfer once, the network blips, and the transfer runs twice. What lets this happen when no idempotency key exists?Explain
Message sequence between reviewer, workflow and bank. The reviewer approves a $100 transfer from A to B once, the workflow sends the transfer, a network blip occurs, and the workflow sends the transfer to the bank again. The note says it ran twice and asks what allowed it.
Typed schematic, not captured output.
12. Why does LangGraph pair `interrupt()` with PostgreSQL checkpointing rather than in-memory state?Explain
LangGraph interrupt() connected to PostgreSQL checkpointing, with a dashed greyed box below labelled in-memory state, not used. A line asks why interrupt() is paired with PostgreSQL rather than in-memory state.
Typed schematic, not captured output.
13. What does the challenge-and-response checklist mitigate, and how?Explain
A reviewer's approval screen for a db.drop_table proposal with a checklist of three items, each shown as a question mark box, and an Approve button. A line asks what the checklist mitigates and how.
UI mock-up, typed. Payload from main.py Demo 3.
14. Why is "the commit ran" insufficient evidence that the action worked?Explain
A workflow box with the log line commit ran, an arrow to a dashed box for the target resource, marked with a question mark. A line asks why the commit having run is not enough evidence the action worked.
Typed schematic, not captured output.
15. Why would a plain Approve/Reject button struggle to satisfy EU AI Act Article 14?Explain
A default approval screen with the proposal email.send to team@example.com and two buttons, Approve and Reject. A dashed box below asks whether it meets EU AI Act Article 14.
UI mock-up, typed.
16. An agent proposes sending a $5,000 refund. The reviewer approves, the payment API times out, and the workflow retries the commit. Which design choice keeps the customer from being refunded twice?Apply
Four boxes in a row: the agent proposes a $5,000 refund, the reviewer approves once, the commit calls the payment API, and the API call times out. An arrow leads to a box where the workflow retries the commit, and a dashed box asks which design stops a second refund.
Typed schematic, not captured output.
17. An agent run needs three actions: reading a log file, editing a staging-environment config that has a clear rollback, and dropping a production table. Which pairing of action to treatment is right?Apply
Three actions listed as rows: read a log file, edit a staging config that has a clear rollback, and drop a production table. Each has an arrow to a dashed box labelled treatment with a question mark.
Typed schematic, not captured output.
18. A developer adds the current timestamp to the idempotency key hash. After an approved commit, a retry executes the action again. Why?Analyze
A modified key function that adds a timestamp to the hashed fields, followed by two boxes for the first commit attempt and the retry, each with its key shown as a question mark. A note says the approved action executes twice and asks why.
Modified code typed from the stem.
19. Reviewers approve proposals in about two seconds, and later none of them can recall approving a request that dropped a table. The proposals do carry intent and rollback text. Which missing ingredient best explains this?Analyze
A proposal card for db.drop_table with intent and rollback text, a timeline showing about 2 seconds between the proposal being shown and Approve being clicked, a note that nobody later recalls approving it, and a dashed box asking which ingredient is missing.
Card text from main.py Demo 3; the two seconds come from the question.
20. A workflow waiting for approval is gone after the server redeploys overnight, and the reviewer's approval the next day has nothing to resume. What design flaw caused this?Analyze
Three boxes in a row: on day 1 a workflow is waiting for approval, overnight the server redeploys, on day 2 the approval arrives. An arrow leads to a box saying there is nothing to resume, and a dashed box asks which design flaw caused this.
Typed schematic, not captured output.
21. You must let an agent post to a company's public social account. Which review checklist best counters rubber-stamping for this action?Design
A proposal card for posting to a company's public social account, with placeholders for the action and post text, and a reviewer checklist of three boxes each marked with a question mark. A line asks which checklist best counters rubber-stamping.
Typed mock-up with placeholder values.
22. A nightly agent sends invoice emails. Approvals often arrive hours late, and the workflow sometimes retries after network errors. Which design fits?Design
A nightly agent job that sends invoice emails, feeding an approval step that often arrives hours late, then a send step that is retried after network errors. A dashed box asks which design fits.
Typed schematic, not captured output.
23. A coding agent edits files in a local scratch copy that can be reset at any time. Which approval design is defensible?Design
A coding agent box with an arrow to a local scratch copy box noting it can be reset at any time, and a dashed box asking which approval design is defensible.
Typed schematic, not captured output.

Use It

code/main.py implements a propose-then-commit state machine in stdlib Python. The durable store is a JSON file. The idempotency key is a hash of thread_id, action, and payload. The driver simulates three cases: a clean approval flow, a retry after transient failure (which must not double-execute), and a rubber-stamp default versus a challenge-and-response flow.

Check: Use It0/6
1. In `main.py`, the Demo 1 proposal is resubmitted with the same thread_id, action and payload, but a reworded intent string. What does `propose()` do?Apply
Two record cards side by side. The stored one from Demo 1 has thread t-001, action email.send, subject release, to team@example.com and status committed. The resubmitted one has the same fields except the intent, which is reworded. A dashed box asks what propose() does.
Stored card from main.py Demo 1. The reworded intent text is only marked, not drawn.
2. In `main.py`, the Demo 1 proposal is resubmitted with the subject changed to "release v2" and everything else the same. What does `propose()` do?Apply
Two record cards side by side. The stored one from Demo 1 has subject release and status committed. The resubmitted one has the same thread, action, recipient and intent but the subject release v2. A dashed box asks what propose() does.
Stored card from main.py Demo 1.
3. Demo 1 commits the release email once. Demo 2 then calls `commit()` twice more on the same key. How many entries are in `SIDE_EFFECTS` afterwards?Apply
A panel showing SIDE_EFFECTS after Demo 1's commit, holding the email.send entry, then two more commit calls on the same key from Demo 2, and a dashed box asking for the length of SIDE_EFFECTS after them.
SIDE_EFFECTS content from a real run of main.py Demo 1.
4. In Demo 3, `checklist_approve` is called on the drop_table proposal with `rollback_ready=False`, and then `commit()` is called on it. What does `commit()` do?Apply
The drop_table proposal from Demo 3 with its action, payload, blast radius and rollback, followed by the calls checklist_approve with rollback_ready False and then commit. A dashed box asks what commit prints.
Fields from main.py Demo 3. Calls typed from the stem.
5. In `main.py`, what does the `rollback` text on the drop_table proposal actually do?Apply
The drop_table proposal from Demo 3 with its action, payload, blast radius, and the rollback field reading restore from weekly backup; data loss up to 6 days. A line asks what that rollback text actually does in main.py.
Fields from main.py Demo 3.
6. In `main.py`, `verify()` returns True after every commit. If a real backend had silently dropped the write, why would this toy `verify()` still report success?Analyze
Three boxes: commit calls execute and leads to a real backend that silently drops the write and to a verify box that returns True. A dashed box asks why the toy verify still says True.
Typed schematic, not captured output.

Ship It

outputs/skill-hitl-design.md reviews a proposed HITL workflow for propose-then-commit shape and flags missing metadata, idempotency, verification, or challenge-and-response layers.

Author's Note

I'd run main.py before reading the theory twice. The three demos print every phase as it happens, so you can watch a proposal go from waiting to approved to committed and see the second and third commit() calls report that they did nothing. The store is a plain JSON file in a temp directory, so you can print its path and read the records yourself.

The part I'd study is how the key is built. Proposal.key() hashes thread_id, action, and payload, and leaves out the intent, lineage, and rollback text. That is a choice worth thinking about: reword the intent and you still get the same record, change the payload and you get a new one. Exercise 1 pushes on this directly.

I'd also be honest about what the toy skips. verify() checks a Python list that execute() just appended to, so it can never disagree with the commit. In a real system that read has to hit the actual backend, or it proves nothing. The rollback field is only displayed to the reviewer, and nothing runs it. The checklist is three booleans passed as arguments rather than a UI, so Demo 3 shows the gate logic but not whether a person would actually slow down at it.

Exercises

  1. Run code/main.py. Confirm that a retry of an approved proposal uses the durable record and does not re-execute. Now change the idempotency key to include a timestamp and show the retry double-executes.

  2. Extend the proposal record with a rollback field. Simulate an execution whose verify step fails. Show the rollback firing automatically.

  3. Read Microsoft Agent Framework's RequestInfoEvent docs. Identify one metadata field the API includes that the toy engine is missing. Add it and explain what it protects against.

  4. Design a challenge-and-response checklist for a specific action (e.g., "post to a public Twitter account"). What three questions must the reviewer answer? Why those three?

  5. Pick one case where a synchronous "Approve?" prompt would be sufficient (no durable store needed). Explain why, and name the risk class you are accepting.

Key Terms

TermWhat people sayWhat it actually means
Propose-then-commit"Two-phase approval"Persisted proposal + positive commit + verify
Idempotency key"Retry-safe token"Unique per proposal; second execution no-ops
Data lineage"Where it came from"The specific source content that led to the proposal
Blast radius"Worst case"Scope of effect if the action goes wrong
Rubber-stamp"Fast approval""Approve" clicked without genuine review
Challenge-and-response"Forcing checklist"Reviewer must positively acknowledge specific questions
RequestInfoEvent"MS Agent Framework primitive"Durable HITL request with structured metadata
interrupt() / waitForApproval()"Framework primitives"LangGraph / Cloudflare equivalents of the same shape

Further Reading

Code

Ships With