You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#2226 added start(..., { attributes }) to seed plaintext attributes at run creation, but always validates them with the reserved $ prefix disallowed and exposes no opt-out. That leaves a gap relative to the rest of the attributes surface: experimental_setAttributes accepts { allowReservedAttributes: true } for framework-level callers, and the run_created / run_started event schemas plus the local and Postgres worlds already accept and validate the flag — so the wire format supports seeding reserved attributes at creation, just not through the public start() API.
This closes that gap by threading the same option through start():
StartOptions.allowReservedAttributes (same semantics and guidance as the experimental_setAttributes option) is passed to client-side validation and forwarded on the run_created eventData.
The flag rides along in the queue runInput (new optional field on RunInputSchema) and is forwarded into the run_started eventData, so the resilient-start path (run bootstrapped from the queue message when run_created was missed) validates the seeded attributes identically to the original attempt.
No world changes needed — world-local and world-postgres already honor allowReservedAttributes on both the run_created and run_started creation paths.
This unblocks framework-level use cases like cross-run lineage (#2153, $rootRunId / $parentRunId) that need reserved keys present from creation time.
Testing
pnpm --filter @workflow/core test
pnpm exec turbo run typecheck test --filter=@workflow/world --filter=@workflow/core --filter=@workflow/world-local
New unit tests: reserved keys + flag accepted and forwarded on both run_created and the queue runInput; flag absent from both payloads when not requested; size/cap validation still enforced with the flag set; reserved keys still rejected without the flag (existing test).
New e2e test (run locally against the nextjs-turbopack dev server): start() with a $-prefixed key + allowReservedAttributes: true completes, the reserved key is persisted on the run, and it survives the workflow's own attr_set writes.
workflow-server already validates and honors the flag on initial run attributes (InitialRunAttributesSchema) and forwards it on its own resilient path, so no server change is needed.
experimental_setAttributes already exposes allowReservedAttributes for
framework-level callers that own a $-prefixed sub-namespace, and the
run_created / run_started event schemas plus the local and Postgres
worlds already accept and validate the flag. start() was the one gap:
it always validated initial attributes with the reserved prefix
disallowed and had no way to opt out, so framework code could not seed
reserved attributes at run creation.
Thread the option through start():
- StartOptions.allowReservedAttributes, passed to client-side
validation and forwarded on the run_created eventData
- carried in the queue runInput (new RunInputSchema field) and
forwarded to run_started so the resilient/lazy run creation path
validates identically
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR extends the start() API to optionally permit reserved $-prefixed attribute keys at run creation time (for framework/library callers), and ensures the same behavior is preserved for the resilient-start (queue → run_started) creation path.
Changes:
Add StartOptions.allowReservedAttributes and thread it through client-side attribute validation in start().
Carry allowReservedAttributes through the queue runInput and into run_started eventData for resilient run creation.
Add unit tests and update v5 start() documentation and package changesets.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
…ributes
Verified locally against the nextjs-turbopack dev server: the reserved
key passes client and server validation, lands on the run at creation,
and survives the workflow's own attr_set writes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Approve — minimal, correct gap-closure; every claimed precondition verified
This is the right shape for this change: the wire format and storage layers already supported seeding reserved attributes at creation — only the public start() API lacked the escape hatch. I verified each "already supported" claim in the PR description against the code:
normalizeAttributeChanges(attrs, { allowReservedAttributes }) — the options parameter and semantics already exist (added with the attributes MVP), so client-side validation is byte-identical to the experimental_setAttributes path ✓
run_created, run_started, and attr_set event schemas all already carry allowReservedAttributes: z.literal(true).optional() ✓
world-local honors the flag on both creation paths (events-storage.ts handles it for run_created and the resilient run_started bootstrap) ✓
So this PR is genuinely just threading: StartOptions → client validation → run_created eventData → queue runInput → resilient run_started eventData.
What I particularly like
The resilient-start path is handled, not forgotten. The subtle failure mode here would be: run_created is lost, the run bootstraps from the queue message via run_started, and the server-side validation rejects the reserved keys because the flag didn't ride along — making resilient starts behave differently from normal ones for exactly the callers using this option. The shared attributeSeed object + the RunInputSchema field + the runtime.ts forwarding close that loop, and the unit test asserts the flag lands on both payloads.
z.literal(true).optional() matches the established event-schema style — the flag is either true or absent, never false noise on the wire, and the ...(allowReservedAttributes ? { allowReservedAttributes: true as const } : {}) spread keeps the payloads clean in the default case (also pinned by the not-requested test).
The negative test matters most: still enforces non-reserved validation rules when allowReservedAttributes is set proves the flag only bypasses the $-prefix check, not size caps — the escape hatch doesn't accidentally become a validation bypass.
The option docstring is the best namespace-ownership explanation in the attributes surface so far ("only flip this to true if your caller is itself a framework or library that owns a $-prefixed sub-namespace").
The e2e test smartly reuses experimentalSetAttributesWorkflow and asserts the seeded reserved key survives the workflow's own attr_set writes — covering the merge path, not just creation.
No v4 docs this time — correctly scoped to v5 only, since attributes are a v5/spec-4 surface. (A pleasant break from the recurring sequencing flag.)
Verified locally
27 start tests, full core suite (1189), and world suite (50) all pass
Docs link target (experimental-set-attributes.mdx) exists
Changeset covers exactly the changed packages (workflow/@workflow/core fixed pair + @workflow/world), all minor — correct for new API surface
One observation (no action needed)
Version-skew safety of the new RunInputSchema field: an older consumer parsing a queue message with the extra field would silently strip it (Zod non-strict) and reject the reserved keys on the resilient path — but queue messages are deployment-pinned to the SDK that enqueued them, so producer and consumer can't skew. Worth knowing, not worth changing.
This change builds directly on the native run-attributes surface introduced only on main: I verified that on origin/stable, packages/core/src/runtime/start.ts, packages/world/src/queue.ts, and packages/core/src/runtime.ts contain no attributes support at all (no start(..., { attributes }), no RunInputSchema.attributes, no normalizeAttributeChanges), and the touched docs page docs/content/docs/v5/api-reference/workflow-api/start.mdx does not exist on stable. Without the underlying attributes/spec-version-4 feature from #2226 and experimental_setAttributes, the allowReservedAttributes option has nothing to attach to on stable.
To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#2226 added
start(..., { attributes })to seed plaintext attributes at run creation, but always validates them with the reserved$prefix disallowed and exposes no opt-out. That leaves a gap relative to the rest of the attributes surface:experimental_setAttributesaccepts{ allowReservedAttributes: true }for framework-level callers, and therun_created/run_startedevent schemas plus the local and Postgres worlds already accept and validate the flag — so the wire format supports seeding reserved attributes at creation, just not through the publicstart()API.This closes that gap by threading the same option through
start():StartOptions.allowReservedAttributes(same semantics and guidance as theexperimental_setAttributesoption) is passed to client-side validation and forwarded on therun_createdeventData.runInput(new optional field onRunInputSchema) and is forwarded into therun_startedeventData, so the resilient-start path (run bootstrapped from the queue message whenrun_createdwas missed) validates the seeded attributes identically to the original attempt.No world changes needed — world-local and world-postgres already honor
allowReservedAttributeson both therun_createdandrun_startedcreation paths.This unblocks framework-level use cases like cross-run lineage (#2153,
$rootRunId/$parentRunId) that need reserved keys present from creation time.Testing
pnpm --filter @workflow/core testpnpm exec turbo run typecheck test --filter=@workflow/world --filter=@workflow/core --filter=@workflow/world-localrun_createdand the queuerunInput; flag absent from both payloads when not requested; size/cap validation still enforced with the flag set; reserved keys still rejected without the flag (existing test).start()with a$-prefixed key +allowReservedAttributes: truecompletes, the reserved key is persisted on the run, and it survives the workflow's ownattr_setwrites.InitialRunAttributesSchema) and forwards it on its own resilient path, so no server change is needed.Docs Preview
start()API reference🤖 Generated with Claude Code