| title | Approval workflow |
|---|---|
| description | Route a record for sign-off — who can configure the automation, and the run-identity decision that keeps an approval flow from quietly bypassing row-level security. |
A record (an invoice, a discount, a leave request) must be routed for approval before it proceeds. Who is allowed to build that automation, and how do I make sure it runs safely?
An approval is a flow with an approval node. Two access decisions matter as much as the routing itself.
Authoring flows/automations is a builder capability — it needs manage_metadata (typically Studio users). End users submit records and act on approval requests, but they do not edit the automation. Keep the automation surfaces out of consumer apps (see role-based interfaces).
A flow declares runAs (ADR-0049), and for approvals this is the decision that keeps it safe:
runAs: 'user'(default) — the flow's data operations run as the submitter, respecting their RLS. Good when the flow only touches records the submitter can already see.runAs: 'system'— elevated, bypasses RLS. Needed when the flow must read/write records the submitter can't (e.g. post to a ledger, notify an approver who owns rows the submitter can't see). Declare it explicitly so the elevation is visible, not accidental.
A schedule-triggered escalation has no triggering user — so it must be system to act at all.
The node declares who approves (a named user, a role, the submitter's manager, …) and what happens on approve / reject / escalate. The authoritative shape is ApprovalNodeConfig / ApproverType; a flow strings the trigger, the approval node, and the post-decision branches together.
// Illustrative — see the Approval reference for the exact node schema.
defineFlow({
name: 'invoice_approval',
runAs: 'user', // submitter's RLS unless a step needs more
trigger: { object: 'invoice', on: 'create' },
nodes: [
{ type: 'approval', approver: { type: 'role', role: 'finance_manager' },
onApprove: 'mark_approved', onReject: 'mark_rejected' },
],
});Approving is itself a gated action — model "may approve" as a capability (approve_invoice) the approver role holds, and gate the approve action's requiredPermissions on it so the gate is enforced on both the UI and the server (ADR-0066 D4).
Separating who configures from who approves from as whom it runs is the same capability/assignment/requirement decoupling as the rest of authorization (ADR-0066). The runAs default of user means an approval flow can't silently grant the submitter cross-tenant or cross-owner reach — elevation is opt-in and auditable. That is the safe-by-default posture: the common case respects RLS; the elevated case is explicit.
- Flows:
examples/app-showcase/src/flows. - Approval schema:
packages/spec/src/automation/approval.zod.ts.
runAs: 'system'everywhere "to make it work". Default touser; elevate a single step only when it genuinely needs to bypass RLS.- Exposing automation config to approvers/submitters. Configuring the flow is a
manage_metadata(builder) concern; acting on a request is not. - Gating "Approve" only in the UI. Make
approve_*a capability and gate the action so the server enforces it too.
- Decision records: ADR-0049 (
runAs), ADR-0066 (unified authorization). - Guide: Business Logic (Flows, Approval Nodes); Who can see data / automation / interface.
- Reference: Flow, Approval.