Skip to content

Commit c893b3d

Browse files
docs(spec): state the notify click-through default in the sourceObject / sourceId / actionUrl describes (#30742)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuKB8Sg6c6sVpL96ruVvdxaf)_ Fixes #30689 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`notify` 节点 `sourceObject` / `sourceId` / `actionUrl` 三个字段的 spec 说明文字(及其注释、生成的参考文档页),补上"三者都不写时,通知自动链接到触发本次运行的记录"这条默认行为及其例外。 - 为什么改:运行时早已默认链接到触发记录,但 spec 文档只说"成对生效、缺一半会被丢弃",元数据作者和 AI 读到的说明与实际行为不一致。 - 风险与回滚:只改说明文字,不改任何键、结构、默认值或校验;回滚即 revert 本 PR,无数据或行为影响。 - 先看:`packages/spec/src/automation/io-node-config.zod.ts` 的三个 `.describe()` 与 `NotifyConfigSchema` 文档块;其次 `content/docs/references/automation/io-node-config.mdx` 的生成差异。 - 两分钟验证:`pnpm --filter @objectstack/spec check:generated`,预期输出 "All 14 generated artifacts are up to date"。 - 需要你做的:无,席位落地。 ## What changes - `packages/spec/src/automation/io-node-config.zod.ts` - `sourceObject` and `sourceId` `.describe()`: keep the pair rule (a half-written pair is dropped, now "not defaulted"), and add the default: when `sourceObject`, `sourceId` and `actionUrl` are all left out, a run started by a record (a record change other than a delete, a time-relative sweep, an action on a record) links to that record, a run with no trigger record links nowhere, and an explicit pair or an `actionUrl` is kept as written. - `actionUrl` `.describe()`: adds "written alone keeps the notification from linking to the record that started the run". This is the runtime's own sentence from `notify-node.ts`. - The JSDoc of the three keys, and a new "Click-through default" bullet in the `NotifyConfigSchema` docblock that names every exception the executor applies: delete events, runs with no trigger record, an explicit pair or `actionUrl`, a half pair. - `content/docs/references/automation/io-node-config.mdx`: regenerated by `gen:docs`, not edited by hand. - `.changeset/30689-notify-click-through-default-describe.md`: `@objectstack/spec` patch. No key, shape, default, refinement or export changes. Nothing outside `packages/spec/**` changes, apart from the generated page and the changeset. **The claims, checked against the runtime on `origin/main` `ab44ec94b`** (`packages/services/service-automation/src/builtin/notify-node.ts`, read only): - `triggerRecordOf` returns a trigger record only when the run context has both `object` and `record.id`. - `defaultClickThroughOf` returns nothing when any of the three keys is written, and nothing for `beforeDelete` / `afterDelete`. - The executor uses the explicit pair only when both halves resolve, and otherwise falls back to `defaultClickThroughOf`. So a half pair is dropped and is not defaulted. - Which runs carry `object` + `record`: - record change: yes, through `trigger-record-change`; - time-relative sweep: yes (`time-relative-trigger.ts`, `event: 'time_relative'`); - action on a record: yes (`action-execution.ts`, which passes `record` and `object`). - Which runs do not: - inbound API post: `trigger-api` passes `record: payload` and no `object`; - manual REST start: `buildAutomationContext` sets `object` and no `record`; - schedule tick: neither. - The six tests in `notify-node.test.ts` under "notify click-through defaults to the trigger record" pin each case. ## Tests All readings below are on head `5c4599568` (`git rev-parse --short HEAD`), worktree off base `ab44ec94b`. Exit codes were captured before any pipe. - `pnpm --filter @objectstack/spec build` under the verify lock: `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec check:generated`: - before regenerating: exit 1, `content/docs/references/** stale`; - after `gen:docs`: exit 0, `All 14 generated artifacts are up to date`. - The nine `@objectstack/spec` test files that import or cover `io-node-config`, under the lock: `Test Files 9 passed (9)`, `Tests 628 passed (628)`. They include `io-node-config.test.ts`, whose `/dropped at execute time/i` pin still holds. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts typecheck, test typecheck): exit 0. - `npx eslint --no-inline-config packages/spec/src/automation/io-node-config.zod.ts`: exit 0. - `node scripts/check-changeset-no-major.mjs --base ab44ec9 --event` with an offline PR event that carries this body's two declaration lines: exit 0. Its verdict: "LEVEL AXIS: this PR declares clause-② `no` and metadata-protocol `none` … a non-protocol change at `patch`". - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 96 commands, and every one ran. - Five first exited 3 (PREREQUISITE NOT MET: unbuilt `lint`, `formula`, `client-react` and `objectql` dists). A sixth also exited 3 because it needs the full workspace build. After `turbo run build` of those closures, and then the full build, all six re-ran at exit 0. - `--ran` reconciliation: `96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN`. - Local-only gates: `pnpm check:dev-prereqs` exit 0, `pnpm check:skill-frame-freshness` exit 0, `node scripts/check-nul-bytes.mjs` exit 0. - No ablation: this diff changes no tested behaviour, only describe text and docs. ## Acceptance notes - **Pair note in `NOTIFY_KEY_GUIDANCE.source` (≈:137) is left as it is, on purpose.** It says "the pair only takes effect together: a half-specified target is dropped". That is still true under the default. It speaks to an author who is moving a nested `source: { object, id }` onto the flat pair, so it is about a pair that is written, and the default never applies to such a pair. Nothing in it became false or incomplete for that reader. - **Skill wording:** `skills/objectstack-automation/SKILL.md:124` comments the example as `click-through: a PAIR, else dropped`. That is still true, and it is not the old "only takes effect together" sentence. It does not mention the trigger-record default. Not edited here, because `skills/**` is outside this card's surface. - **Hand-written docs:** `content/docs/automation/flows.mdx:352` says "a half-specified click-through pair is dropped, as before". That is still true, and the page does not mention the default either. Not edited here: it is outside this card's surface. - **Not merged with `origin/main`.** The branch is 6 commits behind `447226b22`. Those commits touch `packages/spec/src/system/i18n-resolver*` and `platform-locales*` plus five unrelated changesets, and none of them touches this diff's files. The dispatch-gates derivation says none of them touched what its answer derives from. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3214615 commit c893b3d

3 files changed

Lines changed: 39 additions & 11 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `notify` node's `sourceObject`, `sourceId` and `actionUrl` descriptions now state the click-through default the runtime already applies
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A `notify` node that writes none of `sourceObject`, `sourceId` and `actionUrl` links to the run's trigger record when the run has one (a record change other than a delete, a time-relative sweep's row, an action started on a record). The `NotifyConfigSchema` descriptions, and the reference page generated from them, said only that the pair takes effect together. They now also say that an explicit pair or an `actionUrl` is kept as written, that a half-written pair is still dropped rather than defaulted, and that a run with no trigger record links nowhere. No key, shape, default or refinement changes.

