Approval workflows
Approval workflows
Approval workflows gate sensitive create, update, and delete operations. Norbital uses a prepare, gate, commit, settle lifecycle: the write contract prepares the proposed graph, then the approval gate either commits it outright or commits it provisionally and puts it on hold , and an approval request tracks the review. Every row the graph creates or updates is a real row stamped with `approval_id`, and a `hold` revision keeps its pre-image. Approval seals the write and clears the stamps; rejection restores every touched row to its hold snapshot.
approval item whose `flow` returns `approveBy(…).thenBy(…)` or `noApproval`, plus `superceded_by`. See Policies.Request lifecycle
gated mutation
│
▼
write commits provisionally + hold recorded + request created
│
├── approved ────────► change stands, stamps cleared
├── rejected ────────► request closed; every touched row restored
├── request changes ─► request closed; every touched row restored
│
├── supersede ───────► approved, as though the superseding
│ team had decided every remaining step
└── withdraw ────────► requestor withdraws; every touched row restored Statuses
The review moves in stages — one team per stage, alternatives in one stage — and the request follows its statuses:
ONGOINGAPPROVEDREJECTEDCHANGES_REQUESTEDCONFLICTEDWITHDRAWN
The request status vocabulary that reports filter on:
ONGOINGAPPROVEDREJECTED— every row touched by the hold is restored to its snapshot and the stamps are clearedCHANGES_REQUESTEDCONFLICTEDWITHDRAWN— the requestor withdrew the request; every touched row is restored the same way
APPROVED REJECTED REQUEST_FOR_CHANGE SUPERSEDED — the client verbs are APPROVED, REJECTED, REQUEST_FOR_CHANGE, and SUPERSEDED, and only a pending request can be decided. `supersede` and `request_changes` require a reason; a refused request whose restore cannot apply keeps the hold and is marked conflicted.Locks
A routed write commits provisionally in the same transaction that opens the request; every created or updated row exists and is stamped with the request’s id:
- Held creates and updates — the row exists with the proposed values and carries the stamp (
approval_id) - Held deletes — the row is removed and its pre-image is kept as a hold revision (
approval_id)
The rows under review are the history revisions carrying the request’s approval_id ; a record’s restore point is read with api.collection_history.<name>.at(id, { before: requestId }).
Supersede and revision
- Staged review — teams listed in one stage are alternatives and stages run in sequence; authoring
approveBy('Team').thenBy('Other Team')appends the next stage on top of the current one. - Superseding teams — the approval can name
superceded_by: teams allowed to finish every remaining step at once. Administrators may always supersede. - Conflict — a restore that cannot apply (an unheld row now points at a row the restore must delete) keeps the hold and marks the request
CONFLICTEDinstead of silently committing.
On approval and rejection
Approval performs resume — the stamps are cleared, the change is published, `committed` notifications fire, and the request closes. Rejection closes the request with the decision recorded and restores every touched row to its hold snapshot: rows born under the hold are deleted, others are rewritten, and a `restore` revision is appended.
When to use approvals
- Financial changes above a threshold
- Changes to records with compliance implications
- Edits to sensitive master data
- Mutations that should clearly separate requester and approver roles
Recommended operating rules
- Use approvals selectively on high-risk mutations only.
- Pair approvals with clear notifications so reviewers know when action is needed.
- Test lock behavior on linked or nested records before rolling out a workflow widely.
- Remember that gated values are committed provisionally and visible under `approval_id`; approval seals them and rejection restores every touched row to its hold snapshot.