ApprovalModule handles two kinds of asking. Someone refused at a screen or menu item asks for access; someone about to publish a guarded record asks for sign-off. Either way the ask becomes a workflow task, the approvers get it in their tray and their mail, and the person who asked is told the answer. The admin screens only call in and render what comes back.
Datatypes: access_request (one ask for a screen), task (what the workflow raises and the approver decides), workflow_definition, and message for tray notices. Workflows are covered under Workflow.
Routes
/approval/askJWT/approval/trayJWT/approval/historyJWT/approval/task/:taskId/approveJWT/approval/task/:taskId/rejectJWT/approval/request/:id/cancelJWT/approval/notices/readJWT/approval/notices/:id/readJWT/approval/setupJWTAll take the orgid header and act as the signed-in caller, matched by email.
Asking
ask takes a target and an optional request (reason, label):
- A screen or menu item —
{ kind: 'menu' | 'screen', path }. Opens anaccess_requestand fires the workflow that listsaccess_requestin its collections. Returns{ decision: 'pending', requestId, taskId, approvers, escalatesAt }, oralready-pendingif the same person already has an open ask for that path. If no enabled workflow takesaccess_request, the request is cancelled and the call answers422. - A record operation —
{ kind: 'record', operation: 'publish', datatype, id }. Fires whichever workflow lists that datatype. If none is enabled there is nothing to wait for and the answer is{ decision: 'allowed' }— the caller just publishes.
setup seeds the shipped access-approval workflow only when the org has no workflow listing access_request; ask runs it first, so it is idempotent. After that the owner may edit or replace it — its stage rule decides who approves.
Deciding
approve and reject take { note }. Only someone the task is assigned to, or an Owner, ConfigAdmin or RootAdmin, may decide; a task no longer open answers 409.
- The task's workflow must be a decision: it needs an
Approved(orPublished) stage and aRejectedstage. Anything else is work, not an approval, and answers422with reasonnot-a-decision. - Approving an access request must say how —
grant: { via: 'role' | 'group', name }. The requester is added to that role or group; access is never granted as a one-off. Without it the call answers400with reasongrant-required. - Approving a publish publishes the record: pages and posts are copied to published; other datatypes are marked
state: published. - The task moves to the workflow's yes or no stage, the
access_requestrecords the decision inapprovalChain, and the requester is notified.
request/:id/cancel lets the person who asked withdraw it while it is still pending; the approvers are told.
Tray and history
tray returns waitingOnMe (open tasks assigned to me whose workflow is a decision — workspace tasks are excluded), myRequests (my pending access requests), notices newest first, unread and counts. history returns what I decided and my requests that are no longer pending. notices/read marks all my notices read; notices/:id/read marks one, and only if it was sent to me.
AI employees
When an AI employee asks before acting, the module fires the ai-employee-approval workflow so it lands in the approver's tray like any other ask — assigned to the employee's supervisor when it has one. A decision in the tray is passed back to the employee; a decision made elsewhere cancels the tray item.
access_request
| Field | |
|---|---|
requester | email, name, userId |
app, path, label, reason | What was asked for |
status | pending · approved · rejected · cancelled |
approver | emails, since, escalatesAt |
approvalChain | Each step: round, level, approverEmail, status, assignedAt, decidedAt, decidedBy, comments, taskId |
grant | via (role · group), name — set on approval |
taskId, workflowName, submittedAt, decidedAt, decidedBy, comments |