Skip to content

Approval workflows

Approval workflows

Approval workflows gate sensitive create, update, and delete operations. Norbital uses a write-then-lock model: the change is applied immediately, the affected records are locked , and an approval request tracks the review. On approval the change stands and the locks clear; on rejection the records stay — locked, with the values the mutation wrote — and the workspace’s own liveness predicate (`approval_id is null`) keeps them out of view until revised.

Approval config lives on a grant
Approvals are configured on the create/update/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 applies + records locked + approval request created
        │
        ├── approved ────────► change stands, locks cleared
        ├── rejected ────────► request settled; row stays locked
        │                      until a revision replaces it
        ├── request changes ─► still active, locks stay
        │
        └── supersede ───────► approved, as though the superseding
                               team had decided every remaining step
        └── withdraw ────────► requestor withdraws, request settled

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 — the requestor may also withdraw a pending request
  • CHANGES_REQUESTED
  • CONFLICTED
  • WITHDRAWN
Decision states
An approver decides with approve reject request_changes supersede — approve, reject, request_changes, or supersede — and only a pending request can be decided. `supersede` requires a reason; an approved request whose graph no longer applies is marked conflicted.

Locks

To prevent conflicting edits while a request is active, the records the mutation touched stay locked with the request’s id:

  1. Record writes — the written row plus every row in the graph is held behind the request id ( approval_id )
  2. Pending deletions — a delete is also currently a row; it is locked the same way ( approval_id )

The exact set the review is deciding on is recorded in the request’s locked_record_refs — the refs of every record the review holds, root and related rows alike.

Supersede and revision

  • Staged review — ports alternate within one stage 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 always hold that capability when the config allows it.
  • Conflict — approving re-runs the hooks and compares the rebuilt graph against the reviewed one; a changed graph is marked CONFLICTED instead of silently committing.

On approval and rejection

Because the change is already live, approval performs no execution — the system clears the lock and the request is settled: the change stands. Rejection settles the request with the decision recorded; the row stays locked and a follow-up revision replaces it.

Delete approvals work the same way
An approval-gated delete is applied with the request id and stays locked until the review ends.

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 changes go live immediately; a rejection does not restore the previous values.