LangGraph Human-in-the-Loop: Consent Is a Launch Gate
The programme was ready and waiting on a legal answer, not a build. The fastest option to switch on had no consent ruling on record, and no amount of engineering closes that.
LangGraph in operations, a 10-part series. ← Part 5 · Part 7 →
Consent is not a policy in a drawer that runs alongside the code. It is an ordered structure inside the graph, every step failing closed — and a graph that stops and waits for a person is frequently the fastest path to production, not the slow one.
The Launch Slipped on a Legal Answer, Not on a Missing Feature
A missing feature has a burn-down chart; a missing ruling has a lead time and no velocity.
What Human-in-the-Loop Actually Gates in LangGraph
In LangGraph, an interrupt() suspends the graph where it is called, saves state through the checkpointer, and waits indefinitely. Resuming requires the same thread ID and a Command(resume=...) (LangChain (n.d.), Interrupts).
What they do not tell you is whether the action on the far side is permitted at all. A resume is a mechanism for continuing a graph, not a ruling or a consent record. LangGraph will happily let you resume a send to a person who was suppressed last Tuesday, provided the thread ID matches.
The documented rule is that the interrupt comes first in its node, because everything before it runs again on resume (LangChain (n.d.), Thinking in LangGraph). So the gate checks and anything with a side effect stay out of the approval node, which is where they belonged anyway.
An Ordered Chain Fails Closed Once; A Copied Check Fails Open Once Per Branch
The spine is an ordered chain: allow-list, suppression, the service's own enable flags, human hand-off. Each step has to be able to answer no on its own and halt the graph. The failure mode of an out-of-order check is not an exception in a log — it is a send.
Count your failure points. One chain with four gates that fails closed has one place to be wrong and one place to fix. The same four gates copied into every branch fail open once per branch, and once is the incident.
If No Service Reads Another Service's Record, a Daily Claim Is the Only Shared Truth
No service reads another service's record of who has already been contacted. In architecture reviews this is a virtue — bounded contexts, no shared database, independent deploys. In outreach it is how the same person can hear from two systems on the same day.
The fix is not a shared database. It is a claim: this person, this day, this purpose, taken once before anything is sent. Whoever holds the claim sends; everyone else reads a denial and moves on.
A Uniqueness Constraint Is Not a Send Key
The failure is always confident. There is a uniqueness constraint. It stopped re-imported rows, and it was never a send key.
A constraint answers "does this row already exist?" A send key answers "has this person been contacted today, for this purpose, by anyone?" Only the second has a time component and an identity component and a purpose component, and a constraint that ignores all three is protecting the wrong noun.
Designing the Approval Gate Before the Side Effect
On resume, LangGraph restarts the node from its beginning, not from the line after the interrupt. The documentation is unusually direct: side effects called before an interrupt should be idempotent, precisely because the node re-runs (LangChain (n.d.), Interrupts). A node that writes an audit record before the interrupt writes it twice.
Which is why the interrupt belongs after the gate chain and before the node with the side effect. The interrupt is not a review of what happened; it is a precondition for it happening at all.
Automate the Preparation and Leave the Contact to a Person
The path that ships with no automated contact at all was not a compromise. The graph assembles a brief from hashes and counts, writes it, calls interrupt(), and waits. A person reads the brief and decides. Nothing leaves the building without a human.
LangGraph's product framing is exactly this: human-in-the-loop checks placed to steer and approve agent actions (LangChain, LangGraph). Steering and approving are two different verbs, and the brief-writing graph does both.
The Gates That Quietly Fail Are the Ones That Log Instead of Branch
A gate that writes an approval to an audit table and then lets the node run anyway is not a gate. A gate that treats a timeout as consent fails open on the schedule of your slowest reviewer. A gate that accepts a resume from a thread whose state moved after the interrupt is approving a decision about a world that no longer exists.
So: deny by default. Reject stale resumes by comparing a state hash against the checkpoint the interrupt was raised on. Expire approvals with the claim window. And approve and deny through the same code, so neither can drift from the other.
There is public precedent for the shape of this. An app built on LangGraph requests human confirmation when needed and lets users edit AI-generated summaries so they can verify their own information (LangChain (n.d.), OpenRecovery). Trellix describes the same pattern one level deeper: engineers approving or rewinding an agent's actions, pausing execution or restarting a step with different input (LangChain (n.d.), Trellix).
Four questions decide whether a touch can be automated at all, and they are asked in this order.
Which chain does it pass through, in what order, and does every step fail closed? If the answer is "the service checks its own flag and we trust that," you have one branch and a hope.
Is there a key for this person, this day, this purpose, and who takes the claim? If the key has no time component, it is a constraint pretending to be a dedupe mechanism.
What happens if the graph resumes twice? Walk the node from its first line. Anything before the interrupt that mutates the world is a bug waiting for a network retry.
Which ruling is on record, and what is its date? An answer without a date cannot be re-checked, and a re-check is what you will need when the programme expands scope.
The touches that pass all four, with a ruling on record, are the ones you can automate. The rest go in the queue beside the build, with an owner and a lead time — because consent is a dependency, and dependencies have lead times whether or not anyone put them on the plan.
The useful question is not "can this be automated?" That question has an obvious answer and it is usually yes. The useful question is narrower: for each automated touch, which chain did it pass through, and which ruling is on record?
Answer that for every one of them and you have something better than a launch gate. You have a system where the graph stopping for a person is not a delay in the flow. It is the flow.
