Skip to content

Commit 6a9593d

Browse files
committed
docs(approvals): document the full decision surface, send-back/resubmit, declared actions + viewer gating
The approvals guide only covered approve/reject. Bring it up to date with what has shipped: - Full decision-verb surface as a table (approve/reject/reassign/revise/ request-info/remind/recall/resubmit/comment) with route, who-may-call, and effect — including the /revise and /resubmit routes. - Send-back → resubmit revision lifecycle (ADR-0044), incl. the maxRevisions auto-reject and the required `revise` edge. - Decision file attachments (`attachments: string[]` on the audit row). - "Acting on requests in the console": the verbs ship as server-declared `sys_approval_request` actions rendered by the generic console action runtime (the inbox), so decisions need no per-action UI. - The server-computed per-viewer block (`viewer.can_act` / `viewer.is_submitter`) that gates the declared actions, and why a submitter never sees approver buttons while a position-addressed approver is never wrongly hidden. - Generalize the FORBIDDEN error-code wording to cover submitter-only verbs. Docs-only; no code or schema change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
1 parent 45a3769 commit 6a9593d

1 file changed

Lines changed: 66 additions & 1 deletion

File tree

content/docs/automation/approvals.mdx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ or the call returns 403 (`FORBIDDEN: actor '…' is not a pending approver`); a
213213
request that isn't pending returns 409 (`INVALID_STATE`). Always go through
214214
these endpoints — never resume the flow run directly.
215215

216+
A decision may carry **file attachments**`attachments: string[]` of `sys_file`
217+
ids — recorded on its audit row (e.g. a signed contract on the approve):
218+
219+
```jsonc
220+
POST .../approve { "comment": "Signed.", "attachments": ["file_abc", "file_def"] }
221+
```
222+
216223
<Callout type="info">
217224
**With `behavior: 'unanimous'`, an approve is not the end.** Every approver but
218225
the last only trims `pending_approvers`; the request stays `pending`
@@ -231,6 +238,64 @@ Statuses in full: `pending`, `approved`, `rejected`, `recalled`, `returned`.
231238

232239
</Steps>
233240

241+
### Beyond approve/reject — the full decision surface
242+
243+
`approve` / `reject` move the flow; the rest are thread interactions and
244+
continuity levers on the same request. All are `POST
245+
/api/v1/approvals/requests/:id/<verb>`; the service enforces who may call each:
246+
247+
| Verb | Route | Who | Effect |
248+
|:---|:---|:---|:---|
249+
| `approve` / `reject` | `/approve` `/reject` | pending approver | Records the decision (finalizes per `behavior`). Accepts `comment`, `attachments`. |
250+
| `reassign` | `/reassign` | pending approver | Hands one slot to another user (`to`); the request stays pending. |
251+
| `revise` (send back) | `/revise` | pending approver | Ends this round as **`returned`**, unlocks the record for rework (ADR-0044). |
252+
| `request-info` | `/request-info` | pending approver | Asks the submitter for more info; the request **stays pending**. `comment` required. |
253+
| `remind` | `/remind` | submitter | Nudges the pending approvers (publishes a notification topic). |
254+
| `recall` | `/recall` | submitter | Withdraws a `pending` (or abandons a `returned`) request → **`recalled`**. |
255+
| `resubmit` | `/resubmit` | submitter | After a send-back, re-enters the approval node and opens the next round (ADR-0044). |
256+
| `comment` | `/comment` | participant | Free-form reply on the request thread; accepts `attachments`. |
257+
258+
**Send-back → resubmit (ADR-0044).** An approver who wants changes rather than a
259+
hard reject calls `revise`: the round finalizes `returned`, the record unlocks,
260+
and the run parks at a wait point. The submitter reworks the record and
261+
`resubmit`s (a fresh round opens for all approvers) or `recall`s (abandons it).
262+
Past the node's `maxRevisions` budget (default 3) a send-back **auto-rejects**
263+
instead. The flow's approval node must declare a `revise` edge for send-back to
264+
be available.
265+
266+
### Acting on requests in the console
267+
268+
You rarely call these routes by hand. The decision verbs above ship as
269+
**server-declared actions** on `sys_approval_request` (`actions[]` in its object
270+
metadata) — `approval_approve`, `approval_reject`, `approval_reassign`,
271+
`approval_send_back`, `approval_request_info`, `approval_remind`,
272+
`approval_recall`, `approval_resubmit`. The console's generic action runtime
273+
renders and executes them wherever the object is surfaced — the **Approvals
274+
inbox** included — so a decision (comment, a file-upload for `attachments`, the
275+
reassign user-picker) is collected by the generic action dialog with **no
276+
per-action UI code**. New decision capabilities ship as backend metadata, not
277+
hand-written buttons.
278+
279+
Each action's visibility is gated by a **server-computed per-viewer block** the
280+
service attaches to every request it returns:
281+
282+
```jsonc
283+
// getRequest / listRequests responses carry, per the calling user:
284+
"viewer": { "can_act": true, "is_submitter": false }
285+
```
286+
287+
- `can_act` — the caller is a **current pending approver** (their id is in the
288+
resolved `pending_approvers` while the request is `pending`). This is the same
289+
check the decision routes authorize with, so it already reflects
290+
position/team/manager resolution.
291+
- `is_submitter` — the caller submitted the request.
292+
293+
Approver actions gate on `record.viewer.can_act`, submitter levers
294+
(remind/recall/resubmit) on `record.viewer.is_submitter`. So a submitter viewing
295+
their own pending request never sees Approve/Reject/Reassign (buttons the server
296+
would 403 anyway), and a position-addressed approver is never wrongly hidden.
297+
The service stays the sole authority — the predicate only trims the UI.
298+
234299
### Timeouts and escalation
235300

236301
`escalation` is real, not decorative: set `enabled: true` and `timeoutHours`,
@@ -286,7 +351,7 @@ in-flight request, `reassign` hands one slot to another user directly.
286351
| Code | HTTP | Meaning |
287352
|:---|:---|:---|
288353
| `VALIDATION_FAILED` | 400 | Malformed request |
289-
| `FORBIDDEN` | 403 | Actor isn't a pending approver |
354+
| `FORBIDDEN` | 403 | Actor may not take this action (not a pending approver, or not the submitter for a submitter-only verb) |
290355
| `REQUEST_NOT_FOUND` | 404 | No such request |
291356
| `DUPLICATE_REQUEST` | 409 | A pending request already exists for this record |
292357
| `INVALID_STATE` | 409 | The request is no longer pending |

0 commit comments

Comments
 (0)