From 630966a2b2216d4b0029b79b183f019a353f77ec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 14:47:40 +0000 Subject: [PATCH 1/2] docs(approvals): say how a request with no submitter leaves returned The approvals guide and the IApprovalService recall / sendBack / resubmit docblocks now state the path for a request whose run's trigger resolved no user: nobody can recall it once 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 guide says when such a flow should declare its revise edge. Comments and prose only; no behaviour change. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- .changeset/29910-no-submitter-returned-docs.md | 10 ++++++++++ content/docs/automation/approvals.mdx | 14 ++++++++++++++ packages/spec/src/contracts/approval-service.ts | 14 +++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/29910-no-submitter-returned-docs.md 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..d73d1d5031e 100644 --- a/content/docs/automation/approvals.mdx +++ b/content/docs/automation/approvals.mdx @@ -773,6 +773,20 @@ 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`). That opens the next round `pending`, again with no submitter, +where the system or the admin override can recall it. On a flow with such a +trigger, 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. + 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..3de21c6d2f3 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -1038,6 +1038,12 @@ export interface IApprovalService { * the run resumes down `reject` the same way. 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 +1054,11 @@ 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 resolves no user declares the + * `revise` edge only when server-side code will make that call. */ sendBack( requestId: string, @@ -1057,7 +1068,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. From f4d534bd32cb18f6a350e67a7eb88469d78b53d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 15:47:56 +0000 Subject: [PATCH 2/2] docs(approvals): fold in the contract review's should-fixes Scope the "declare revise only with a server-side resubmit" rule to the triggers that never carry a user (schedule, time-relative, api), and say what a record-change flow fired by a system write faces: the sent-back round waits for that resubmit, or an operator cancel discards the run and leaves the request returned. The recall docblock's returned arm now says the run is cancelled (the revise window has no reject edge), the reissueContinuation docblock names the system context resubmit admits, and the guide says a system-context resubmit must name an actorId. Comments and prose only. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- content/docs/automation/approvals.mdx | 18 ++++++++++++------ .../spec/src/contracts/approval-service.ts | 14 +++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/content/docs/automation/approvals.mdx b/content/docs/automation/approvals.mdx index d73d1d5031e..cbc111d3419 100644 --- a/content/docs/automation/approvals.mdx +++ b/content/docs/automation/approvals.mdx @@ -780,12 +780,18 @@ 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`). That opens the next round `pending`, again with no submitter, -where the system or the admin override can recall it. On a flow with such a -trigger, 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. +`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 diff --git a/packages/spec/src/contracts/approval-service.ts b/packages/spec/src/contracts/approval-service.ts index 3de21c6d2f3..ee6c2039ed9 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -1035,7 +1035,8 @@ 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. * @@ -1057,8 +1058,11 @@ export interface IApprovalService { * * 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 resolves no user declares the - * `revise` edge only when server-side code will make that call. + * {@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, @@ -1144,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.