fix(trigger-schedule): a scheduled run's platform seeds never answer a screen field (#19900) - #19982
Conversation
…its seeds never answer a screen The schedule trigger seeds params with jobId, flowName and schedule and has no caller. Without an explicit producer signal the screen node's headless verdict inferred those three seeds as caller answers and skipped a screen whose field shared a name. The trigger now states callerParamKeys: []. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
…Keys: [] The callerParamKeys TSDoc, the screen verdict's docblock, the flows guide and the pending caller-param-keys changeset each listed the schedule trigger among the producers that leave the key absent. It now states the empty list, so each statement is corrected. Adds this change's own changeset. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
… state the superseded sentence in this change's own note Correcting another change's pending release note is a release decision (check-empty-changeset refuses it and asks for confirmation). This change's own changeset states which sentence of that note it supersedes instead. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6c1dcb46b69eb3dfc3cc5140dccd71bdaafae3af && git checkout 6c1dcb46b69eb3dfc3cc5140dccd71bdaafae3af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 eeed06a5be7b5efca2b669d1e9114fed5f00c7b2 && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff eeed06a5be7b5efca2b669d1e9114fed5f00c7b2
node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375
|
Contract reviewServed-tier: Rendered by an isolated at-tier reviewer and adopted by the ① Derived judgments
② Semver levelCorrect and complete.
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… schedule trigger as leaving it absent (objectstack-ai#19900) (objectstack-ai#19991) Part of objectstack-ai#19900 Clause-②: no ## The pending release note this corrects This PR edits one sentence of `.changeset/19846-automation-caller-param-keys.md`, the pending note for `AutomationContext.callerParamKeys` (PR objectstack-ai#19899). The note's frontmatter covers `@objectstack/spec`, `@objectstack/runtime` and `@objectstack/service-automation`. Nothing else in the note changes, frontmatter included (1 line changed: +1 / −1). **Before:** > Record-change, schedule, time-relative and webhook triggers, and code calling `execute` directly, leave it absent. **After:** > Record-change, time-relative and webhook triggers, and code calling `execute` directly, leave it absent; the schedule trigger, whose run has no caller, states an empty list (objectstack-ai#19900). **What made it false:** PR objectstack-ai#19982, landed as `ae7a35a63b`. Since that commit, `ScheduleTrigger` sets `callerParamKeys: []` on every run context it builds, so the schedule trigger no longer leaves the key absent. PR objectstack-ai#19982's own changeset (`.changeset/19900-schedule-caller-param-keys.md`) reaches only the `@objectstack/trigger-schedule` and `@objectstack/spec` CHANGELOGs. The `@objectstack/runtime` and `@objectstack/service-automation` CHANGELOGs would publish this sentence unchanged. ## The gate is expected to stay red `node scripts/check-empty-changeset.mjs --base origin/main` exits 1 on this head. This is a DELIBERATE CORRECTION of a foreign pending note, and the gate stays red on it by design until a person confirms the rewritten sentence (the seat's ruled path: ruling D on objectstack-ai#17712, and the landing rule added in objectstack-ai#19970). Its output begins: ```text Diffing HEAD from ae7a35a (merge base with origin/main). ✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added). This PR changes a changeset it did not add: .changeset/19846-automation-caller-param-keys.md present on the merge base and CHANGED by this PR -- this is somebody else's release note ``` The DELIBERATE CORRECTION remedy it prints is: "do NOT restore it -- say so on the PR and get it confirmed". This section is that statement. ## `skip-changeset`: not applied Two readings, and they agree. (1) This diff changes release-note text that `changeset version` will compile into three published CHANGELOGs, so it is not a diff that publishes nothing. (2) The standing rule for a PR that edits an existing `.changeset/*.md` is never to apply `skip-changeset`: the red gate is correct, and the confirmation goes through the PR text. ## Gates (head `465a0f9342`) `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 19 commands from the change set (merge base `ae7a35a63`). All 19 ran with their exit codes recorded. 18 exited 0, and `check-empty-changeset.mjs --base origin/main` exited 1, as expected above. `pnpm check:changeset-gate-self-tests` exited 0. The `--ran` reconciliation reads: 19 derived, 19 run, 0 NOT-MEASURED. --- _Generated by [Claude Code](https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19900
Clause-②: no
What this changes
The schedule trigger now sets
callerParamKeys: []on every run context it builds. A tick has no caller, so none of itsparamsseeds (jobId,flowName,schedule) is a caller's answer. Thescreennode's headless verdict reads the list when it is present (PR #19899) and no longer infers. The card's property: "A scheduled run's platform seeds never answer a screen field". A screen in a scheduled flow now pauses, whatever its fields are named. The three seeds stay inparams, so flows that read them are unaffected.Premise, re-measured on
origin/main2c1011b01bpackages/triggers/trigger-schedule/src/schedule-trigger.ts:806-810buildsparams: { jobId, flowName: binding.flowName, schedule }and sets nocallerParamKeys. It is the only producer of anevent: 'schedule'context underpackages/(non-testgit grep).[]as "the caller supplied nothing", not as absent.callerSuppliedinscreen-input-contract.ts:357readsif (declared !== undefined) return Array.isArray(declared) && declared.includes(name);. The consumer side already has a pin for this:screen-headless-caller-signal.test.ts:160("an EMPTY list is an answer: it pauses a screen the inference would skip"). The reader is unchanged here.schedule, the run completed instead of pausing:success: true,output: { schedule: { type: 'cron', expression: '0 2 * * *' } }, and nodescreen_1had statussuccess.jobIdandflowNamefailed the same way.Files
packages/triggers/trigger-schedule/src/schedule-trigger.ts: addscallerParamKeys: [](the fix).packages/triggers/trigger-schedule/src/schedule-caller-param-keys-e2e.test.ts(new): the realScheduleTriggeris bound through the realAutomationEnginewith builtin nodes. Atype: 'schedule'flow runsstart, thenscreen_1, thenend. The job is fired by hand. The test reads the context the trigger passed toengine.executeand the result the engine returned to the trigger's callback. It pins:callerParamKeys: [], and the seeds are still inparams;schedule,jobIdorflowNamepauses atscreen_1(it.each), andlistSuspendedRuns()names the run;scheduleas a caller key continues. The screen is not paused just because the flow is a schedule flow.packages/spec/src/contracts/automation-service.ts, thecallerParamKeysTSDoc. It now names the schedule trigger as a producer that states[], and drops it from the "Absent means" list. TSDoc only; no type or key change.packages/spec/src/contracts/automation-context-caller-param-keys.pin.test.ts, a comment ("an absent key is what every producer other than the two doors sends").packages/services/service-automation/src/screen-input-contract.ts, the docblock's "Absent" paragraph. Comment only; the inference is unchanged.content/docs/automation/flows.mdx, the "A run started any other way" paragraph..changeset/19900-schedule-caller-param-keys.md:@objectstack/trigger-schedulepatch and@objectstack/specpatch. The spec TSDoc ships: after the build, the new sentence is inpackages/spec/dist/contracts/index.d.tsandindex.d.mts, one hit each. Theservice-automationdocblock does not ship:grep -rlover itsdist/finds 0 files, while the positive controljudgeHeadlessScreenis found inindex.jsandindex.cjs. Soservice-automationhas no entry.The pending
callerParamKeysrelease note (#19846) is left as it isThat note says "Record-change, schedule, time-relative and webhook triggers ... leave it absent". Once this lands, that is false for the schedule trigger. I edited it once, and
check-empty-changeset.mjs --base origin/mainrefused the edit with exit 1: "Correcting a pending release note is a decision about a release". I restored it byte-identical to the merge base (blobc5cbff2689, both sides). This PR's own changeset now states the superseded sentence instead. If that note should be corrected as well, a person has to confirm it; the gate stays red on that edit by design.Time-relative trigger (dispatch item 3): measured, no defect, not changed
This was a one-time probe, not committed. It fired the real
TimeRelativeTriggerand passed its callback to the real engine. The run context had keysrecord, object, event, params, withparams === record(the same object) and nocallerParamKeys. A screen with required fields named after swept columns (name,status) paused atscreen_1. An all-optional screen with the same names also paused after astructuredCloneround trip. The inference's record leg already treats every key as a seed, becauseparamsis the record. So there is no defect and no change.Other producers (dispatch item 4): no finding
The same probe ran two hand-built shapes on the real engine, each copied from its producer's code. The record-change shape (
paramsis the same object asrecord, fromrecord-change-trigger.ts:499) paused. The webhook shape (record: payload, params: { ...payload }, fromapi-trigger.ts:142-143) paused. Neither skips a screen on its own seeds, so I filed nothing.Tests (HEAD
eeed06a5b)pnpm --filter @objectstack/trigger-schedule test: 7 files, 150 tests passed.typecheck(tsc --noEmit) passed with exit 0, and--listFilesincludes the new test file (7 test files in the program).@objectstack/service-automation:vitest run, 144 files, 1725 tests passed.typecheckexit 0.@objectstack/spec:vitest run --project local, 530 files, 15617 passed and 1 todo.typecheckexit 0 (tsc --noEmit,check:scripts-typecheck,check:test-typecheck).Ablation of the pin
I committed the fix first, at
afde207. Thennode scripts/ablation-replace.mjs --file packages/triggers/trigger-schedule/src/schedule-trigger.ts --anchor 'callerParamKeys: [],' --delete -- (the test run):5d67f8394debto85d2e4c9054f.expected undefined to deeply equal [], and four timesexpected { success: true, …(4) } to match object { status: 'paused', …(1) }.5d67f8394deb, the same as at HEAD, andgit diff HEADis empty../schedule-trigger.js(source), not through a package export, so nodist/preflight applies.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 107 commands from the change set, with no paths passed. All 107 ran ateeed06a5b, each with its exit code recorded. The--ranreconciliation reads: 107 derived, 105 run, 2 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsandcheck:type-check-debt. Each exited 3 (PREREQUISITE NOT MET): both need every package built, and a turbo dry run shows 77 of 78 build tasks as cache misses. CI's lint job builds that closure before running them.check-plugin-teardown-shape --self-testfirst exited 3, because its pinned fixture commit was missing from the shallow clone. I fetched that one commit, and the self-test passed on re-run (48 cases).check:skill-examplesfirst exited 3 because client-react was not built. I built@objectstack/clientand@objectstack/client-react, and it passed on re-run (259 blocks).eslint --no-inline-config --format jsonover the 5 touched.tsfiles gave 5 files, 0 errors, 0 warnings. Population: the config'sfilesglobs are**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}and thepackages/**/**/*.{ts,…}blocks. The other two changed paths (.md,.mdx) match nofilesglob. Invariance: all 7parserOptionsblocks are{ ecmaVersion, sourceType }and none setsproject, so there is no type-aware linting. The config reads onlyscripts/slot-lookup-baseline.jsonandscripts/query-options-erasure-baseline.jsonfrom disk, and this diff touches neither. So the diff cannot change the verdict on any untouched file.Acceptance notes
recordandparams. A screen in a webhook-started flow can therefore never be answered by that payload, because the inference reads every key as a record seed. That leans toward pausing (a lost skip, never a skipped screen). This is an observation, not filed.packages/metadata-protocol/src/runtime-authoring-gate.ts:106-108abbreviates the schedule context as{ event: 'schedule', params: { jobId, flowName, schedule } }. Its subject istenantId, and it already omits the conditionaltenantId. Left as it is.Generated by Claude Code