Repository navigation
feat(spec,runtime,service-automation): the flow doors state callerParamKeys, and the headless screen verdict reads it - #19899
Conversation
…ams keys the caller supplied AutomationContext gains an optional callerParamKeys. The action door (dispatchFlowAction) and the trigger door (buildAutomationContext) fill it from the caller's own bag before seeding, leaving their row-id keys out. The screen node's headless verdict reads it instead of inferring provenance; an absent signal keeps the inference. subflow and map child runs drop the parent's list. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…tract, the two doors and the screen verdict The two boundary constructions (non-default recordIdField with a recordIdParam naming a key the record lacks, object-less and object-bound) pause on the real engine; the doors hand the engine exactly those contexts. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…ovenance signal; changeset The accepted-cost sentence now describes the doors' callerParamKeys and the row-id keys they leave out. The durable-store gap paragraph is replaced: it has not held since the record leg compared by value (4177ed3), and the fallback inference for producers without the signal is described instead. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…tomation-caller-provenance
…ies callerParamKeys The endpoint executor sends buildAutomationContext's output, which now states the caller's keys; the pin asserts the whole context with toEqual, so it names the new key rather than loosening to a partial match. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…tomation-caller-provenance
…ing commit The comment moved into flowActionRowIdKeys with the code it explains; the card number it carried no longer resolves (live citation gate), so it now names the commit that landed the single predicate. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 141 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 b513eabdc4b92f077956a4f127a73e2682265505 && git checkout b513eabdc4b92f077956a4f127a73e2682265505
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1f0b3419ff818c5b97c2d0c00dc917410a67e8b2 0155b343ef2c723a5a92f6c1a52b3a67b1db69cd && git checkout -B drift-repro 1f0b3419ff818c5b97c2d0c00dc917410a67e8b2 && git merge --no-ff 0155b343ef2c723a5a92f6c1a52b3a67b1db69cd
node scripts/docs-audit/affected-docs.mjs --json 1f0b3419ff818c5b97c2d0c00dc917410a67e8b2
|
Contract reviewServed-tier: Reviewed and posted 2026-09-23T16:55Z by the at-tier review subagent the ① Derived judgmentsProducers of an
Can a caller make a filled list name a key it did not supply, or omit one it did? No and yes-by-design: both lists are Row-id keys. Trigger door leaves out Pins. Head: service-automation 41/41 (11 new + 24 + 6 untouched), runtime 62/62, spec 2/2. Ablations (each mutated file hash-checked, restored with Sentences that ship. Docblock, changeset, Dev-report claim that is misattributed (not in shipped text): the action door's Governed surfaces: none in the file list. Model identifiers: swept the diff, all seven commit messages and the PR body for every spelling — none. ② Semver level
CI at the head, 46 runs de-duplicated by name keeping the latest ③ Boundary flags
Blocking: none. Implemented-by: VERDICT: PASS |
…a screen field (objectstack-ai#19900) (objectstack-ai#19982) Fixes objectstack-ai#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 objectstack-ai#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 (objectstack-ai#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](https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… 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 #19846
Clause-②: yes
Rewritten short by the
domain:spec#5seat (2026-09-23T16:17Z). The dev report is on #19846; the earlier long body is in the edit history.AutomationContextgains an optionalcallerParamKeys?: string[](packages/spec): the keys of the caller's ownparams. When it is present, the screen node's headless verdict reads it and never infers. When it is absent (an older producer), the inference from #15787 runs unchanged.What changed
Doors that fill it:
dispatchFlowAction(RESTPOST /api/v1/actions/..., MCPrun_action);buildAutomationContext(POST /automation/:name/trigger, the legacy route, a declarativetype: 'flow'endpoint).Each door records the caller's keys before seeding anything. It leaves out its row-id keys:
recordId, the camelCase object-id alias, and on the action door the action'srecordIdParam.Producers that leave it absent (today's inference): the record-change, schedule, time-relative and webhook triggers, and direct
engine.executecallers.Child runs:
subflowandmapdrop the parent's list from the child run's context.Behaviour edge: a caller that sends the action's
recordIdParamkey with a value other than the row id no longer answers a screen field of that name. The changeset says so.Docs:
flows.mdxand the changeset describe the signal;flows.mdx"known gap" paragraph on durable resume is replaced, because it has been false since4177ed3a84.Pins
screen-headless-caller-signal.test.ts). Removing the signal read turns both red.screen-headless-satisfaction.test.tsandscreen-headless-provenance-durable-resume.test.tsare untouched and green; they now pin the absent path. The dev report names the fixtures in them that encode the inference rather than the contract.endpoint-executor.test.tscompares the whole context withtoEqual, so it now includescallerParamKeys: ['reason'].Not in this PR
paramscan still answer a screen: filed as A scheduled flow run skips a screen whose required field is namedschedule,jobIdorflowName: the schedule trigger's own params are read as the caller's answers #19900.run_actionon a screen flow dead-ends: the screen pauses even when every input is bound, andlist_actionsnever surfaces flow input variables #15705 stays open for the MCP resume verb.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1