U.S. teaching example: Amounts are fictional USD prices, stored as integer cents. This is a U.S. version of the shop exercise, not a currency conversion or a record of a real charge.
The vanilla tubs that shouldn't ship
Picture a small system that manages ice cream stock for a shop called Lucy's. On Monday, a proposal draft says: order six tubs of vanilla, the shop has two, the target is eight. Lucy reviews the draft, checks the arithmetic, and approves it. That approval gets logged with a timestamp and her name attached.
Now suppose a delivery arrives Tuesday morning, before anyone runs the approved order. The shop now has eight vanilla tubs on hand. The target is still eight. Required quantity: zero. Lucy's approval, however, still says "order six." If the system executes that approval unchanged on Tuesday afternoon, it will over-order six tubs the shop doesn't need, using a decision that was correct on Monday and wrong on Tuesday.
This hypothetical example illustrates a common timing problem in systems where a human approves an AI-proposed action and execution happens later, even by a few seconds. The question this article works through is precise: how do you decide, mechanically, that an approval has expired because the data underneath it changed? The answer requires binding approval to a specific version of the world, not to a general intention, and rechecking that binding immediately before the action fires.
What an approval actually authorizes
An approval is often modeled loosely as a boolean: did a human say yes. That model is too thin. A useful approval record needs at least four fields: the snapshot identifier the proposal was built from, a hash of the draft itself, the identity of the approver, and the specific action the approval authorizes. Call these four together the approval's binding.
The reason for binding to a snapshot ID, not just a timestamp, is that stock levels, prices, and budgets can all move independently of the clock. A snapshot ID lets you ask a sharp question at execution time: is the current state of the world the same state Lucy looked at when she said yes? If the current snapshot ID differs from the one recorded in the approval, this conservative gate treats the approval as stale, even if the changed revision happens to preserve all relevant values.
The approval record must come from an authenticated, trusted approval path; a hash alone does not prove who approved anything. The draft hash matters for a related but distinct reason. Even if the snapshot hasn't moved, you want to be certain the thing about to execute is exactly the thing that was shown to the approver, not a similar-looking draft that was regenerated afterward with a different quantity or a different line item. Comparing hashes catches substitution; comparing snapshot IDs catches staleness. Neither check does the other's job.
The existing teaching example that grounds this idea sits in the ITAM course's business-rules lesson, where a validator checks a proposed order against a trusted inventory snapshot before accepting it, explicitly treating accepted_draft as a validation result rather than a purchase (see /courses/zeo-itam-autumn-2026). That lesson builds the comparison logic for one moment in time. This article extends the same logic across two moments: the moment of approval and the moment of execution, which are not guaranteed to be the same moment at all.
Why a paused workflow does not pause the world
Human-in-the-loop systems commonly implement approval as an interruption: the workflow pauses, waits for a person, then resumes. LangGraph's documentation on interrupts describes this pattern directly, and it makes a specific warning worth sitting with: resuming an interrupted node can rerun code that executed before the interrupt point, so any side effects in that earlier code need to be safe to repeat (LangGraph, "interrupts," https://docs.langchain.com/oss/python/langgraph/interrupts, retrieved 2026-09-10).
That warning is about replay safety inside the orchestration layer, and it matters here for a related reason. If a workflow can rerun steps merely by resuming, then the approval step cannot assume the state it captured before the pause is still accurate after the pause. The pause itself might be seconds or it might be hours; nothing about "waiting for a human" guarantees the underlying data held still. The AWS Builders Library entry on idempotent APIs makes a companion point from the execution side: stable request identity helps make retries safe, but the design still has to handle late arrivals and distinguish a repeated intent from a genuinely new operation (Malcolm Featonby, AWS Builders Library, "Making retries safe with idempotent APIs," https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/, 2021-01-15, retrieved 2026-09-10).
Put those two ideas together and you get the actual mechanism this article teaches: approval and execution are two separate events, potentially far apart in time, and the thing that connects them safely is not the human's earlier yes but a fresh comparison performed at the second event.
Working the case: v17 approved, v18 on the shelf
Return to Lucy's shop. On Monday, the system captures snapshot v17: vanilla on hand is two, target is eight, required is six, price is 250 cents per tub, line cost 1,500 cents. The draft is validated, Lucy approves it, and the approval record stores snapshot_id="v17", a hash of the draft, Lucy's identity, and the authorized action place_order.
By Tuesday afternoon, a delivery has landed and the inventory system regenerates a new snapshot, v18, where vanilla on hand is now eight. Required quantity under v18 is zero. The approval record still points at v17.
Here is the check that has to run immediately before execution, not merely before the approval was requested:
import hashlib
import json
def draft_hash(draft):
encoded = json.dumps(draft, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(encoded).hexdigest()
def revalidate_before_execution(approval, snapshot_id, draft, action, approvers):
if approval["approver"] not in approvers or approval["action"] != action:
return {"status": "approval_not_authorized"}
if approval["snapshot_id"] != snapshot_id:
return {"status": "stale_approval"}
if approval["draft_hash"] != draft_hash(draft):
return {"status": "approval_mismatch"}
return {"status": "eligible"}
draft = {"sku": "vanilla", "quantity": 6}
approval_v17 = {"snapshot_id": "v17", "draft_hash": draft_hash(draft),
"approver": "lucy", "action": "place_order"}
# Approval and approver membership stand in for trusted application records.
result = revalidate_before_execution(approval_v17, "v18", draft, "place_order", {"lucy"})
print(result) # {'status': 'stale_approval'}The function returns an eligibility decision, never a write count or supplier receipt. It checks current approver membership, the requested action, the snapshot revision and the hash of the draft about to be used. Matching inputs produce eligible; they do not execute the action or consume the approval. The JSON encoding convention must be fixed across producers and consumers, especially if you later add types this simple encoder does not support.
This distinction is worth being blunt about. An eligibility result proves only that these comparison branches accepted the supplied values. It says nothing about whether an external supplier's system received a write, skipped one, or did both because of a retry you didn't account for. Production correctness depends on what actually crosses the wire to that supplier and how that supplier's system behaves on receipt, which a local mock cannot observe.
The race that two sequential steps cannot close
Suppose you implement the check correctly as written above, but you implement it as two separate operations: first read the current snapshot ID and compare it, then, in a later line of code, write the order. Between those two lines, another process could update the stock, meaning your comparison was accurate at the instant you made it and stale by the instant you acted on it. This is the same category of problem the AWS Builders Library piece addresses when it discusses handling late arrivals safely: a check-then-act sequence with a gap in the middle is not the same guarantee as an atomic operation.
For local state, combine the comparison with a durable operation reservation in one protected transaction: reserve this operation only if the relevant snapshot revision is still v17 and this approval has not already been consumed. If another process changed the snapshot in between, the conditional write fails, and your code observes that failure rather than succeeding on stale grounds. This is a genuine concurrency guarantee, and it depends entirely on what your storage layer actually offers. An in-memory Python dictionary with an if statement in front of an assignment offers no such guarantee; two threads or two processes can both pass the check before either one writes. A conditional update or correctly isolated transaction can protect the local reservation. It does not make a remote supplier call atomic with the database. Dispatch the reserved operation using a stable idempotency identity, retain its outcome and reconcile uncertain replies. If stock must remain reserved while dispatch is pending, represent that reservation in the inventory policy too.
If your test only inspects a "stale" flag after a write has already happened, you have tested logging, not prevention. A correct test forces the race to occur before the write and confirms the write count stayed at zero.
Ordered steps for building the revalidation gate
Bind every approval record to four fields at creation time: snapshot ID, draft hash, approver identity, and the specific action authorized. Store all four together, not just a yes/no flag.
At execution time, fetch the current snapshot ID and current draft hash independently of the stored approval, and compare both against the approval's binding before any write is attempted.
Wrap the current-state check, approval consumption and local operation reservation in one protected transition. Handle remote dispatch and reconciliation as a separate protocol.
The local comparison matrix includes a positive control and separate reasons for refusal:
assert revalidate_before_execution(approval_v17, "v17", draft, "place_order", {"lucy"}) == {"status": "eligible"}
assert revalidate_before_execution(approval_v17, "v18", draft, "place_order", {"lucy"}) == {"status": "stale_approval"}
changed = dict(draft, quantity=5)
assert revalidate_before_execution(approval_v17, "v17", changed, "place_order", {"lucy"}) == {"status": "approval_mismatch"}
assert revalidate_before_execution(approval_v17, "v17", draft, "cancel_order", {"lucy"}) == {"status": "approval_not_authorized"}
assert revalidate_before_execution(approval_v17, "v17", draft, "place_order", set()) == {"status": "approval_not_authorized"}
print("One eligible control and four distinct refusal cases")
Checking the transfer case
Check the decision
What would prove this wrong
The claims here rest on comparing recorded values against current values, and on the general behavior LangGraph's own documentation states about resumed nodes rerunning prior code. Nothing here claims a measured production incident, a benchmarked speedup, or a verified supplier interaction. If your storage layer lacks any conditional-write primitive, the race condition section does not close automatically just by writing the comparison in code; you would need to add locking or serialize writes some other way, and that is a different, more expensive mechanism than a single compare-and-set call. If your approval workflow never pauses across a state-changing boundary, meaning approval and execution always happen inside one atomic operation with no gap, then staleness in the sense described here cannot occur, and the whole gate becomes unnecessary. Test for that condition specifically before assuming you need this pattern at all.
A reasonable next step is running the state-transition matrix this article implies: matching snapshot and hash produces eligibility, changed snapshot produces stale_approval, and a changed draft produces approval_mismatch. None of these results measures a write. Add dispatch instrumentation when testing the larger operation protocol. Building that matrix as an actual test, with an intentionally unprotected version first to observe the race and then a protected version to close it, is the concrete next exercise, and it extends the validation boundary already built in the linked course lesson rather than replacing it.
See how the underlying validator that this approval check builds on rejects stale snapshots, duplicate lines, and budget overruns before any draft is accepted.

