Skip to main content
A framework’s human-in-the-loop primitive is the right place for a human to answer, and CTRLRun uses it rather than replacing it. What it does not do is bind that answer to the exact action that executes, notice that the same effect already happened, or leave evidence outside the framework’s own run state. Those are different jobs, and an adapter joins them.

What a framework interrupt is good at

LangGraph’s interrupt() and the OpenAI Agents SDK’s tool-approval interruption both stop a run at exactly the right moment, carry the pending call to whatever console your operators use, and resume the run where it stopped. They are integrated with the framework’s state, its checkpointing and its streaming, and they are what your users already know. Nothing here replaces that; the adapters exist so a human keeps answering there.

What it does not do

When to use both

If your agent runs on LangGraph or the Agents SDK and your operators answer there, install the adapter: @protect(wait=True) sends the approval out through the framework’s interrupt, the answer comes back, and one core provider writes the grant through the same calls ctrlrun approve makes. Two places to say yes would be one place nobody is watching, so there is only ever one. If your framework has no such primitive, you need no adapter. @protect covers the call anyway, and ApprovalRequired is raised for your own code to handle.

The distinction that matters

A framework binds an approval to a call it is about to make. CTRLRun binds it to what that call would do. Where the framework’s resumption carries the arguments the human saw, an adapter hands them back and CTRLRun re-checks the hash: prevention. Where it carries only a verdict, CTRLRun records who answered and cannot re-check what about: attribution. Each adapter’s page says which it is, in that word.

Next