How it works
An agent proposes. Something else decides.
Every action OnDuty takes on your behalf passes through the same four stages, in the same order, with no way around them. This page is what happens in each one, including what happens when a stage fails.
The four stages
The order is the design. An agent that could execute would be an agent whose judgement is the last word, and no rule you wrote would be able to stop it.
01
Propose
An agent reads the situation and says what it thinks should happen. It names an abstract action and its parameters, nothing more.
It cannot call a connector, hold a credential, or reach anything outside OnDuty. Its tools are read-only retrieval and one propose_action.
02
Evaluate
The proposal goes to the decision engine, which checks it against the guardrails written for your workspace and answers permit, hold, or deny with a reason and a trace id.
The engine never executes and holds no connector credentials. It decides; it cannot act.
03
Execute
Only a clean permit reaches the executor, which calls the connector once, keyed on an idempotency key so a retry cannot send the same message twice.
A hold goes to a person. A deny stops. Anything that is not an unambiguous permit does not execute.
04
Record
The decision, its reason, its trace and its outcome are written to a hash-chained trail, whether the action was permitted, held, or refused.
Refusals are recorded as carefully as executions. A trail that only holds successes cannot answer the question anybody actually asks.
Nothing acts on your customers because a model was confident. It acts because a rule you wrote said it could, and the record of that rule saying so is kept.
Contracts
The two contracts
The proposal and the verdict are fixed shapes. Fixed shapes are what make the seam auditable: there is no free-text channel through which an agent can talk the executor into something.
What an agent proposes
- action
- The abstract operation, for example messaging.send. Never a vendor name: the channel is the connector's business, not the agent's.
- parameters
- What the operation needs. Structured, validated, and readable by a person reviewing a hold.
- agent_id
- Which agent proposed it, so a pattern of bad proposals is traceable to its source.
- tenant_id
- Whose workspace this belongs to. Every rule and every key is resolved per tenant.
- context
- What the agent was looking at when it proposed. This is what a reviewer reads before deciding.
- idempotency_key
- The identity of the intent. Two deliveries of the same intent execute once.
What comes back
- decision
- permit, hold, or deny. There is no fourth answer and no partial one.
- reason
- Why, in terms of the rule that applied. A verdict without a reason is not reviewable.
- trace_id
- The handle that ties this verdict to its audit entry and to the action that followed.
- latency_ms
- How long the decision took, because a governance layer that is slow is a governance layer people will want to switch off.
- active_campaigns
- Which standing rules were in force at that moment, including a kill switch if one was on.
- shadow
- Whether the workspace is still proving its rules rather than enforcing them.
What happens when something goes wrong
Fail-closed is not a setting. It lives in the executor, which is the one place that can actually stop an action, and it applies to every failure mode below.
| Situation | What happens | Why |
|---|---|---|
| The decision engine times out | The action does not execute. | A slow answer is not a permit. Treating silence as approval is how an outage becomes a message sent to a customer. |
| The decision engine returns an error | The action does not execute. | An error means the rules were not applied. Acting anyway would be acting ungoverned while claiming to be governed. |
| The engine is unreachable | The action does not execute. | Same reason. Availability of the governance layer is never traded against getting the action out. |
| The verdict is hold | It goes to the review queue and waits for a person. | A hold is the rule saying a human should look. Auto-resolving it after a timeout would delete the only thing it was for. |
| The verdict is deny | The action stops and is recorded as denied. | Denied actions are kept, because the useful question is often what the system was stopped from doing. |
| The connector fails after a permit | It retries, then goes to a dead-letter queue. | The decision was clean, so the intent stands. What failed is delivery, and the idempotency key means a retry cannot duplicate it. |
A hold is a person, not a timer
When a rule stops an action, someone has to answer for it. The review queue is where that happens, and an unanswered hold is treated as a problem rather than as a quiet default.
A held action carries everything the reviewer needs in one place: what the agent proposed, what it was looking at, which rule stopped it, and what happens if it is approved. Approving does not hand the agent more freedom, it releases this one action.
Holds resolve asynchronously. OnDuty subscribes to the engine's webhooks and also polls the review list, because a webhook that fails every retry while a service is down would otherwise leave an action waiting forever with nobody aware of it.
The Brain itself watches for the failure mode this creates. A hold nobody has resolved becomes a signal, which becomes something on the Today view, which is how an ignored queue stops being invisible.
Prove it before it acts
Shadow mode runs every rule and records every verdict without letting anything execute. You get a week of real decisions to read before a single one is real.
Shadow is a property of your workspace, set on the governance side, and OnDuty honors what the engine reports rather than deciding for itself. A workspace in shadow is shown as being in shadow everywhere it matters, including here: we do not describe a tenant as governed in production until its shadow is off.
What you read at the end of it is not a simulation. Those are the actual proposals your agents made against your actual data, and the actual verdicts your actual rules returned. The only thing that did not happen is the acting.
The decision trail
Every verdict is written to an append-only trail whose entries are hash-chained, so a missing or altered entry is detectable rather than merely unlikely.
Each entry carries the action, the verdict, the reason, the rules in force, and the trace id that ties it to what happened next. You can open a single decision and read it, or verify the chain and get an answer about the whole record rather than about one row.
The trail exists for the moment someone asks why. Not why in general, but why this message, to this customer, on this day. A system that cannot answer that question specifically has not really been governed, it has been trusted.
Where this stands today
The mechanism is built. Here is what is and is not running.
The seam is real code and covered by tests: agents cannot construct an executor, no route names a gate implementation, and the executor refuses to act without a clean permit. Those are enforced by the test suite rather than by convention.
The gate has two implementations behind one interface. The production path calls the decision engine. The offline path, used in development, permits everything and says so loudly: a workspace running on it is shown as ungoverned rather than quietly treated as safe.
We would rather tell you which one you are on than let the word governance do work the configuration is not doing.
Read the rules before you trust them.
Connect one system, run it in shadow, and read a week of decisions that did not happen.