Approvers and resolvers
A step’s approvers says who decides. Rungs does not keep users or an org chart; it asks your
application through three callbacks you pass to createRungs:
const directory: Record<string, { manager?: string; roles: string[]; active: boolean }> = { u_ana: { manager: "u_ben", roles: [], active: true }, u_ben: { roles: ["finance"], active: true }, u_cat: { roles: ["finance"], active: false }, // left the company u_dan: { roles: ["finance"], active: true },};
const rungs = createRungs({ store, clock: () => new Date(), ids: () => crypto.randomUUID(), // A role name to the users who have it. roles: (role) => Object.keys(directory).filter((id) => directory[id]?.roles.includes(role)), // Named resolvers for anything else; `request` has the requester, subject and payload. resolvers: { directManager: ({ request }) => { const manager = directory[request.requesterId]?.manager; return manager ? [manager] : []; }, }, // Inactive users are removed when a step resolves its approvers, and the reason is recorded. isActiveUser: (userId) => directory[userId]?.active ?? false,});Runs in CI:examples/docs-snippets/src/concepts/approvers.ts
| Approvers | Resolves to |
|---|---|
{ type: "users", ids: [...] } |
Exactly these users |
{ type: "role", role } |
roles(role) |
{ type: "resolver", name, params? } |
resolvers[name]({ request, stepId, params }) |
{ type: "union", of: [...] } |
Everyone the inner specs resolve to |
{ id: "review", approvers: { type: "union", of: [ { type: "resolver", name: "directManager" }, { type: "role", role: "finance" }, ], },},Runs in CI:examples/docs-snippets/src/concepts/approvers.ts
What Rungs removes, and records
Section titled “What Rungs removes, and records”When a step starts, Rungs resolves its approvers and then removes, with the reason recorded on the step and in its audit event:
- The requester, unless the flow sets
allowSelfApproval(EC-01). - Inactive users, according to
isActiveUser(EC-06). - Duplicates: a user reached twice gets one task (EC-44).
If nobody is left, Rungs does not skip the step silently. At submit, the submit is refused with
NoApproversError (EC-43); later in the flow, the step is blocked
until an admin acts (EC-42). rungs.blocked() lists such steps.
preview shows the requester who would approve, before anything is stored:
const preview = await rungs.preview({ flow: "expense", requesterId: "u_ana", subject: { type: "expense", id: "EXP-7" }, payload: { amount: 120 },});console.log(preview.steps[0]?.approvers); // ["u_ben", "u_dan"]: u_ben once, u_cat is inactiveconsole.log(preview.steps[0]?.removed); // [{ userId: "u_cat", reason: "inactive" }]Runs in CI:examples/docs-snippets/src/concepts/approvers.ts
Keep callbacks fast
Section titled “Keep callbacks fast”The callbacks run inside the command’s database transaction, while the request is locked. Read from memory or a cache where you can, and never use the command’s own database connection inside a callback: a database with a single connection, such as PGlite, would wait forever.
Resolvers that walk an organization chart (direct manager, head of department, acting holder of a vacant position) are planned. Until then, write your own resolver, as above.