| Role | View | Generate | Finalize | Record payment | Void | Counter sale |
|---|---|---|---|---|---|---|
| Owner | Allowed | Allowed | Allowed | Allowed | Allowed | Allowed |
| Manager | Allowed | Allowed | Allowed | Allowed | Not allowed | Not allowed |
Overview
Approval work has two halves: a human decision and the domain change that follows it. The Approvals inbox shows the open stage assigned to you. A separate blocked-work inbox shows later stages that cannot resolve a responsible holder. A failed apply is neither a lost decision nor a silent success: it records why the domain change stopped and can be retried when you are allowed to do so.
Where to find it
Open Approvals at /approvals on the web dashboard, or /m/admin/approvals in the mobile admin app. Your personal inbox and the blocked-work exception list are separate reads, so an exception does not look like a task assigned to you. Policy publishing is available through the Approvals policy controls/API (approvals.policy.publish) for the admin integration.
Key concepts
An open step is an approval stage in state open, on a pending request, with an active assignment to your user. The inbox orders steps oldest first and includes the responsibility key, org unit, age, due time, and overdue flag. Use those fields to decide what needs attention first.
Blocked work is a request in blocked_unresolvable. A later stage found zero or more than one holder for its responsibility key and org unit, so the engine stops before making an assignment. Only a user who can reassign that stage and holds its responsibility key in scope can see the row. There is no Owner fallback or automatic approval.
Apply failure means the decision was recorded but the handler could not make the domain change. The current failure reasons are context_drift, insufficient_stock, credit_exceeded, and budget_line_exhausted. A request in apply_failed remains retryable until it is superseded or cancelled.
A policy version is an immutable, tenant-scoped definition for a subject type and action. A request keeps the version it was submitted against; publishing a new version does not rewrite work already in flight.
Common workflows
resolveBlocked with an eligible same-tenant recipient and a reason. If a seat already exists, use reassign instead. Both operations restore the request to pending and write an audit entry.approval.request.apply-retry, run retryApply for the request. This re-arms its apply job; the worker rechecks the current context, so retrying does not bypass stock, credit, budget, or revision checks.approvals.policy.publish. v1 requires a catch-all band, resolves the first matching band, and opens sequential single-seat stages; quorum, multiple required approvals, and fork/join stages are refused. A successful publish creates the next immutable version and records who published it.Role notes
Owner is the documented tenant administrator and the default operator for policy configuration. The engine still checks the responsibility key and org-unit scope for every assignment, recovery, and decision; ownership never creates a fallback seat.
Manager can work approval steps assigned to them. To see and recover a blocked row, the Manager must hold both approval.stage.reassign and the stage's responsibility key at an ancestor unit. Retrying an apply additionally requires approval.request.apply-retry; neither permission is implied by being a Manager.
Tips & time-savers
Decide first, then watch the request state. decisioned means the human step is complete; applying and applied describe the domain effect that follows.
Gotchas
resolveBlocked only when the stage has no seat; when a seat exists, resolveBlocked refuses and reassign is the correct operation.An old policy version is never edited in place. A retry also does not force an apply through changed context: a new failure is the safe result when the subject, stock, credit, or budget no longer matches.