task_reminder and overdue_escalation stored objectName on a schedule
start node. The schedule binding carries no object: deriveTriggerBinding
lifts objectName into the record-change binding only, and ScheduleTrigger
never reads it, so the key was stored and never acted on (the flow
designer flags it "kept, not in effect"). Both sweeps already name
todo_task on their get_record steps.
Adds a pin over the shipped stack: no flow whose resolved trigger kind is
schedule carries objectName on its start node.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
Fixes #24067
Clause-②: no
What
examples/app-todo's two daily sweeps,task_reminderandoverdue_escalation, storedobjectName: 'todo_task'on atype: 'schedule'start node, a key the schedule binding never reads. This PR drops the key from both start nodes and adds a pin over the example's shipped stack. Clause-② isno: an example app's metadata loses a key nothing reads, and no export, type or accept set moves.Premise verified first (on
origin/main9ffe8af094)AutomationEngine.deriveTriggerBinding(packages/services/service-automation/src/engine.ts) putsconfig.objectNameinto the record-change binding (:3799,:3816) and, as a fallback behindtimeRelative.object, into the time-relative binding (:3842). Itsschedulebranch (:3859to:3872) buildsflowName,schedule,organization,conditionandconfig, and no object.ScheduleTrigger(packages/triggers/trigger-schedule/src/schedule-trigger.ts) readsbinding.scheduleandbinding.organization, and frombinding.configonlyscheduleandorganization. It never readsobjectName, and the run context it builds carries no object.resolveFlowTriggerKindnever readsobjectName. Neither start node has arecord-*triggerType, so both flows resolve toschedule.todo_taskon their read steps:get_upcoming_tasks(:61) andget_overdue_tasks(:166). These areget_recordnodes withlimit: 200; the card calls themget_records, but they are the same nodes. They keep theirobjectName.validateFlowExpressionsschema hint (it logs and never throws) and@objectstack/lint'svalidate-expressionshint. Under "Evidence" below,objectstack validategives the same output before and after.Changes
examples/app-todo/src/flows/task.flow.ts: dropobjectNamefrom both schedule start nodes. A comment on the first one says why.examples/app-todo/test/schedule-start-no-object-name.test.ts(new): reads the flows from the example's defaultdefineStackexport, not from a hand list, and keeps those whoseresolveFlowTriggerKindanswer isschedule. It then asserts that none of them hasobjectNameon its start node'sconfig. Anti-vacuity: the selection must be non-empty, and each selected start node must carryschedule.Selection, as a deviation from the dispatch wording. The claim describes the pin's population as "
type: 'schedule'flows". The pin selects by the spec resolver'sscheduleanswer instead, because that is the predicate the engine binds by. Everytype: 'schedule'flow is included unless its start node routes it to another trigger. The case that matters is atimeRelativesweep: its binding does fall back toobjectName, so pinning the key away there would be wrong.Evidence
pnpm --filter @objectstack/example-todo exec vitest run --maxWorkers=2 test/schedule-start-no-object-name.test.tsgives 2 passed.scripts/ablation-replace.mjs(wrap mode, with a restore trap). Each leg puts today'sobjectName: 'todo_task'back into one start node:0 8 * * *: anchor count 1 to 0, blobbf97d6a9016dtoef4f5551ce62. Red, offendersflow 'task_reminder' · start node config.objectName. Restored: blob equals HEADbf97d6a9016d,git diff HEADempty.0 9 * * *: blobbf97d6a9016dtob6d8aab6cd15. Red, offendersflow 'overdue_escalation' · start node config.objectName. Restored: blob equals HEAD,git diff HEADempty.a60f8cde84:vitest rungives 8 files and 240 tests passed.typecheck(tsc --noEmit) exits 0, andtsc --listFilesincludes the new test.objectstack validateon the example passes both before and after, and its 7 warnings are byte-identical. For the "before" reading, both keys were put back with nestedablation-replace, then restored to HEAD.examples/,skills/,content/docs/andpackages/create-objectstack/found no other schedule start node withobjectName(control: the same pattern finds 2 in the base tree'stask.flow.ts).Gates (local, head
a60f8cde84)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 41 commands. All 41 were run, and every exit code was captured before any pipe: 41 exit 0.--ranreconciliation:41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN.73f5bf9968),check:dual-build-cjs-loadsandcheck:i18n-coveragerefused with PREREQUISITE NOT MET (exit 3, nothing measured). I built the packages they named, and both then measured exit 0. The whole union was then re-run on the merge head.Acceptance notes
@objectstack/example-todois"private": true, so this PR carries theskip-changesetlabel.objectNameon a schedule start node. That is the spec lane's call, and this PR does not touch it.origin/mainat265ef64ba7. Since then,mainhas advanced by one commit that touches onlyplugin-approvals, which this diff does not touch.Generated by Claude Code