Skip to main content
The twelve questions that come up first, answered in under eighty words each.
No. An idempotency key deduplicates at one API that chose to support it, inside its retention window. An effect key deduplicates at the agent, before the call, across every remote it touches, is bound to the approval and the receipt, and refuses a retry when the outcome is unknown rather than resending and hoping. Where a remote does support idempotency keys, pass the effect key as one and get both. More.
Different guarantee. An engine makes a workflow finish, retrying activities until they succeed; CTRLRun decides whether a consequential effect is authorized and refuses to retry an unknown outcome. Their advice, make activities idempotent, is the work CTRLRun does for you and enforces in the store. Run the workflow in the engine and decorate the activity that acts. More.
Probably not. @protect covers anything in your Python process, and the gateway covers anything behind an MCP server in any language. An adapter buys exactly one thing: routing an approval through the framework’s own interrupt so a human answers where they already answer. A framework with no such primitive needs none. More.
No, and nothing can be against systems it does not control. CTRLRun guarantees it will not knowingly execute the same logical effect twice, and never treats an unknown outcome as a failure. The remote is the only thing that knows what the remote did; when nobody knows, CTRLRun says so and stops. More.
The effect becomes AMBIGUOUS, never FAILED, and a retry against it is refused. Only NotExecuted, raised by your executor when it knows the remote did nothing, means failed. A human runs ctrlrun resolve --committed or --failed after asking the remote, or a reconcile hook asks for them. More.
It can call the undecorated function, and the threat model says so: process-level enforcement is out of scope. Two things narrow it. The gateway sits between the agent and its tools, where the agent has no choice; and CTRLRun is never a tool the agent decides to call, because a check the agent opts into is not a check. Threat model.
No. There is no telemetry, no licence check and no network call of any kind in the kernel. ctrlrun stats counts the local SQLite file, and a test runs the demo in a subprocess whose sockets are all refused. The extras reach the network only where you point them: your JWKS, your webhook, your OTLP endpoint, your Postgres.
The approval request expires at its TTL, fifteen minutes by default, and a waiting call raises ApprovalTimeout with nothing executed. Raise the TTL for slow approvals, or do not wait: ApprovalRequired carries the request id, so an agent can surface it and come back later with with_approval(request_id). More.
Either. On one host the store is a SQLite file and reservation is atomic across processes through BEGIN IMMEDIATE. Across hosts, pip install "ctrlrun[postgres]" and change the store: a unique index on the effect key and compare-and-set updates give the same guarantee, graded by the suite written for SQLite. More.
One executed action: who proposed it, the action and its canonical arguments, the decision and why, the approval and approver, the effect key, the outcome, the timestamps, the hash and declared version of the policy that decided it, and the hash of the receipt before it. Portable JSON, one object per line. More.
No. Each receipt carries the hash of the one before, so an edit, a deletion from the middle or a reordering is detected and named by seq. It does not prove who wrote a receipt, receipts are not signed, and it does not survive an administrator who can rewrite every row including the chain head. More.
A compromised host or process. A malicious administrator with write access to the store. A lying remote. A compromised approver. An executor that raises NotExecuted after the remote acted, which ctrlrun verify cannot see. Data exfiltration through reads. Authority across an agent-to-agent hop. Threat model.

Next