Approvals
An approval records that an eligible person decided on this exact request. It never executes anything. Only the original agent's authenticated resume, after a fresh authority check, can claim execution.
Bind what the person saw
Every action has a versioned canonical representation, axiom.action.v1:
{
"v": "axiom.action.v1",
"tenant": "ten_…",
"agent": "agt_…",
"audience": "svc_…",
"operation": "billing.refund.create",
"resource": { "type": "invoice", "id": "417" },
"params": { "amount": "25000", "beneficiary": "Acme Corp refund account (sample)", "currency": "USD" },
"not_after": "2026-11-15T18:00:00.000Z"
}It is serialised with RFC 8785 (JSON Canonicalization Scheme) and hashed with SHA-256 into the request_digest (sha-256: followed by base64url). Params are normalised first (decimals without trailing zeros, currencies upper-case), so "25000.00" and "25000" bind to the same digest. Delegated child operations add root_action.
The approval page, the connector request, the approval decision, the execution check and the receipt all use this digest:
- A decision must carry the digest the approver was shown. A different digest is refused with
409 conflict("the request changed after you viewed it"),reason_code: material_change. - A resume must reproduce the approved digest. A changed amount, currency, beneficiary, resource or operation key is
409 conflictwithchanged_fields. A new request needs a new approval. - A field marked
approver_visible: falseis hidden from the approver view but still bound by the digest.
The approver view contains the summary ("Billing agent requests USD 25,000 against invoice 417 for customer Acme Corp (sample customer)"), tenant, agent and operator, operation, resource, the visible material fields, the digest, the deadline, the policy rule that triggered the approval, how many approvers are required and received, the agent's free-text intent, and a receipt link once the provider confirms the result. The intent is shown to the approver but never used for authorisation.
Approval modes
| Mode | How to express it |
|---|---|
| Routine actions within a standing grant | A grant with no matching require_approval rule. The action goes straight to approved and is dispatched. |
| Approval when a condition is met | A require_approval rule with when conditions (amount, resource, environment, tenant…). |
| Approval for every sensitive action | A require_approval rule scoped to the operation with no conditions. |
| Two distinct approvers | approval.count: 2. The status shows second_approver_required after the first approval. |
| An external system of record | approval.connector_id pointing at a workflow connector (ServiceNow). |
Every matching requirement becomes its own approval record (one per role after merging), with its own correlation ID ({action_id}:{role}), deadline and delivery state. All of them must be approved; any denial denies the action.
Who may decide
An eligible approver holds the requirement's approver_role in the action's tenant (for example finance_approver), either directly on the tenant or through the organisation that owns the tenant, is not disabled, has not already decided on this approval and is not excluded by separation of duties.
Separation of duties (separation_of_duties: true) excludes:
- the agent's owner and sponsor;
- the author of the policy version that required the approval. Publishing a rule cannot satisfy it.
With count: 2, the second decision must come from a different person; the same person cannot approve twice.
Approvers decide on the built-in approval page (app.sectigoaxiom.com/approve/{approval_id}) or, when the requirement names a connector, in that native workflow. The two are exclusive: a connector-routed approval can be decided only in the connector's system, and the Axiom page refuses it, so the customer's chosen system of record cannot be bypassed. Approval emails for the built-in page go to every eligible holder of the role, including organisation-level holders. MCP clients acting for a person can list pending approvals but cannot approve or deny: no MCP tool records a decision.
Decisions from external systems
When the approval is routed to a connector, a webhook from the native system only wakes Axiom. The webhook endpoint is unauthenticated by design, because its body is never trusted: it only names the native record to read back, and it is rate limited (60 calls per minute per connector). The decision itself is fetched by a server-to-server readback that verifies the approver, the action binding (the digest stored on the native record), the decision and the deadline. Axiom then signs its own observation (axiom.observation.v1, observation key) and keeps the native record reference, so the receipt names the actual signer: Axiom observed a decision via connector_readback; it does not claim the approver signed anything.
verified_via | Meaning |
|---|---|
axiom_session | Decided on the approval page in an authenticated Axiom session. |
connector_readback | Read back from the native system by the connector. |
signed_callback | Reserved for systems that can sign the decision themselves. |
Replays converge: a repeated webhook or a restored backup cannot count the same approver twice.
Preview status
ServiceNow is the only connector with a working request and readback (catalog availability: "implemented"), in simulated or live mode. The other catalog entries are availability: "declared": their operations are listed, but they are not yet available and the console shows them as "Planned, not yet available". An approval routed to one of them fails delivery with "is in the connector catalogue but not implemented yet". No connector is qualified against a real customer tenant. See Connectors.
Deadlines
Two separate deadlines apply:
| Deadline | Set from | When it passes |
|---|---|---|
decision_deadline | The shortest decision_ttl_seconds, capped by the request's not_after and the grant's expiry. | The action becomes expired (approval_expired); pending approvals expire. |
resume_deadline | Set when the last approval arrives: the shortest resume_ttl_seconds, capped by not_after and the grant's expiry. | An approved action that was not resumed becomes expired (resume_expired). |
A late decision, a late webhook or a late readback cannot revive an expired, cancelled or revoked action: every state change is conditional on the current state. Expiry is applied on the next status read or resume, and by the periodic sweep (every five minutes on Cloudflare).
What happens on resume
When the original agent resumes, Axiom re-checks the agent, its credential, the grant (and its ancestors), emergency denies and current policy. If current policy requires an approval that was not satisfied, the resume is refused with policy_stale; if an Axiom-session approver no longer holds the role, with approver_ineligible. External approvers' eligibility is taken from the connector readback that was recorded.
Progress while pending
The status document's progress field distinguishes why an action is still pending:
progress | Meaning |
|---|---|
delivery_pending | Axiom has not yet delivered the request to the approval system (it retries). |
waiting_on_person | Delivered; waiting for a person to decide. |
decision_readback_unavailable | A connector readback failed; Axiom retries every minute. The decision is unknown. |
awaiting_agent_resume | Approved; waiting for the original agent to resume. |
approvals[].next_party names who is responsible next, for example finance_approver in axiom or approval delivery.