Skip to main content
An approval policy is a sequence of ordered stages attached to a requestable role or scope type. Each stage either evaluates automatically or waits for a human decision. Stages are processed in order: the first stage runs at submission, subsequent stages unlock one at a time as each preceding stage approves. A denial at any stage is final and writes nothing. Because a policy stores stages and never resolved approver identities, it survives org restructuring and hierarchy promotion without modification.

The ApprovalPolicyInput type

You supply an ApprovalPolicyInput wherever a requestability declaration expects a policy field, either on a scope type in the catalog or inside a role’s requestable block.

Stage types

Auto-approval predicates

An auto stage evaluates a condition against the requester’s subject closure, using the same can() machinery that powers every check in Alfiz. There is no separate rules engine. If the predicate passes, the stage is recorded as auto-approved and the workflow immediately advances to the next stage. If it fails, the stage abstains without recording a decision, and the workflow advances to the next stage. Auto stages accelerate workflows; they never deny.
With one exception: a failing auto stage that is the last stage has nothing to advance to. Since 0.7.0 such a request is auto-denied, and the decision is recorded with decidedBy: "auto" and the requester can re-request when circumstances change. (Before 0.7.0 it stayed pending at a stage nobody could decide, resolvable only by administrative override, which is the failure mode the change removes.) An auto-final policy is therefore the instant-decision shape: predicate met approves, predicate unmet denies. If unmet predicates should escalate to a human instead, put a named_approvers or management stage after the auto stage.
{ type: "in_group"; groupId: string }
Passes when the requester is a member of the given group (directly or through group inheritance).
{ type: "in_org"; orgId: string }
Passes when the requester belongs to the given organization in the directory.
{ type: "member_of"; subject: SubjectId }
Passes when the requester’s subject closure contains the given subject. Works with any valid subject including implicit groups like directs:<uid> or orgof:<uid>, for example { type: "member_of", subject: "directs:team-lead-uid" } auto-approves everyone who reports directly to a particular manager.
{ type: "holds_pattern"; pattern: PermissionPattern }
Passes when the requester’s effective access already intersects the given pattern. Grounded in the catalog’s concrete keys; a fully-revoked requester never passes, and a pattern matching no catalog key never passes. Negative-always-wins holds here exactly as it does in can().

Named approvers

A named_approvers stage requires approval from any subject who holds an unexpired grant of the designated role, either directly or through any closure member.
The canonical use is expressing the application owner as a role on the namespace. Rather than naming a specific person, you name the role: whoever holds it at evaluation time is the approver. This keeps the policy stable across ownership changes.
To approve a named_approvers stage, a user must hold an unexpired grant of the designated role, either on their own subject or through a group or org they belong to. Expired grants do not confer approval authority.

Management layers

A management stage routes the request to the requester’s manager, or layers transitive managers upward in the reporting hierarchy. The chain is resolved by walking reporting edges at evaluation time.
layers defaults to 1, which means the requester’s direct manager. layers: 2 means the manager’s manager, and so on. Where the reporting chain is shorter than layers, the topmost manager approves.
A policy with a management stage is a configuration error when no reporting hierarchy is populated in the provider. Alfiz surfaces this at policy attachment time (for roles) and at request submission time (for catalog scope types), and never silently skipped. Populate reporting edges via setReportingEdge or importDirectory before attaching management-layer policies.
Because stages reference a layer count rather than a resolved person, they survive org-root promotion untouched. A pending request that was created with "2 layers up" still means exactly that after a hierarchy reorganization: the new two-layers-up manager becomes the approver at their next evaluation.

Policy attachment

Attach an approval policy directly to a role’s requestable block using createRole. The policy applies to every request for that role, regardless of scope.

A complete three-stage policy example

This policy uses all three stage types in order: first try to auto-approve, then fall through to a named application owner, and finally escalate to a skip-level manager.
In this flow:
  1. Stage 0 runs immediately at submission. If the requester’s effective access already includes projects.admin.*, the stage auto-approves and the workflow advances to stage 1. If it does not, the stage abstains and the workflow still advances to stage 1, because the auto stage never blocks.
  2. Stage 1 shows the request to any user holding the project-owner role, who sees this request in their approver queue and may approve or deny it.
  3. Stage 2 starts only after stage 1 approves, when the skip-level manager receive the request. Their approval writes the grant row.

Requestability does not inherit

requestable exists in exactly two places: on a scope type (ScopeTypeInput.requestable, whose policy lives under a policy key) and on a role (RoleInput.requestable, whose stages sit directly on the block). There is no third place, and there is no inheritance of any kind. A policy on docs.folder does not propagate to docs.doc, even though documents nest inside folders. Each scope type declares its own requestable block or is not requestable at all. Nothing is requestable by default.
A requestable scope type must declare at least one approval stage. A policy with an empty stages array is a validation error, surfaced by lintCatalog at build time and by assertPolicyResolvable at request submission.

Just-in-time access

The canonical just-in-time (JIT) access pattern combines three features:
  1. requireExpiry: true on the requestable role or scope type, so every request must propose an expiry; open-ended requests are rejected.
  2. proposedExpiresAt in the request, where the requester proposes how long they need the access; the maxDurationMs cap enforces an upper bound.
  3. can.fresh() at enforcement points, which bypasses the subject-side cache so the grant expiry is evaluated at exactly the moment of the check, not against a potentially stale snapshot.
can.fresh is the right choice for any destructive action that may have been time-limited by an approved request. It is the only check shape guaranteed to reflect expiry at the instant of evaluation rather than at the last cache fill.