‎content/docs/references/automation/io-node-config.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ const result = HttpConfigSchema.parse(data);
103103
| **channels** | `string \| string[]` | optional | Channels to fan out to (default: inbox) |
104104
| **topic** | `string` | optional | Event topic (default: "notify") |
105105
| **severity** | `Enum<'info' \| 'warning' \| 'critical'>` | optional | Severity forwarded to the messaging service |
106-
| **sourceObject** | `string` | optional | Object name of the record the notification links to (writes sys_notification.source_object) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
107-
| **sourceId** | `string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }` | optional | Record id the notification links to (writes sys_notification.source_id) — a string, or a CEL value envelope `{ dialect: 'cel', source }` evaluated per run to one (`record.id`); a `{…}` template token is refused. Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
106+
| **sourceObject** | `string` | optional | Object name of the record the notification links to (writes sys_notification.source_object) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, not defaulted, so the inbox never renders a dead link. When sourceObject, sourceId and actionUrl are all left out, a run started by a record (a record change other than a delete, a time-relative sweep, an action on a record) links to that record, and a run with no trigger record links nowhere; an explicit pair or an actionUrl is kept as written. |
107+
| **sourceId** | `string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }` | optional | Record id the notification links to (writes sys_notification.source_id) — a string, or a CEL value envelope `{ dialect: 'cel', source }` evaluated per run to one (`record.id`); a `{…}` template token is refused. Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, not defaulted, so the inbox never renders a dead link. When sourceObject, sourceId and actionUrl are all left out, a run started by a record (a record change other than a delete, a time-relative sweep, an action on a record) links to that record, and a run with no trigger record links nowhere; an explicit pair or an actionUrl is kept as written. |
108108
| **actorId** | `string` | optional | User id that caused the event (writes sys_notification.actor_id) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value |
109-
| **actionUrl** | `string` | optional | Explicit click-through URL, rendered per run with `{{ }}` holes like `title` (`/crm_case/{{ record.id }}`); a single-brace `{token}` is refused. Overrides the link synthesized from sourceObject/sourceId. |
109+
| **actionUrl** | `string` | optional | Explicit click-through URL, rendered per run with `{{ }}` holes like `title` (`/crm_case/{{ record.id }}`); a single-brace `{token}` is refused. Overrides the link synthesized from sourceObject/sourceId, and written alone keeps the notification from linking to the record that started the run. |
110110
| **payload** | `Record<string, any>` | optional | Extra values merged into the notification payload — literals, read as written: a single-brace `{token}` in any string of one is not resolved here and is refused, as is a CEL value envelope written as a value; write the value |
111111

112112
### Nested Shape: `NotifyConfig.recipients[option 4]`

