Skip to content

Commit 96f1258

Browse files
docs(skills): objectstack-automation teaches the emailField recipient, flow requiredPermissions, excludeSubmitter and the screen file field (#26390)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8GtSu7285zjAYqM8VgQB5Gr)_ Fixes #26298 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:objectstack-automation 技能包补上四条审计缺口 —— `notify` 收件人的第四种写法 `{ emailField }`(给非用户的申请人发邮件;CEL 信封算出的地址若无用户持有,节点运行时失败)、流程级 `requiredPermissions`(谁能启动流程)、审批节点 `excludeSubmitter`(提交人不能批自己的单;与 `auto_approve` 并写被拒)、屏幕字段 `type: 'file'`(续跑拿到 `sys_file` id,后续写入记录即认领文件);每条配一条 eval。 - 为什么改:这四个键在 spec 里已存在,技能包零命中;AI 作者照现有文本写出的 `recipients` 信封现在会在运行时失败,不写 `excludeSubmitter` 则提交人可自批。 - 风险与回滚:只改已发布技能文本与 eval,不改运行时;token 天花板三行按实测余额上调(你 2026-10-27 的裁决 A)。回滚 = revert 本 PR。 - 先看:`skills/objectstack-automation/SKILL.md`(`notify` 代码块与 `requiredPermissions` 引言段)、`references/state-machines-and-approvals.md`(配置表新行、删掉的两条最佳实践)、`scripts/check-skills-token-ratchet.mjs`(三行上限)。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs` —— 预期末行 `81 authored bundle file(s) within their ceilings`,automation 三行 headroom 0。 - 需要你做的:批准(Approve)或「Merge when ready」—— `skills/**` 是 Tier H,席位不入队。 ## What changes Four rows from the skills gap audit (#26184, Part A § objectstack-automation, rows A1–A4), each taught in the fewest lines an AI author needs: | Row | Where | Teaching | |:--|:--|:--| | A3 `{ emailField }` | `SKILL.md` `notify` block, the `recipients` line | the fourth recipient form `{ emailField: 'applicant_email' }` (an `email`-typed field of the trigger record, for a non-user), `channels: 'email'` alone beside it, and that a CEL envelope must yield user ids — an address no user holds fails the node (`packages/spec/src/automation/io-node-config.zod.ts` `NotifyEmailFieldRecipientSchema`, the `recipients` describe and the `channels` refinement) | | A1 `requiredPermissions` | `SKILL.md`, a blockquote after *Elevate the write, not the flow* | flow-level `requiredPermissions` is checked at every door that starts the flow by name; an action starting it inherits the list; a `subflow` node is not a door; gate an elevated `screen` / `api` flow here, never on `current_user.positions` (`flow.zod.ts` `requiredPermissions`, ADR-0066 D4) | | A4 screen `file` field | `SKILL.md` node table, the `screen` row | `config.fields[]` shape; `type: 'file'` renders the upload widget and resumes with the `sys_file` id bound to `name`; a later `create_record` / `update_record` writes it into a `file` field, which claims the file — collect first, write after the last screen (`builtin-node-config.zod.ts` `ScreenFieldConfigSchema.type`) | | A2 `excludeSubmitter` | `references/state-machines-and-approvals.md`, the Node Config table | drops the run's submitter from the resolved slate; an emptied slate takes the `onEmptyApprovers` route; `onEmptyApprovers: 'auto_approve'` is refused beside it (`approval.zod.ts` `excludeSubmitter`, `checkExcludeSubmitterPolicyPairing`) | Three eval prompts in `evals/flows-triggers-approvals.json`, in the file's existing shape: #6 the card's acceptance eval (email the applicant → `recipients: { emailField: … }`), #7 `requiredPermissions` + `excludeSubmitter` on one approval, #8 the screen `file` field written by a later `create_record`. **Paid down first (surviving homes named):** - `SKILL.md` CRM Blueprint, the *Approval flow* row restated `approvers` / `behavior` / `lockRecord` / `approvalStatusField` and the `approve` / `reject` branching — cut to a pointer; the surviving home is `references/state-machines-and-approvals.md` § Node Config (`ApprovalNodeConfigSchema`) and § Branching, side-effects & rejection. - `references/state-machines-and-approvals.md` Approval Best Practices 1 (*Gate entry on the edge*) and 4 (*Model rejection as a visible branch*) restated § Branching's *Conditional step*, *Roll back on reject* and *Hard reject* bullets and the section lead "wired on the graph, not in node config" — deleted; items 2, 3, 5 (each carrying a rationale found nowhere else) stay, renumbered. - Nothing else is removed. `SKILL.md` line 1 (the `version:` stamp PR #25640 owns) is untouched. **Token ratchet (`scripts/check-skills-token-ratchet.mjs`), the measured remainder per file, under the maintainer's ruling.** The decision card asked 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」; Warren's tap, 2026-10-27, verbatim: 「A:先削重复,余额按实测提高」. | File | lines before → after | tokens before → after | ceiling before → after | |:--|:--|:--|:--| | `skills/objectstack-automation/SKILL.md` | 432 → 441 | 5771 → 6004 (+233 net; ~17 paid by the Blueprint cut; 9 absorbed by headroom) | 5780 → 6004 (+224) | | `skills/objectstack-automation/references/state-machines-and-approvals.md` | 383 → 380 | 5555 → 5587 (+32 net; ~73 paid by the two deleted practices; 14 absorbed) | 5569 → 5587 (+18) | | `skills/objectstack-automation/evals/flows-triggers-approvals.json` | 55 → 85 | 1255 → 1823 (+568; nothing in the file to delete) | 1255 → 1823 (+568) | | `references/examples-flows.md`, `references/_index.md` (generated) | 134, 46 (unchanged) | 1421, 919 (unchanged) | — | | **Whole package** (`SKILL.md` + `references/**/*.md` + `rules/**/*.md`) | **995 → 1001** | **13666 → 13931** (authored 12747 → 13012) | — | ## Why The audit measured 0 hits in the package for `requiredPermissions`, `excludeSubmitter` and `emailField`; `SKILL.md`'s `recipients` comment read "id, list, or envelope", which since `d1e329e96d` sends an author mailing an applicant into a run-time failure; the approval config table had no `excludeSubmitter` row, so a submitter could approve their own request; screen field types were not enumerated. The premise was re-read on `origin/main` at `756220c4` before the first edit and holds (the seat's read-only check of 2026-10-27 00:36Z, re-measured here: `grep -rn` of the three keys over `skills/objectstack-automation/` → 0 hits; the screen `file` type's spec location is `ScreenFieldConfigSchema.type` in `packages/spec/src/automation/builtin-node-config.zod.ts`, which the seat had not located). ## Tests No runtime code changes; the eval file parses (`python3 -c json.load` → ok) and the package gates below read the new text. The three eval prompts are the test surface for the teachings. ## Acceptance notes - `check:skill-examples` has three surfaces; the skills + docs surface is the one this PR touches. Its verdict needs `packages/spec`, `@objectstack/client` and `@objectstack/client-react` built first (it refuses with exit 3, `PREREQUISITE NOT MET`, otherwise) — see Verification for the run against the built tree. - Six commits the card cites (`49653f9181`, `4bcc4df649`, `6021b5e837`, `d1e329e96d`, `6d9d3ef857`, `f1ddace887`) are outside this container's shallow clone (`git cat-file -e` → missing); the spec text on `main` was read directly instead, so the teachings quote what `main` holds, not the commits. - No out-of-scope findings: nothing reproducible, no contract breach, no new authoring trap beyond the four the card names. ## Verification Gate list derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final tree (`83b5ec5e`), 54 commands, every exit code captured before any pipe, reconciled with `--ran`. <details><summary>Derived gates — 53 commands, 52 run green, 1 NOT MEASURED (<code>--ran</code> exit 0)</summary> - `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack` → `53 derived famil(ies) accounted for — 52 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`, exit 0; change set `4 path(s) vs merge base 756220c`, `changed lines: 85 (+72 / -13)`. - NOT MEASURED: `pnpm check:dual-build-cjs-loads` → exit 3, `PREREQUISITE NOT MET — some package has no dist/` (33 packages unbuilt in this container; a repo-wide build is CI's run). CI's `Lint & Repo Gates` carries it. - Two gates raced this thread's own concurrent builds in the batch and were re-run alone, the re-run codes recorded: `node scripts/check-comment-mask-corpus.mjs` (first run `ENOENT … packages/spec/.examples-build/…`, a transient dir the concurrent `check:skill-examples` run was materialising; re-run exit 0, `18308 files, 0 disagree, 0 unparseable`); `pnpm check:dts-closure` (first run while `@objectstack/client-react` was mid-build under the verify lock; re-run exit 0, `38 built package(s) swept - 160/160 declared declaration file(s) present`). - `node scripts/check-skills-token-ratchet.mjs` exit 0 — `81 authored bundle file(s) within their ceilings`; automation rows `6004 / 6004 (+0)`, `1823 / 1823 (+0)`, `5587 / 5587 (+0)`; `--self-test` exit 0, `79 cases pass`. - `pnpm check:pm-dispatch-gates` exit 0, `pnpm check:pm-governed-merges` exit 0, `pnpm check:ratchet-remedy-authority` exit 0 (all in the batch). </details> <details><summary>The card's three package gates and the liveness gate</summary> - `pnpm --filter @objectstack/spec check:skill-docs` exit 0 — `✅ Skill docs in sync`. - `pnpm --filter @objectstack/spec check:skill-refs` exit 0 — `✅ 9 generated files in sync with packages/spec` (`references/_index.md` unchanged, nothing to regenerate). - `pnpm --filter @objectstack/spec check:skill-examples` exit 0 after `pnpm --filter @objectstack/spec build` (lock `VERDICT command-exit 0`, 88 s) and `pnpm --filter "@objectstack/client-react..." build` (`VERDICT command-exit 0`, 304 s) — `✅ 264 prose examples type-check across 3 surface(s)`; the two earlier runs on an unbuilt tree exited 3 (`PREREQUISITE NOT MET`), not findings. - `pnpm check:skill-identifier-liveness` exit 0 — `Leg 1: 491 citation(s) over 69 published file(s) … Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s)`. - Not run locally (CI-owned, not derived): `pnpm lint`, `test:repo`, the full battery. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (no schema change) - [x] 2. Every name I added uses the casing for its kind. (no new names; keys quoted as the spec spells them) - [x] 3. No formatter ran; `pnpm lint` is CI's run (repo-level scan, not owed locally per `.claude/agents/os-dev.md`). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes from a released package (`skills/**` carries no `package.json`); `skip-changeset`, and both declaration lines are above. - [x] 6. `packages/spec` untouched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block and the quoted ruling are the repo's own exceptions). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbXsLdroTYaf1DRqVUG)_ Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 56b0a58 commit 96f1258

4 files changed

Lines changed: 72 additions & 13 deletions

File tree

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,17 @@ export const CEILINGS = new Map([
381381
// row this PR itself lowered by 7,006, and the entry stays 448 lines).
382382
// 5785 -> 5780: A2′ — the release-stamp line left the count (see
383383
// STAMP_LINE_REPIN); the file's count fell by the same 5, headroom unchanged.
384-
['skills/objectstack-automation/SKILL.md', 5780],
384+
// 5780 -> 6004 (card #26298, audit #26184 rows A1/A3/A4): three teachings an
385+
// author had no row for — the fourth `notify` recipient form `{ emailField }`
386+
// (an envelope over an address no user holds now fails the node), flow-level
387+
// `requiredPermissions` (who may START a flow), and the screen field typed
388+
// `file` (the run resumes with the `sys_file` id). Paid down first: the CRM
389+
// Blueprint's approval row restated the approvals reference's config table
390+
// and branching section and was cut to a pointer (~17 tokens). Landed 5771 ->
391+
// 6004: +233 net, 9 absorbed by headroom, ceiling +224. Maintainer ruling
392+
// 2026-10-27 on the skills-lane decision card 「skills 修复卡的教学和 eval 都撞
393+
// token 天花板,怎么付?」, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
394+
['skills/objectstack-automation/SKILL.md', 6004],
385395
// 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above).
386396
// The invariant-vs-transition-gate choice, placed in the field-conditional-rule
387397
// bullet list where the tool is actually picked. Both wrong picks are silent:
@@ -701,7 +711,13 @@ export const CEILINGS = new Map([
701711
// Independently reproducible without running this script:
702712
// ceil($(wc -c < skills/objectstack-api/evals/endpoints-auth-routes.json) / 4) == ceil(4936 / 4)
703713
['skills/objectstack-api/evals/endpoints-auth-routes.json', 1234],
704-
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 1255],
714+
// 1255 -> 1823 (card #26298): three eval prompts, one per new teaching — the
715+
// applicant email over `{ emailField }`, `requiredPermissions` + `excludeSubmitter`
716+
// on one approval, the screen `file` field written by a later `create_record`.
717+
// +568 tokens, nothing in the file to delete, zero headroom, ceiling +568.
718+
// Same maintainer ruling as the SKILL.md row above (2026-10-27, verbatim and
719+
// untranslated): 「A:先削重复,余额按实测提高」.
720+
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 1823],
705721
// 894 -> 1095 (#26179): one regression case for the field permission `when`
706722
// (the contract prompt: lock the legal terms for finance from approval on,
707723
// legal keeps the edit) — expected: `when` on the finance set's entries with
@@ -745,7 +761,14 @@ export const CEILINGS = new Map([
745761
// because a split that arrives with budget is a raise wearing a new path.
746762
// The package deltas are the pointer lines and the four new file titles only.
747763
['skills/objectstack-automation/references/examples-flows.md', 1436],
748-
['skills/objectstack-automation/references/state-machines-and-approvals.md', 5569],
764+
// 5569 -> 5587 (card #26298, audit #26184 row A2): the approval config table
765+
// gains `excludeSubmitter` (refused beside `onEmptyApprovers: 'auto_approve'`),
766+
// +105 tokens. Paid down first: Best Practices 1 and 4 restated the Branching
767+
// section's "Conditional step" and "Roll back on reject" / "Hard reject"
768+
// bullets and were deleted (~73 tokens). Landed 5555 -> 5587: +32 net, 14
769+
// absorbed by headroom, ceiling +18. Same maintainer ruling as the SKILL.md
770+
// row above (2026-10-27, verbatim and untranslated): 「A:先削重复,余额按实测提高」.
771+
['skills/objectstack-automation/references/state-machines-and-approvals.md', 5587],
749772
['skills/objectstack-data/references/examples-objects.md', 1779],
750773
['skills/objectstack-data/references/lint-rules.md', 970],
751774
['skills/objectstack-data/references/seeds.md', 1299],

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Flows are built from **20 built-in node types** (plugins register more via
8888
| `notify` | Send a notification through the messaging service (inbox channel by default) |
8989
| `connector_action` | Invoke a pre-built integration connector |
9090
| `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) |
91-
| `screen` | Display a UI form to the user (screen flows only) |
91+
| `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen |
9292

9393
#### Human Decision
9494

@@ -104,7 +104,9 @@ undeclared key is a named parse error. **RAW** keys are read as written: a
104104

105105
```ts
106106
{ id: 'tell_owner', type: 'notify', label: 'Notify Owner', config: {
107-
recipients: { dialect: 'cel', source: 'record.assignee' }, // REQUIRED — id, list, or envelope; a CSV is ONE id
107+
recipients: { dialect: 'cel', source: 'record.assignee' }, // REQUIRED — id, list, or envelope; a CSV is ONE id.
108+
// 4th form `{ emailField: 'applicant_email' }`: an `email` field of the trigger record, for a non-user —
109+
// then `channels: 'email'` alone. An envelope must yield USER ids: an address no user holds fails the node
108110
title: 'Done: {{ record.title }}', // inline; XOR `template` (RAW, localizable)
109111
message: 'Closed {{ record.closed_at | date:long }}', // body; only with inline `title`
110112
topic: 'task', // RAW; default 'notify'
@@ -182,6 +184,13 @@ variables: [
182184
> organization predicate reads and writes across every tenant. The tenant column
183185
> is platform-injected — filter on it, never re-declare it per object.
184186
187+
> **Who may START a flow: `requiredPermissions: ['…']` on the flow** (ADR-0066
188+
> D4) — every door that starts it by name (trigger route, action of type `flow`,
189+
> declared endpoint) refuses a non-system caller missing any named capability
190+
> with `403 PERMISSION_DENIED`; an action starting it inherits the list, a
191+
> `subflow` node is not a door. Gate an elevated `screen` / `api` flow here, never
192+
> on `current_user.positions` inside it.
193+
185194
> **A hook elevates itself with `runAs`, never with `sudo`.** An object hook
186195
> (objectstack-data) declares its own `runAs: 'system' | 'user' | 'inherit'` —
187196
> default `'inherit'`, the context of the write that fired it — scoping that
@@ -399,7 +408,7 @@ in this CRM-style structure:
399408
| Automation Type | Typical Location | Pattern |
400409
|:--|:--|:--|
401410
| Screen flow | `src/flows/*.flow.ts` | Use explicit `variables`, node graph (`nodes` + `edges`), and decision branches |
402-
| Approval flow | `src/flows/*.flow.ts` | A flow with `approval` node(s); set `approvers` / `behavior` / `lockRecord` / `approvalStatusField` in node `config`, branch on `approve` / `reject` edges |
411+
| Approval flow | `src/flows/*.flow.ts` | A flow with `approval` node(s) — config and branching in the approvals reference |
403412
| Flow registry | `src/flows/index.ts` | Export `allFlows: Flow[]` and register centrally in `defineStack({ flows })` |
404413

405414
---

‎skills/objectstack-automation/evals/flows-triggers-approvals.json‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,36 @@
5050
"must_contain": ["function:", "outputVariable", "type: 'fault'", "timeoutMs", "update_record", "dialect: 'cel'"],
5151
"must_not_contain": ["label: 'error'", "query_record", "{recalc"]
5252
}
53+
},
54+
{
55+
"id": 6,
56+
"prompt": "When a `job_application` is approved, email the applicant. Applicants are not users; the address is in the record's `applicant_email` field.",
57+
"expected_output": "`notify` with `recipients: { emailField: 'applicant_email' }` (the fourth recipient form: an `email`-typed field of the trigger record) and `channels: 'email'` alone — never a CEL envelope over `record.applicant_email`, which must yield user ids and fails the node for an address no user holds.",
58+
"files": [],
59+
"assertions": {
60+
"must_contain": ["notify", "emailField", "applicant_email", "'email'"],
61+
"must_not_contain": ["source: 'record.applicant_email'", "channels: ['inbox']"]
62+
}
63+
},
64+
{
65+
"id": 7,
66+
"prompt": "An expense approval only holders of the `expense.approve_request` capability may start; route to the `finance_manager` position, and the submitter must never approve their own expense.",
67+
"expected_output": "Flow-level `requiredPermissions: ['expense.approve_request']` (checked at every door that starts the flow, not a `current_user.positions` check inside it); the `approval` node sets `excludeSubmitter: true`, and its `onEmptyApprovers` is anything but `auto_approve`, which is refused beside it.",
68+
"files": [],
69+
"assertions": {
70+
"must_contain": ["requiredPermissions", "excludeSubmitter: true", "type: 'position'"],
71+
"must_not_contain": ["onEmptyApprovers: 'auto_approve'", "current_user.positions"]
72+
}
73+
},
74+
{
75+
"id": 8,
76+
"prompt": "A screen flow: the user uploads the signed contract PDF on the last step, then the flow creates the `contract` record with the file in its `signed_copy` field.",
77+
"expected_output": "A `screen` field `{ name: 'signed_copy', type: 'file', required: true }`; the screen resumes with the uploaded `sys_file` id bound to `signed_copy`, and a `create_record` after the last screen writes `fields: { signed_copy: { dialect: 'cel', source: 'signed_copy' } }`, which claims the file for the record.",
78+
"files": [],
79+
"assertions": {
80+
"must_contain": ["type: 'screen'", "type: 'file'", "create_record", "signed_copy"],
81+
"must_not_contain": ["type: 'upload'", "type: 'attachment'"]
82+
}
5383
}
5484
]
5585
}

‎skills/objectstack-automation/references/state-machines-and-approvals.md‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data**
347347
| `lockRecord` | Lock the triggering record from edits while pending. Default `true` |
348348
| `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) |
349349
| `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) |
350+
| `excludeSubmitter` | `true` drops the run's submitter (`submitter_id`) from the resolved slate, whatever approver type produced them — segregation of duties; a slate it empties takes the `onEmptyApprovers` route, never a silent pass. `onEmptyApprovers: 'auto_approve'` is **refused** beside it (it would approve exactly the request this exists to stop): use `admin_rescue`, `fail` or `fallback`. Default `false` |
350351
| `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 |
351352
| `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) |
352353
| `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 |
@@ -369,15 +370,11 @@ These are wired on the **graph**, not in node config:
369370

370371
### Approval Best Practices
371372

372-
1. **Gate entry on the edge** (`condition` into the Approval node) so the flow
373-
only pauses for records that actually need sign-off.
374-
2. **Set `approvalStatusField`** to mirror status onto the row — views and
373+
1. **Set `approvalStatusField`** to mirror status onto the row — views and
375374
formulas can then filter on it without joining `sys_approval_request`.
376-
3. **Keep `lockRecord: true`** unless you have a strong reason to allow
375+
2. **Keep `lockRecord: true`** unless you have a strong reason to allow
377376
edits while pending — otherwise approvers chase a moving target.
378-
4. **Model rejection as a visible branch** — a back-edge to revise, or an `end`
379-
node to terminate. The path is on the diagram, not hidden in config.
380-
5. **Notify the submitter from downstream nodes** wired to the `approve` /
377+
3. **Notify the submitter from downstream nodes** wired to the `approve` /
381378
`reject` edges. Opening already tells each resolved approver
382379
(`approval.requested`); ⛔ no `notify` node for the opening: it tells
383380
them twice.

0 commit comments

Comments
 (0)