Skip to content

feat(service-automation): a record-triggered flow's notification links to the record that started the run when it names no link - #30252

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-0eqtwo
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-0eqtwo

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

维护者速读

  • 改了什么: 由记录触发的流程里,通知节点没写 sourceObject/sourceId/actionUrl 时,通知默认链到触发这次运行的那条记录;点开通知直接进记录页。
  • 为什么改: 技术员收到「工单分配给你」的通知,点开却落在通知列表;作者照最自然的写法(收件人、标题、正文)写,os validate 也通过,链接却是空的。卡 Flow notify: a record-triggered flow's notification links nowhere unless the author adds the source pair #30121「第一组:会卡住人或误导人」。
  • 风险与回滚: 只在三项都没写、且运行确有触发记录时生效;显式的一对或 actionUrl 照旧优先,半对照旧丢弃,定时/API/无记录的手动运行不变,删除事件不链(记录已不在)。回滚即撤本 PR。
  • 先看: packages/services/service-automation/src/builtin/notify-node.ts 的 defaultClickThroughOf 与调用处,再看 notify-node.test.ts 末尾新的 describe。
  • 两分钟验证: pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/notify-node.test.ts,预期 23 条全绿。
  • 需要你做的: 无,席位落地。

Fixes #30121

Clause-②: no — no accepted key, value or exported type changes; sourceObject / sourceId / actionUrl keep their meaning when written, and no option or type member was added to reach the trigger record (the node already reads it through triggerRecordOf).
Metadata-protocol: semantic
Skills-docs: follow-up owed — skills/objectstack-automation/SKILL.md (the notify example's click-through: a PAIR, else dropped line), content/docs/automation/flows.mdx (the notify sourceId paragraph), packages/spec/src/automation/io-node-config.zod.ts (the sourceObject / sourceId describes, which feed content/docs/references/automation/io-node-config.mdx). The skill line is a governed (Tier H) edit and the spec describe is out of this order's scope, so neither rides here; the seat files the card.

What changes

@objectstack/service-automation — the notify node (src/builtin/notify-node.ts):

  • New defaultClickThroughOf(cfg, context, trigger). When the node writes none of sourceObject, sourceId and actionUrl, the click-through record is the run's trigger record, read through the existing triggerRecordOf(context): the same reading recipients: { emailField } and the organization fallback already use. The messaging service then stores it as sys_notification.source_object / source_id and synthesizes /OBJECT/ID, exactly as for an authored pair.
  • What counts as a trigger record, measured on origin/main at the run-starting doors:
    • record change (trigger-record-change): object + the row, record.id present. Defaults.
    • time-relative sweep (trigger-schedule time-relative-trigger.ts): object: desc.object, record: row. That object's row is the record the run is about, so it defaults.
    • action on a record (runtime action-execution.ts): object + record with the requested recordId stamped. Defaults.
    • schedule tick, inbound API post (trigger-api: record: payload with no object), manual REST start (object but no record): no trigger record. Unchanged.
  • Delete events (beforeDelete, afterDelete) are not defaulted: the record is gone once the write lands, and this node already drops a half-written pair so the inbox never renders a dead link. An authored pair on a delete flow is still honoured.
  • A half-written pair keeps today's behaviour: it is dropped and nothing is defaulted in its place, because the author did write a link.
  • An actionUrl written alone keeps today's behaviour too: the URL is the link and no source is defaulted beside it, per the card's "an explicit actionUrl or pair still wins".
  • The organization fallback (Notifications emitted from a system-context run (schedule tick, approval node) are stored with organization_id NULL on 18.0.0-next.0: the platform's own WARN says reports and cleanup that filter by organization will not see them #23284) asked [source, trigger]; a defaulted source is the trigger record itself, so the second candidate is skipped when it is the same object (no second findOne). The organization it resolves is unchanged.
  • The Studio form help for sourceObject, sourceId and actionUrl (the node descriptor's configSchema) says the default.

Tests

src/builtin/notify-node.test.ts, one new top-level describe in the existing harness, six pins:

  1. record-triggered notify, no link written ⇒ source is { object: 'repair_ticket', id: 't_7' }, no url;
  2. explicit pair wins over the trigger record;
  3. explicit actionUrl alone: url as written, no source defaulted;
  4. half-written pair (sourceObject only): still dropped;
  5. run with no trigger record (object, no record): no source;
  6. afterDelete: no source.
Local runs (head after the final commit is in the report)
  • pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/notify-node.test.ts: Test Files 1 passed, Tests 23 passed (17 before + 6).
  • pnpm --filter @objectstack/service-automation test (under os-verify-lock.sh): Test Files 263 passed (263), Tests 3407 passed (3407), lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/service-automation typecheck: exit 0 (test layer included, check:test-typecheck OK, 0 debt).
Ablation (one, on the behaviour the pins hold)

node scripts/ablation-replace.mjs on notify-node.ts, anchor return trigger; (the default's answer) replaced by return trigger && undefined; // ABLATED_30121: anchor x1 to x0, replacement x0 to x1, blob 94746f6b12e1 to 381704ef410f. Run: Tests 1 failed, 22 passed; the one red is pin 1, "links a record-triggered notify that names no source to the trigger record". Restored: blob equals HEAD (94746f6b12e1), git diff HEAD empty. The test imports the executor from src, so no build sat between the mutation and the run.

A first attempt used a bare identifier as the marker, which threw a ReferenceError and turned eight unrelated tests red; that run was discarded as a malformed mutation, not counted.

Acceptance notes

  • The spec's own describes for sourceObject / sourceId (packages/spec/src/automation/io-node-config.zod.ts) still say "Only takes effect together with …", which stays true for an authored pair but does not mention the default. Out of this order's scope (packages/spec/src/**); listed under Skills-docs: above.
  • A flow started by an action whose caller could not read the row (recordLoadDenied: true) still carries the requested id, so its notification links to that id, as the authored sourceId: record.id would. The record page applies the recipient's own access when opened.

Verification

Gate list derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack; each command's exit code was recorded before any pipe and reconciled with --ran. Readings are in the os-dev-report comment on #30121. origin/main was merged once before opening (no incoming commit touched service-automation or packages/spec).


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 8 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/knowledge-rag.mdx (via sourceId (literal, a string literal in defaultClickThroughOf))
  • content/docs/api/data-api.mdx (via sourceId (literal, a string literal in defaultClickThroughOf))
  • content/docs/api/data-flow.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS), beforeDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/automation/flows.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf), sourceId (literal, a string literal in defaultClickThroughOf), sourceObject (literal, a string literal in defaultClickThroughOf))
  • content/docs/automation/hooks.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/data-modeling/formulas.mdx (via beforeDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/data-modeling/validation.mdx (via beforeDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/deployment/production-readiness.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/deployment/validating-metadata.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf))
  • content/docs/kernel/events.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS), beforeDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/permissions/system-context.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/protocol/kernel/i18n-standard.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf))
  • content/docs/protocol/knowledge.mdx (via sourceId (literal, a string literal in defaultClickThroughOf))
  • content/docs/protocol/objectql/schema.mdx (via afterDelete (literal, a string literal in DELETE_EVENTS), beforeDelete (literal, a string literal in DELETE_EVENTS))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf), beforeDelete (literal, a string literal in DELETE_EVENTS))
  • content/docs/releases/v17/17-0.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf))
  • content/docs/releases/v18/18-0.mdx (via actionUrl (literal, a string literal in defaultClickThroughOf), sourceId (literal, a string literal in defaultClickThroughOf), sourceObject (literal, a string literal in defaultClickThroughOf))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bb86ff96cc6e3854d00c39488a0a8985fb1312d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 37c449daf493163c4437c8ac55b459d3f864e12d — the merge of head 6de9d516466427f468fd35178380b7c1acac90eb into base bb86ff96cc6e3854d00c39488a0a8985fb1312d1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 37c449daf493163c4437c8ac55b459d3f864e12d && git checkout 37c449daf493163c4437c8ac55b459d3f864e12d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb86ff96cc6e3854d00c39488a0a8985fb1312d1 6de9d516466427f468fd35178380b7c1acac90eb && git checkout -B drift-repro bb86ff96cc6e3854d00c39488a0a8985fb1312d1 && git merge --no-ff 6de9d516466427f468fd35178380b7c1acac90eb

node scripts/docs-audit/affected-docs.mjs --json bb86ff96cc6e3854d00c39488a0a8985fb1312d1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bb86ff96cc6e3854d00c39488a0a8985fb1312d1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

席位意见:ACCEPT。默认只在作者一个链接都没写、且本次运行有触发记录时生效;显式写法照旧优先,删除事件不链接,组织解析不重复读。

席位核对

Head-sha: 6de9d516466427f468fd35178380b7c1acac90eb · read 2026-11-05T04:10Z

Item Reading
Shape Draft → main, author objectstack-fleet[bot] (relay pr_create); line 1 attribution, Fixes #30121, Clause-② / Metadata-protocol / Skills-docs at line start. 3 files, +139/−7.
Premise measured On origin/main, notify-node.ts built the click-through only from an explicit sourceObject + sourceId pair or actionUrl; the trigger record was already reachable through triggerRecordOf. The dev's report 6649095495 and the diff agree.
Gates (check-runs on this head) 35 success, 13 skipped, 0 failure, 0 pending. Dev's local --ran: 59 families, 58 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, needs a full build; CI carries it); pnpm lint exit 0. 6 new pins; ablation of the default's return turns its pin red.
Routes the dev chose (accepted) A time-relative sweep's row counts as the trigger record (it is that object's row). A delete event is not linked (the record is gone once the write lands). A half-written pair is still dropped, not defaulted. Each one is pinned and stated in the changeset.
Clause-② no: no accepted key, value or exported type changes; the three keys keep their meaning when written. Agrees with claim 6648379861.
Metadata-protocol / Skills-docs semantic (an omitted link now means "the trigger record"), changeset minor. Skills-docs: follow-up owed: filed as #30254 (skill example line, flows.mdx, the spec describes, an eval), blocked by this merge.
Landing Not governed (check-governed-merges --pr 30252: 0 of 3 paths), 146 lines ≤ 3000. Seat lands: pr_ready + automerge_enable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow notify: a record-triggered flow's notification links nowhere unless the author adds the source pair

1 participant