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

Queued
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/project-thread-0eqtwo
Queued

objectstack-fleet[bot] wants to merge 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 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.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 04:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 04:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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

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