Skip to content

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 config lives on a grant
Approvals are configured on the `mutate.new`, `mutate.existing`, or `delete` grant inside a policy — an 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:

  • ONGOING
  • APPROVED
  • REJECTED
  • CHANGES_REQUESTED
  • CONFLICTED
  • WITHDRAWN

The request status vocabulary that reports filter on:

  • ONGOING
  • APPROVED
  • REJECTED — every row touched by the hold is restored to its snapshot and the stamps are cleared
  • CHANGES_REQUESTED
  • CONFLICTED
  • WITHDRAWN — the requestor withdrew the request; every touched row is restored the same way
Decision states
An approver decides with 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:

  1. Held creates and updates — the row exists with the proposed values and carries the stamp ( approval_id )
  2. 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 CONFLICTED instead 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.

Delete approvals work the same way
An approval-gated delete removes the row provisionally and keeps its snapshot in the hold; rejection re-inserts it.

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.