Approvals
A definition says who approves a record in how many stages; a request walks them one at a time; one rejection ends it. What each refusal means.
Every endpoint here needs the approvals module. A definition is written once; requests are submitted against records and move through its stages.
A definition is up to three stages
POST /approval-definitions takes a label, the objectId it applies to, a version, and stages — one to three of them, each naming exactly one approver:
{ "approverUserId": "…", "label": "Manager" }
Every approver must be a member of the organization. A user id that is not answers approver_outside_organization, and the whole definition is refused rather than created with a gap.
Submitting pins the record
POST /approvals takes a definitionId, a recordId and the record’s revision.
It snapshots what the approvers will read. The request stores the recordRevision, a snapshot of the record and its relations, and a contentHash. Approvers decide on that snapshot, not on whatever the record has become since.
Sending a stale revision is refused — see revisions for why that is not resolved to the newest.
A submitter cannot be an approver. If the submitter’s own user id appears in any stage of the definition, the submission is refused with approver_is_submitter. The check is at submission, not at decision time, so a definition that names you is simply unusable by you.
Deciding walks one stage at a time
POST /approvals/{id}/decisions takes decision — approve or reject — the stage number, and an optional comment of up to 4,000 characters.
The stage number is required and checked. A request carries an activeStage, and a decision whose stage is not the active one is refused with approval_stage_moved, as is any decision on a request that is no longer pending. Read the request before deciding rather than assuming which stage is open.
Only the assigned approver may decide. The approver of stage n is stages[n-1].approverUserId, and anyone else — including the submitter — gets approver_not_assigned.
A request’s state is one of four, and the transitions are not a graph worth drawing:
| Decision | Result |
|---|---|
reject at any stage |
rejected. One rejection ends the request, whatever stages remain |
approve at the last stage |
approved |
approve at an earlier stage |
still pending, with activeStage advanced by one |
Withdrawing
POST /approvals/{id}/withdraw sets the state to withdrawn.
Only the submitter may withdraw, and only while the request is pending: anyone else gets withdraw_not_submitter, and a request already decided gets approval_not_pending.
Approvals block other work
A pending approval is not only a record of intent. Editing the record it was submitted against is refused with record_approval_pending — withdraw the approval first. A quote with one open likewise refuses to be revised, as quote_approval_pending. Submitting for approval is therefore closer to taking a lock on the thing being approved than to sending a notification.
Four scopes, not one
| Scope | What it covers |
|---|---|
approvals.read |
Listing definitions, requests and decisions |
approvals.manage |
Creating a definition. Admin only |
approvals.submit |
Submitting and withdrawing — the two travel together |
approvals.decide |
Deciding a stage |
The split mirrors the rule above: submitting and deciding are different scopes, so a client built to file requests cannot approve them even before the per-request check runs.
Reading
GET /approvals lists requests and GET /approvals/{id}/decisions answers the decisions made on one, each with its stage, decision, comment and decidedBy. A rejected request keeps its decisions, so the reason survives the refusal.