ctrlrun-langgraph makes an approve decision surface as a LangGraph interrupt() instead of
an ApprovalRequired raised past your graph. The human answers where LangGraph users already
answer, with Command(resume=...), and one core provider writes the grant through the same
calls ctrlrun approve makes. With carries_approved_arguments=True the resumption carries the
arguments the human saw and core re-checks them against the action hash: that is prevention.
You probably do not need this. @protect already covers a LangChain tool or a graph node with
no adapter. Use it when your deployment has a place where a human answers a LangGraph interrupt
and you want approvals to land there.
Prerequisites: pip install ctrlrun-langgraph, langgraph>=1.0,<2.0, a graph compiled
with a checkpointer. The blocks below are the adapter’s own example; the adapter’s tests run
them against a real LangGraph install in this repository’s CI.
1
Build the Control, and hand it over
The operator chooses the policy, the store, the identity provider and the authority
document. The adapter is one argument to the approval provider and never constructs a
Control or supplies a principal.wait=True is the whole difference: it routes the approve through the provider, and so
through interrupt(), instead of raising.2
Answer the interrupt
Command(resume=True) grants as langgraph:interrupt; Command(resume=False) refuses;
the mapping form carries the approver and the arguments. approved must be a real boolean;
a truthy string is refused with a message naming your resume value.3
Choose prevention
carries_approved_arguments has no default, because the default somebody assumes is the
one that does not check. True: the resumption must carry arguments, core rebuilds the
proposal with them and compares the hash, and an answer given against €5 that arrives for
a €5,000 action is refused with ApprovalMismatch. False: only the verdict comes back,
the binding across the interrupt is LangGraph’s checkpoint, and CTRLRun records who
answered without being able to re-check what about; the conformance kit reports binding
as not applicable, never as a pass. Choose False only if your console cannot echo what it
displayed.Where LangGraph shows through
- The node runs twice, once to ask and once on resume, so there are two
action_ids and two approval requests for one refund; the first stayspendingfor its TTL.action_hashis continuous, which is why the binding is about content and never about an id. - The resumed pass re-checks everything: principal expiry, authority and policy at resumption time, so an authority revoked while the human deliberated refuses the action then.
- The TTL does not bound deliberation; your checkpoint does. Expire the thread if it matters.
- The kernel’s exceptions arrive as themselves. LangGraph propagates a node’s exception, so
except DuplicateEffectworks with nothing to unwrap.
If it didn’t work
ApprovalMismatchon resume: theargumentsyou sent back are not the ones the human saw; sendpending["arguments"]verbatim.ApprovalRequiredraised past the graph: the decorator lackswait=True, or the function is bound to a differentControlthan the one carrying the provider.InvalidArgument: approved must be True or False: the resume value carried a string.
Next
- Use the OpenAI Agents SDK adapter: the other shape, and why its binding is attribution.
- Approval binding.
- The adapter’s README · Get started · Why.