‎packages/spec/src/automation/io-node-config.zod.ts‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,16 @@ function notifyLiteralRefusals(cfg: {
643643
* click-through target is dropped so the inbox never renders a dead link.
644644
* The schema keeps both optional rather than refining, because the executor
645645
* tolerates (drops) the half-specified shape rather than rejecting it.
646+
* - Click-through default: a node that writes none of `sourceObject`,
647+
* `sourceId` and `actionUrl` links to the run's trigger record, when the
648+
* run has one — a record change other than a delete, a time-relative
649+
* sweep's row, an action started on a record. A schedule tick, an inbound
650+
* API post and a manual start with no record have none and link nowhere;
651+
* a delete's record is gone once the write lands, so it is not linked. An
652+
* explicit pair or an explicit `actionUrl` is the author's word and wins,
653+
* and a half-written pair is still dropped, never defaulted. The executor
654+
* owns the default (`defaultClickThroughOf` in service-automation's
655+
* `notify-node.ts`); the schema shape is unchanged by it.
646656
* - The historical aliases (`to`/`subject`/`body`/`url`, nested
647657
* `source: { object, id }`) are NOT part of this contract: the ADR-0087 D2
648658
* conversion `flow-node-notify-config-aliases` rewrote them at load for
@@ -744,26 +754,34 @@ export const NotifyConfigSchema = lazySchema(() => strictObject({
744754
.describe('Severity forwarded to the messaging service'),
745755
/**
746756
* Click-through target object — only effective together with `sourceId`
747-
* (#2675). A literal, read as written (#19939, "notify literals" above).
757+
* (#2675); a half-written pair is dropped, never defaulted. A literal, read
758+
* as written (#19939, "notify literals" above). Left out together with
759+
* `sourceId` and `actionUrl`, the link defaults to the run's trigger record
760+
* (see "click-through default" above).
748761
*/
749762
sourceObject: notifyLiteralStringSchema('sourceObject').optional()
750-
.describe('Object name of the record the notification links to (writes sys_notification.source_object) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link.'),
763+
.describe('Object name of the record the notification links to (writes sys_notification.source_object) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, not defaulted, so the inbox never renders a dead link. When sourceObject, sourceId and actionUrl are all left out, a run started by a record (a record change other than a delete, a time-relative sweep, an action on a record) links to that record, and a run with no trigger record links nowhere; an explicit pair or an actionUrl is kept as written.'),
751764
/**
752765
* Click-through target record id — only effective together with
753-
* `sourceObject`. A `value` slot (#19939, {@link NotifySourceIdSlotSchema}).
766+
* `sourceObject`; a half-written pair is dropped, never defaulted. A `value`
767+
* slot (#19939, {@link NotifySourceIdSlotSchema}). Left out together with
768+
* `sourceObject` and `actionUrl`, the link defaults to the run's trigger
769+
* record (see "click-through default" above).
754770
*/
755771
sourceId: NotifySourceIdSlotSchema.optional()
756-
.describe('Record id the notification links to (writes sys_notification.source_id) — a string, or a CEL value envelope `{ dialect: \'cel\', source }` evaluated per run to one (`record.id`); a `{…}` template token is refused. Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link.'),
772+
.describe('Record id the notification links to (writes sys_notification.source_id) — a string, or a CEL value envelope `{ dialect: \'cel\', source }` evaluated per run to one (`record.id`); a `{…}` template token is refused. Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, not defaulted, so the inbox never renders a dead link. When sourceObject, sourceId and actionUrl are all left out, a run started by a record (a record change other than a delete, a time-relative sweep, an action on a record) links to that record, and a run with no trigger record links nowhere; an explicit pair or an actionUrl is kept as written.'),
757773
/** User id that caused the event. A literal, read as written (#19939, "notify literals" above). */
758774
actorId: notifyLiteralStringSchema('actorId').optional()
759775
.describe('User id that caused the event (writes sys_notification.actor_id) — a literal, read as written: a single-brace `{token}` is not resolved here and is refused, as is a CEL value envelope; write the value'),
760776
/**
761-
* Explicit click-through URL; overrides the sourceObject/sourceId link. A
762-
* TEXT slot (#19939): rendered per run with `{{ }}` holes, like `title`; a
763-
* single-brace `{token}` is refused by the `superRefine` below.
777+
* Explicit click-through URL; overrides the sourceObject/sourceId link, and
778+
* written alone keeps the trigger-record default from applying (see
779+
* "click-through default" above). A TEXT slot (#19939): rendered per run with
780+
* `{{ }}` holes, like `title`; a single-brace `{token}` is refused by the
781+
* `superRefine` below.
764782
*/
765783
actionUrl: z.string().optional()
766-
.describe('Explicit click-through URL, rendered per run with `{{ }}` holes like `title` (`/crm_case/{{ record.id }}`); a single-brace `{token}` is refused. Overrides the link synthesized from sourceObject/sourceId.'),
784+
.describe('Explicit click-through URL, rendered per run with `{{ }}` holes like `title` (`/crm_case/{{ record.id }}`); a single-brace `{token}` is refused. Overrides the link synthesized from sourceObject/sourceId, and written alone keeps the notification from linking to the record that started the run.'),
767785
/**
768786
* Extra values merged into the notification payload — literals, read as
769787
* written (#19939, "notify literals" above).

0 commit comments

Comments
 (0)