diff --git a/.changeset/29910-no-submitter-returned-docs.md b/.changeset/29910-no-submitter-returned-docs.md new file mode 100644 index 00000000000..80cef7b0adf --- /dev/null +++ b/.changeset/29910-no-submitter-returned-docs.md @@ -0,0 +1,10 @@ +--- +'@objectstack/spec': patch +--- + +docs(approvals): say how a request with no submitter leaves the `returned` state + +Clause-②: no +Metadata-protocol: none + +- **`@objectstack/spec`: comment text only.** The `IApprovalService` docblocks on `recall`, `sendBack` and `resubmit` now state the path for a request with no submitter (its run's trigger resolved no user): nobody can recall it once it is `returned`, a system-context `resubmit` is its way out, and the next round opens `pending` with no submitter, where the system or the override actor may recall it. The `resubmit` docblock now names the system context it already admits. No key, type, schema or behaviour changes. diff --git a/content/docs/automation/approvals.mdx b/content/docs/automation/approvals.mdx index 41bd0f8ab8f..cbc111d3419 100644 --- a/content/docs/automation/approvals.mdx +++ b/content/docs/automation/approvals.mdx @@ -773,6 +773,26 @@ over approval items), the recall also wakes the batch, which records the item's slot as the cancelled marker `{ $status: 'cancelled', $itemRunId }` and moves on to its next item or its end. +**A request with no submitter.** A run whose trigger resolved no user (a +schedule, time-relative or api trigger, or a record-change fired by a system +write) opens its request with no submitter. An approver can still send it back, +but once it is `returned` nobody can recall it: `recall` reaches a `returned` +request for its submitter alone, never the system or the admin override, and no +viewer sees Resubmit or Recall. Its one way out is a `resubmit` made by +server-side code in a system context (`IApprovalService.resubmit` with +`isSystem: true`, and an `actorId` naming the acting identity, which a system +caller must supply). That opens the next round `pending`, again with no +submitter, where the system or the admin override can recall it. On a flow whose +trigger never carries a user (schedule, time-relative or api), keep the `revise` +edge into its `approval_revise` node only when server-side code will make that +resubmit: the window is the only shape send-back accepts, and nothing in the run +can end it. Otherwise leave the `revise` edge off, so send-back is refused and +approvers reject instead. A record-change flow has no submitter only when a +system write fired it; such a round, once sent back, waits in the window for +that server-side resubmit, or a platform operator's +`POST /api/v1/automation/:name/runs/:runId/cancel` discards the run and leaves +the request `returned`. + The window is deliberately **not** an ordinary `wait`: `resubmit` is what authorizes (submitter-only), orders (latest round) and records (an audit row) the continuation, and refuses when another request is already pending on the record — diff --git a/packages/spec/src/contracts/approval-service.ts b/packages/spec/src/contracts/approval-service.ts index 65bfdcb2f3b..ee6c2039ed9 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -1035,9 +1035,16 @@ export interface IApprovalService { * - `returned` (ADR-0044) — the submitter ALONE, and only on the LATEST * `returned` request of its (run, node): they abandon the revision window * instead of resubmitting, the request flips `returned → recalled`, and - * the run resumes down `reject` the same way. Neither the override nor the + * the run is cancelled (the revise window has no `reject` edge to resume + * down). Neither the override nor the * system arm reaches a `returned` request (#12775); an override actor is * refused there exactly as any other non-submitter. + * + * A request with no submitter (its run's trigger resolved no user) has no + * one on the `returned` arm. Its way out of `returned` is a system-context + * {@link resubmit}, which opens the next round `pending`, again with no + * submitter, where the `pending` arm's system context or override actor may + * recall it. */ recall(requestId: string, input: ApprovalRecallInput, context: ExecutionContext): Promise; @@ -1048,6 +1055,14 @@ export interface IApprovalService { * {@link resubmit}s). Requires the approval node to declare a `revise` * out-edge; past the node's `maxRevisions` budget the request auto-rejects * instead. Audited as `revise`. + * + * A request with no submitter is sent back the same way, but only a + * system-context {@link resubmit} moves it on from `returned` (see + * {@link recall}): a flow whose trigger never carries a user (schedule, + * time-relative, api) declares the `revise` edge only when server-side code + * will make that call. A record-change flow has no submitter only when a + * system write fired it; that round, sent back, waits for the call, or an + * operator's `cancelRun` discards the run and leaves the request `returned`. */ sendBack( requestId: string, @@ -1057,7 +1072,8 @@ export interface IApprovalService { /** * ADR-0044 resubmit after rework. Valid on the LATEST `returned` request of - * its (run, node), submitter-only. Resumes the suspended run from the wait + * its (run, node), by the submitter or a system context (the system context + * alone on a request with no submitter). Resumes the suspended run from the wait * point; traversal re-enters the approval node via the declared back-edge * and opens the next round's request (fresh approver slate, record * re-locks). Audited as `resubmit` on the returned request. @@ -1132,8 +1148,8 @@ export interface IApprovalService { * call left the row where none of them can issue the continuation it owes. * ⚠️ Not because the row is never `returned`: a stranded send-back leaves * it exactly there, and {@link resubmit} is still no way back — it is - * submitter-only, and it owes the `resubmit` edge where the stranded - * continuation was the `revise` one. Meanwhile a generic engine resume is + * by the submitter or a system context only, and it owes the `resubmit` + * edge where the stranded continuation was the `revise` one. Meanwhile a generic engine resume is * refused at a node declaring `resumeAuthority: 'service'`. The only verb * left was cancel, which discards the branch's downstream work. This is the * issuer that exits that dead end.