Skip to main content
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 stays pending for its TTL. action_hash is 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 DuplicateEffect works with nothing to unwrap.

If it didn’t work

  • ApprovalMismatch on resume: the arguments you sent back are not the ones the human saw; send pending["arguments"] verbatim.
  • ApprovalRequired raised past the graph: the decorator lacks wait=True, or the function is bound to a different Control than the one carrying the provider.
  • InvalidArgument: approved must be True or False: the resume value carried a string.

Next