Skip to content

Commit a09d797

Browse files
os-justinclaude
andauthored
docs(automation): add the fourth onEmptyApprovers member (fallback) to the skill config table and the approvals checklist (#18795)
Fixes #18527 Clause-②: no ## What The approval node's empty-slate policy has four members since PR #18525 (card #17931) landed on `main`, and two carriers still enumerated three. This PR adds the fourth member — `onEmptyApprovers: 'fallback'` with its sibling `fallbackApprovers` — to each carrier in that carrier's own register. Two files, nothing else: `git diff --stat` at `2b1d5784e8` = `docs/qa/platform-checklist/areas/approvals.json | 15 ++++++++-------` and `skills/objectstack-automation/SKILL.md | 2 +-` (9 insertions, 8 deletions; the SKILL.md change is one table row edited in place). ## The vocabulary, and where the fourth member is read from Read on `origin/main` `e5705a8ea` (the merge base of this branch), from the schema and its tests — not from the card's prose: - `packages/spec/src/automation/approval.zod.ts:884` — `onEmptyApprovers: z.enum(['admin_rescue', 'fail', 'auto_approve', 'fallback']).default('admin_rescue')` - `:886-887` `.describe(` … `"'fallback' opens the request on fallbackApprovers instead"` - `:868-869` docblock: `fallback` — open the request on {@link ApprovalNodeConfig.fallbackApprovers} instead. The only policy that NAMES people - `:878-882` docblock: The fallback slate is resolved by the SAME resolver as `approvers` … A fallback that itself resolves to nobody degrades to `admin_rescue` - `:906-907` — `fallbackApprovers: z.array(ApprovalNodeApproverSchema).min(1).optional().describe("Approvers the request opens on when onEmptyApprovers is 'fallback'")` - `:891-892` docblock: Same shape as `approvers`; `:897-898`: Required iff the policy is `'fallback'`, and refused otherwise — both arms are enforced by {@link checkFallbackApproversPairing} (the refinement at `:728-767`) - `packages/spec/src/automation/approval.test.ts:352` `describe("ApprovalNodeConfigSchema — onEmptyApprovers: 'fallback' + fallbackApprovers"` — accepts the pair, refuses `'fallback'` without a list, refuses an empty list, refuses a list under the default policy. - Runtime, because the checklist carrier asserts behaviour: `packages/plugins/plugin-approvals/src/approval-service.ts:2782-2836` (`openNodeRequest`) — under `'fallback'` the declared list goes through the same `expandApprovers` as the primary slate; if it lands a concrete person the request opens on it (`approvers = fallbackApprovers`, `:2817`) with the warn `resolved to no concrete approver — opening on the declared fallbackApprovers per onEmptyApprovers: 'fallback'.` (`:2805-2807`); if it too resolves to nobody the warn is `… AND its declared fallbackApprovers resolved to nobody either — falling back to admin_rescue.` (`:2820-2822`) and the `admin_rescue` path follows (`:2846-2851`). ## Carrier 1 — `skills/objectstack-automation/SKILL.md` :728 (the only enumerating line) :635 names the key without enumerating, :653 and :693 are examples that pick `'fail'`; only the Node Config table row enumerates. Before: ``` | `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through). Declare it explicitly on any node with an `expression` approver (linted) | ``` After (the fourth parenthetical, same register; the tail sentence unchanged): ``` | `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through), `fallback` (request opens on the sibling `fallbackApprovers` instead — same shape as `approvers`, required by this policy and refused under any other; a fallback that itself resolves to nobody degrades to `admin_rescue`). Declare it explicitly on any node with an `expression` approver (linted) | ``` Every clause of the gloss maps to a schema line above (opens on `fallbackApprovers` :868; same shape :892; required iff / refused otherwise :898; degrades to `admin_rescue` :880-881). No new row for `fallbackApprovers`, no example, no other line. **Token ratchet** (`node scripts/check-skills-token-ratchet.mjs`, convention `ceil(utf8 bytes / 4)`): before, on `e5705a8ea`: 「✓ check-skills-token-ratchet: skills/objectstack-automation/SKILL.md is 12511 tokens (ceiling 12768; headroom 257).」 — after, on `2b1d5784e8`: 「✓ check-skills-token-ratchet: skills/objectstack-automation/SKILL.md is 12567 tokens (ceiling 12768; headroom 201).」 (+56 tokens = 224 bytes; ceiling untouched, no ruling needed). Line readings (the `skills/**` rule): file 961 → 961 lines; package — all ten `SKILL.md` under `skills/` — 6145 → 6145 lines. The edited row was already the file's widest line (512 chars); it is now 700 chars. `check:doc-authoring`, `check:skill-frame-sync` and `check:skill-compatibility` are green on it. ## Carrier 2 — `docs/qa/platform-checklist/areas/approvals.json`: the enumeration exists, and it was edited Premise correction to the claim (5721118247), not to the card: the seat's grep found `onEmptyApprovers` only at :580 / :631 (prose, `'fail'` on a showcase stage) and the `variants` list at :825 (the escalation action vocabulary). On `e5705a8ea` the file has 15 hits, and the item `approvals.approver-resolution-matrix` (:1176-:1265, revision 1 dated 2026-08-30, i.e. older than the card) enumerates the members three times, plus the runtime fork's outcomes once: - :1207 `steps` — 「empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → …; 'fail' → …; 'auto_approve' → …」 - :1236 acceptance `clause` — 「the empty-slate fork honors the declared policy: admin_rescue (the default) …; 'fail' …; 'auto_approve' …」 with its `verify` at :1238 - :1256 `source` — 「(onEmptyApprovers admin_rescue|fail|auto_approve, default admin_rescue)」 - :1257 `source` — 「(the empty-slate fork: 'empty' = no concrete person, NO_APPROVERS / autoApproved / admin_rescue warn)」 The escalation `variants` at :825 are untouched (a different vocabulary). Edits, each in the line's own register: - :1207 steps — appended 「; 'fallback' (fallbackApprovers naming a STAFFED target, e.g. org_membership_level 'owner') → the request opens on the fallback slate — pending_approvers holds the fallback's resolved ids, no literal slot — and the 'opening on the declared fallbackApprovers' warn lands; a fallback that itself resolves to nobody degrades to admin_rescue with its own 'resolved to nobody either' warn」 (both warn fragments are verbatim substrings of `approval-service.ts:2807` and `:2821`). - :1236 clause — inserted 「; 'fallback' opens the request on the declared fallbackApprovers (resolved by the same resolver as approvers) and warns that the fallback fired — a fallback that resolves to nobody too degrades to admin_rescue」 before the trailing 「— 'empty' meaning no CONCRETE person …」. - :1238 verify — appended 「; the fallback request's pending_approvers holds the fallback slate, never the abandoned literal slot」 (the clause's verification half; the other three policies' verify already names the `admin_rescue` assertion). - :1256 source — 「admin_rescue|fail|auto_approve|fallback, default admin_rescue), (fallbackApprovers — same shape as approvers, required iff 'fallback' and refused under any other policy: checkFallbackApproversPairing)」. - :1257 source — 「NO_APPROVERS / autoApproved / fallbackApprovers re-expansion / admin_rescue warn」. - The checklist's own change register (`docs/qa/platform-checklist/README.md:287` 「Change — edit the fields, bump `revision`, append a `history` entry」; enforced by `scripts/check-platform-checklist.mjs:2170`): `revision` 1 → 2 and one `history` row `{ "revision": 2, "date": "2026-09-17", … "ref": "#18527" }`. The revision-1 history text (「all three onEmptyApprovers forks were untested」) is a dated record and stays as written. Not touched: the item's `title`, `knownGaps`, `negative` and the escalation item. `pnpm check:platform-checklist` → exit 0 (「check-platform-checklist: OK — 15 areas, 264 items (264 active) … symbol anchors: 577/633 resolved」, same counts as before the edit). **Reverse check of the register, from the committed head** (trap-restored, absolute paths): with only the `revision` reverted to 1 on disk (node exact-once replacement; `git diff --numstat` = `1 1`), `node scripts/check-platform-checklist.mjs` → exit 1: 「✗ approvals.json · approvals.approver-resolution-matrix: "revision" (1) must equal the last history entry's revision (2) — a semantic edit bumps both」. Restored with `git checkout HEAD -- docs/qa/platform-checklist/areas/approvals.json`: `git hash-object` = HEAD blob `4dd89a76661948c3b1108e35a4a5df92d7a6283a`, `git diff HEAD` empty, `git status --porcelain` empty, validator exit 0 again. ## Changeset `skip-changeset`, decided by the repo's own gate — the 「Require a changeset (or the skip-changeset label)」 step of `.github/workflows/pr-automation.yml` (:757-:780): route 2 「It releases nothing (.github/, .claude/, skills/, docs/, content/, …) → apply the 'skip-changeset' label. PREFERRED」, with `skills/**` spelled out at :771. Measured on this tree: a walk over the 83 tracked `package.json` manifests finds 0 whose `files[]` names a `skills` path and 0 naming `docs/qa`/`platform-checklist` (positive control: 70 name `dist`); no `skills/*/package.json`, no `docs/qa/package.json`. The 14 changeset-triggered families the derivation lists under 「Once a changeset exists」 do not apply (no changeset written). `node scripts/check-changeset-fixed.mjs` (roster family) → exit 0. ## Gates (worktree at `2b1d5784e8`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — change set derived from git, 2 paths vs merge base `e5705a8ea`; 24 commands derived, all 24 run with redirect-then-`$?` capture; `--ran` reconcile: 「Run reconciliation — 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN.」, exit 0. `pnpm --filter @objectstack/lint run check:doc-formula-expressions` was run after building `@objectstack/lint...` under `scripts/pm/os-verify-lock.sh` (「VERDICT command-exit 0 · held the lock 137s · waited 0s」). ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/spec run check:skill-docs :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:platform-checklist :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-compatibility :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Beyond the derived set: `pnpm lint` (repo-wide, `eslint . --no-inline-config`) → exit 0 at `2b1d5784e8`; roster families `node scripts/check-published-list-mirrors.mjs` → exit 0 and `node scripts/check-platform-checklist-watchdog.mjs` → exit 0; control-character self-scan (`grep -naP` over both files) → no match. No ① dependency-closure build and no ② package test are owed — the diff touches no package. **Governed**: `node scripts/pm/check-governed-merges.mjs --test skills/objectstack-automation/SKILL.md docs/qa/platform-checklist/areas/approvals.json` → exit 3, `skills/**` ×1 hit (the published skills catalog), 「One hit governs the whole PR」. Served tier: MANDATORY, derived from the `skills/**` surface by `dispatch-gates.mjs --tier`. Draft at the governed terminal; the seat's four-piece, the maintainer lands. ## Acceptance notes - noted, not filed — 承接者: the skills seat: the claim's carrier-2 reading (three `onEmptyApprovers` hits) undercounted a 15-hit file; the enumerating item predates the card. Corrected above; nothing to file. - noted, not filed — 承接者: 无: the `onEmptyApprovers` table row is now 700 characters on one line; the file's table rows are single lines by construction and no gate reads line width here. Not a defect. ## 维护者速读(草稿) **改了什么**:审批节点空名单策略 `onEmptyApprovers` 自 PR #18525 起有四个成员,两处仍只列三个。① `skills/objectstack-automation/SKILL.md` 第 728 行 Node Config 表的该行,按本行既有的括号注释格式补上第四个成员 `fallback`(请求改在兄弟键 `fallbackApprovers` 上开出;与 `approvers` 同形;此策略下必填、其它策略下拒收;备选名单也解析为空时退化为 `admin_rescue`)。② `docs/qa/platform-checklist/areas/approvals.json` 里 `approvals.approver-resolution-matrix` 这一条的空名单分叉:steps、验收子句及其 verify、两条 source 引用各补上 `fallback` 一腿,并按清单自身规则 revision 1→2、加一条 history。两文件之外一字未动。 **为什么改**:词表是 spec 声明的(`approval.zod.ts:884`),已发布 skill 与测试清单各自枚举它;少列一个成员,AI 作者按 skill 写元数据就不会知道有 `fallback`,测试清单也测不到这条分叉。语义全部取自 schema docblock 与运行时分叉,不取卡面。 **风险与代价(含回滚)**:纯文档/清单改动,不发布任何 npm 内容(`files[]` 实测 0 命中);skills token 棘轮 12511→12567(上限 12768,余量 201),不动上限。回滚 = revert 这一个提交。 **席位意见**: **你要做的**:确认第 728 行新增括号注释与 schema 语义一致(见上文逐句对照),点合并;怎么验:`node scripts/check-skills-token-ratchet.mjs` 与 `pnpm check:platform-checklist` 两条门禁的判定行已在正文引用。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6427ee2 commit a09d797

2 files changed

Lines changed: 9 additions & 8 deletions

File tree

‎docs/qa/platform-checklist/areas/approvals.json‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@
11771177
"title": "Every approver type resolves the slate it declares — department includes all descendants, deprecated role warns and resolves, and a slate with no concrete person takes the declared onEmptyApprovers fork, never a fake approval",
11781178
"since": "v16",
11791179
"status": "active",
1180-
"revision": 1,
1180+
"revision": 2,
11811181
"priority": "P1",
11821182
"surface": "api",
11831183
"personas": ["dev admin (authors the scratch flows; org owner)", "Ada Auditor / Mei Phone (real logins since #9308 — directory rows the graph legs resolve to)"],
@@ -1204,7 +1204,7 @@
12041204
"manager leg: the submitter's manager_id resolves; user leg: the literal id resolves",
12051205
"role leg: author {type:'role', value:'owner'} (it still parses for the deprecation window); read the boot/trigger log for the ADR-0090 D3 deprecation warn naming the canonical spelling, and assert the slate equals what {type:'org_membership_level', value:'owner'} resolves on the same org",
12061206
"queue leg: a stored flow carrying {type:'queue'} (author it directly through the metadata channel if the designer refuses — the designer withholding it IS the xEnumDeprecated contract) — trigger: the slot resolves to nobody, the request opens carrying only the literal queue:<value> slot, and the log carries the #3508 'not implemented — the slot resolves to nobody' warn",
1207-
"empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → the request still opens (decidable only via the #3424 privileged override) and the loud 'resolved to no concrete approver' warn lands; 'fail' → the run fails with NO_APPROVERS and no request row; 'auto_approve' → NO request row, the run continues down the approve edge with output.autoApproved=true and the auto-approve warn lands",
1207+
"empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → the request still opens (decidable only via the #3424 privileged override) and the loud 'resolved to no concrete approver' warn lands; 'fail' → the run fails with NO_APPROVERS and no request row; 'auto_approve' → NO request row, the run continues down the approve edge with output.autoApproved=true and the auto-approve warn lands; 'fallback' (fallbackApprovers naming a STAFFED target, e.g. org_membership_level 'owner') → the request opens on the fallback slate — pending_approvers holds the fallback's resolved ids, no literal slot — and the 'opening on the declared fallbackApprovers' warn lands; a fallback that itself resolves to nobody degrades to admin_rescue with its own 'resolved to nobody either' warn",
12081208
"throughout: read the trigger-time log — every graph expansion to nobody must carry the #3807 'expanded to nobody' warn naming type and value"
12091209
],
12101210
"acceptance": [
@@ -1233,9 +1233,9 @@
12331233
"evidence": "the warn line + the two equal slates"
12341234
},
12351235
{
1236-
"clause": "the empty-slate fork honors the declared policy: admin_rescue (the default) opens the request + warns loudly (decidable only via the privileged override); 'fail' kills the node with NO_APPROVERS and opens nothing; 'auto_approve' opens nothing and resumes down the approve edge with output.autoApproved=true — 'empty' meaning no CONCRETE person, so a slate of only type:value literals takes the fork too",
1236+
"clause": "the empty-slate fork honors the declared policy: admin_rescue (the default) opens the request + warns loudly (decidable only via the privileged override); 'fail' kills the node with NO_APPROVERS and opens nothing; 'auto_approve' opens nothing and resumes down the approve edge with output.autoApproved=true; 'fallback' opens the request on the declared fallbackApprovers (resolved by the same resolver as approvers) and warns that the fallback fired — a fallback that resolves to nobody too degrades to admin_rescue — 'empty' meaning no CONCRETE person, so a slate of only type:value literals takes the fork too",
12371237
"oracle": "api",
1238-
"verify": "per policy: request-row presence/absence, run outcome, and the matching warn/error text ('resolved to no concrete approver…'); the admin_rescue request's pending_approvers holds only the literal slot",
1238+
"verify": "per policy: request-row presence/absence, run outcome, and the matching warn/error text ('resolved to no concrete approver…'); the admin_rescue request's pending_approvers holds only the literal slot; the fallback request's pending_approvers holds the fallback slate, never the abandoned literal slot",
12391239
"evidence": "per-policy request/run reads + log lines"
12401240
},
12411241
{
@@ -1253,14 +1253,15 @@
12531253
],
12541254
"traps": ["seed-data-thin", "wrong-persona"],
12551255
"source": [
1256-
"packages/spec/src/automation/approval.zod.ts#ApproverType (ApproverType, 10 members incl. deprecated role + dead queue), (DEPRECATED_APPROVER_TYPES role→org_membership_level + canonicalApproverType), (NON_AUTHORABLE_APPROVER_TYPES), (onEmptyApprovers admin_rescue|fail|auto_approve, default admin_rescue)",
1257-
"packages/plugins/plugin-approvals/src/approval-service.ts#expandApprovers (expandApprovers — per-spec group tagging; expression resolved outside the flat contract), (resolveApproverSpec — role warn, per-type branches, queue warn, #3807 expanded-to-nobody warn, literal type:value fallback), (expandBusinessUnitUsers — active-descendant BFS + membership union), (the empty-slate fork: 'empty' = no concrete person, NO_APPROVERS / autoApproved / admin_rescue warn)",
1256+
"packages/spec/src/automation/approval.zod.ts#ApproverType (ApproverType, 10 members incl. deprecated role + dead queue), (DEPRECATED_APPROVER_TYPES role→org_membership_level + canonicalApproverType), (NON_AUTHORABLE_APPROVER_TYPES), (onEmptyApprovers admin_rescue|fail|auto_approve|fallback, default admin_rescue), (fallbackApprovers — same shape as approvers, required iff 'fallback' and refused under any other policy: checkFallbackApproversPairing)",
1257+
"packages/plugins/plugin-approvals/src/approval-service.ts#expandApprovers (expandApprovers — per-spec group tagging; expression resolved outside the flat contract), (resolveApproverSpec — role warn, per-type branches, queue warn, #3807 expanded-to-nobody warn, literal type:value fallback), (expandBusinessUnitUsers — active-descendant BFS + membership union), (the empty-slate fork: 'empty' = no concrete person, NO_APPROVERS / autoApproved / fallbackApprovers re-expansion / admin_rescue warn)",
12581258
"examples/app-showcase/src/automation/flows/approver-bindings.flow.ts (the #3508 DESIGNER specimen — draft, empty values, its docblock is the evidence the runtime legs need scratch flows)",
12591259
"examples/app-showcase/src/data/seed/index.ts (BU tree seeded; membership is a runtime admin action by design)",
12601260
"#3424 (privileged override — the admin_rescue escape hatch), #3447 P2, #3508, #3807, ADR-0090 D3"
12611261
],
12621262
"history": [
1263-
{ "revision": 1, "date": "2026-08-30", "change": "initial — the approver-type matrix had no item: siblings cover position (seeded flows) and org_membership_level/expression (dynamic-approval) but department-with-descendants, team, field fan-out, manager, literal user, the role deprecation window, the dead queue slot and all three onEmptyApprovers forks were untested. One register correction: approver-bindings.flow.ts is a draft designer specimen (empty values, status draft by documented design), not a runnable user/manager/field fixture — every uncovered leg needs a scratch active flow, recorded in knownGaps", "ref": "#sweep-2026-08-30" }
1263+
{ "revision": 1, "date": "2026-08-30", "change": "initial — the approver-type matrix had no item: siblings cover position (seeded flows) and org_membership_level/expression (dynamic-approval) but department-with-descendants, team, field fan-out, manager, literal user, the role deprecation window, the dead queue slot and all three onEmptyApprovers forks were untested. One register correction: approver-bindings.flow.ts is a draft designer specimen (empty values, status draft by documented design), not a runnable user/manager/field fixture — every uncovered leg needs a scratch active flow, recorded in knownGaps", "ref": "#sweep-2026-08-30" },
1264+
{ "revision": 2, "date": "2026-09-17", "change": "the empty-slate fork gained a fourth policy — onEmptyApprovers: 'fallback' + fallbackApprovers (PR #18525, #17931): the request opens on the declared fallback slate, resolved by the same resolver as approvers, and a fallback that resolves to nobody degrades to admin_rescue. Added that leg to the steps, the fork clause and its verify, and the two source citations; the three existing legs are unchanged", "ref": "#18527" }
12641265
]
12651266
},
12661267
{

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data**
725725
| `minApprovals` | Approvals required — total for `quorum`, per group for `per_group`. Omitted ⇒ ALL resolvable approvers under `quorum`, `1` per group; clamped at runtime so a misconfiguration can never deadlock |
726726
| `lockRecord` | Lock the triggering record from edits while pending. Default `true` |
727727
| `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) |
728-
| `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through). Declare it explicitly on any node with an `expression` approver (linted) |
728+
| `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through), `fallback` (request opens on the sibling `fallbackApprovers` instead — same shape as `approvers`, required by this policy and refused under any other; a fallback that itself resolves to nobody degrades to `admin_rescue`). Declare it explicitly on any node with an `expression` approver (linted) |
729729
| `decisionOutputs` | Decision outputs a decision may carry (author declares, approvers fill values). Entries are bare keys (free-text input) **or typed declarations** `{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple?, required? }` — a typed entry renders the matching record picker in the decision dialog (`multiple` collects an id array). Accepted outputs resume the run as `<nodeId>.<key>` variables; undeclared keys reject the decision; `decision`/`requestId` reserved |
730730
| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `timeoutHours` is **calendar (wall-clock) hours** — nights, weekends and holidays count; the platform ships no business-hours calendar. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) |
731731
| `maxRevisions` | ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge |

0 commit comments

Comments
 (0)