Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/29910-no-submitter-returned-docs.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 20 additions & 0 deletions content/docs/automation/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
24 changes: 20 additions & 4 deletions packages/spec/src/contracts/approval-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApprovalRecallResult>;

Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Loading