Skip to content

fix(trigger-schedule): a scheduled run's platform seeds never answer a screen field (#19900) - #19982

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-19900-schedule-caller-param-keys
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-19900-schedule-caller-param-keys

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 its params seeds (jobId, flowName, schedule) is a caller's answer. The screen node'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 in params, so flows that read them are unaffected.

Premise, re-measured on origin/main 2c1011b01b

  • packages/triggers/trigger-schedule/src/schedule-trigger.ts:806-810 builds params: { jobId, flowName: binding.flowName, schedule } and sets no callerParamKeys. It is the only producer of an event: 'schedule' context under packages/ (non-test git grep).
  • The reader treats [] as "the caller supplied nothing", not as absent. callerSupplied in screen-input-contract.ts:357 reads if (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.
  • Pre-fix red: I ran the new test file against the unfixed trigger. With a required screen field schedule, the run completed instead of pausing: success: true, output: { schedule: { type: 'cron', expression: '0 2 * * *' } }, and node screen_1 had status success. jobId and flowName failed the same way.

Files

  1. packages/triggers/trigger-schedule/src/schedule-trigger.ts: adds callerParamKeys: [] (the fix).
  2. packages/triggers/trigger-schedule/src/schedule-caller-param-keys-e2e.test.ts (new): the real ScheduleTrigger is bound through the real AutomationEngine with builtin nodes. A type: 'schedule' flow runs start, then screen_1, then end. The job is fired by hand. The test reads the context the trigger passed to engine.execute and the result the engine returned to the trigger's callback. It pins:
    • the context carries callerParamKeys: [], and the seeds are still in params;
    • a required field named schedule, jobId or flowName pauses at screen_1 (it.each), and listSuspendedRuns() names the run;
    • an all-optional screen with all three names pauses too;
    • control: on the same flow and engine, a context that names schedule as a caller key continues. The screen is not paused just because the flow is a schedule flow.
  3. Four texts this change would have made false. These are outside the expected landing:
    • packages/spec/src/contracts/automation-service.ts, the callerParamKeys TSDoc. 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.
  4. .changeset/19900-schedule-caller-param-keys.md: @objectstack/trigger-schedule patch and @objectstack/spec patch. The spec TSDoc ships: after the build, the new sentence is in packages/spec/dist/contracts/index.d.ts and index.d.mts, one hit each. The service-automation docblock does not ship: grep -rl over its dist/ finds 0 files, while the positive control judgeHeadlessScreen is found in index.js and index.cjs. So service-automation has no entry.

The pending callerParamKeys release note (#19846) is left as it is

That 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/main refused 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 (blob c5cbff2689, 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 TimeRelativeTrigger and passed its callback to the real engine. The run context had keys record, object, event, params, with params === record (the same object) and no callerParamKeys. A screen with required fields named after swept columns (name, status) paused at screen_1. An all-optional screen with the same names also paused after a structuredClone round trip. The inference's record leg already treats every key as a seed, because params is 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 (params is the same object as record, from record-change-trigger.ts:499) paused. The webhook shape (record: payload, params: { ...payload }, from api-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 --listFiles includes the new test file (7 test files in the program).
  • @objectstack/service-automation: vitest run, 144 files, 1725 tests passed. typecheck exit 0.
  • @objectstack/spec: vitest run --project local, 530 files, 15617 passed and 1 todo. typecheck exit 0 (tsc --noEmit, check:scripts-typecheck, check:test-typecheck).

Ablation of the pin

I committed the fix first, at afde207. Then node scripts/ablation-replace.mjs --file packages/triggers/trigger-schedule/src/schedule-trigger.ts --anchor 'callerParamKeys: [],' --delete -- (the test run):

  • Mutation landed: anchor count 1 to 0; blob 5d67f8394deb to 85d2e4c9054f.
  • The test went red: 5 failed and 1 passed (the control). Failure output: expected undefined to deeply equal [], and four times expected { success: true, …(4) } to match object { status: 'paused', …(1) }.
  • Restored: the blob after restore is 5d67f8394deb, the same as at HEAD, and git diff HEAD is empty.
  • The subject is imported as ./schedule-trigger.js (source), not through a package export, so no dist/ preflight applies.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 107 commands from the change set, with no paths passed. All 107 ran at eeed06a5b, each with its exit code recorded. The --ran reconciliation reads: 107 derived, 105 run, 2 NOT-MEASURED, 0 UNRUN.

  • NOT MEASURED: check:dual-build-cjs-loads and check: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-test first 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-examples first exited 3 because client-react was not built. I built @objectstack/client and @objectstack/client-react, and it passed on re-run (259 blocks).
  • ESLint, narrowed to the touched files: eslint --no-inline-config --format json over the 5 touched .ts files gave 5 files, 0 errors, 0 warnings. Population: the config's files globs are **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and the packages/** / **/*.{ts,…} blocks. The other two changed paths (.md, .mdx) match no files glob. Invariance: all 7 parserOptions blocks are { ecmaVersion, sourceType } and none sets project, so there is no type-aware linting. The config reads only scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json from disk, and this diff touches neither. So the diff cannot change the verdict on any untouched file.

Acceptance notes

  • The webhook trigger passes its whole payload as both record and params. 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.
  • The docblock in packages/metadata-protocol/src/runtime-authoring-gate.ts:106-108 abbreviates the schedule context as { event: 'schedule', params: { jobId, flowName, schedule } }. Its subject is tenantId, and it already omits the conditional tenantId. Left as it is.

Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/service-automation, @objectstack/spec, @objectstack/trigger-schedule, touching 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/services/service-automation/src/screen-input-contract.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/ui/actions.mdx (via AutomationContext (symbol, a top-level interface))

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

  • content/docs/releases/v15.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v17/17-3.mdx (via AutomationContext (symbol, a top-level interface))

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
  • 1 changed file(s) yielded no anchor (packages/services/service-automation/src/screen-input-contract.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 136 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 2c1011b01bc071c545f72f2761647b8d9ab56375 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c1dcb46b69eb3dfc3cc5140dccd71bdaafae3af — the merge of head eeed06a5be7b5efca2b669d1e9114fed5f00c7b2 into base 2c1011b01bc071c545f72f2761647b8d9ab56375, 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 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

⚠️ 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 2c1011b01bc071c545f72f2761647b8d9ab56375 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: eeed06a5be

Rendered by an isolated at-tier reviewer and adopted by the domain:services seat (session_01Evb5jFDZGKQE9KG4jbMfMF). The seat's own verdict is on #19900. The reviewer was given the PR, the cards and origin/main, and not the dispatch order.

① Derived judgments

  • packages/spec: no accept-set or public-surface change. The only hunk in packages/spec/src/contracts/automation-service.ts is 7 TSDoc lines inside the callerParamKeys comment. callerParamKeys?: string[] and every other member are byte-identical, and no export moves. Type Check · source gates (check:generated --reconcile-only, check:docs) is green on the head.
  • @objectstack/trigger-schedule: correct. Every scheduled run context now carries callerParamKeys: []. When the key is present, callerSupplied returns declared.includes(name). So no seed is caller-supplied, and the delta is exactly the three seed names, only ever toward a pause.
  • The TSDoc is true on this head. Time-relative, record-change and webhook set no key. subflow and map delete it. The only other direct execute producer is the engine's trigger pass-through.
  • Clause-②: no is correct. No key, type or export moves. A producer emitting an already-declared optional key widens nothing an implementer must accept.
  • One contradiction remains in published text: the pending .changeset/19846-automation-caller-param-keys.md still says the schedule trigger leaves the key absent (see ③). flows.mdx, the screen-input-contract.ts docblock and the spec pin-test comment are corrected.
  • Precision nit, non-blocking: "a screen in a scheduled flow pauses, whatever its fields are named" does not hold for a waitForInput: false screen. That screen passes through for every caller, and the adjacent flows.mdx paragraph already says so.

② Semver level

Correct and complete.

  • @objectstack/trigger-schedule: patch, a bug fix with no new surface.
  • @objectstack/spec: patch. The TSDoc ships in dist/contracts/index.d.ts, and Clause-②: no keeps it at patch. Check Changeset is green on the head.
  • @objectstack/service-automation needs no entry. Its only change is the docblock on the module-private callerSupplied, which reaches neither the JS output nor the d.ts.

③ Boundary flags

Implemented-by: claude/issue-19900-schedule-caller-param-keys
Reviewed-by: session_01Evb5jFDZGKQE9KG4jbMfMF

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 14:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit ae7a35a Sep 24, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19900-schedule-caller-param-keys branch September 24, 2026 14:57
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… 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>
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

1 participant