Attributes MVP (experimental and write-only)#2088
Conversation
Outlines the bare-MVP, write-only attributes design that defers the `attr_set` event type (and the associated SPEC_VERSION_CURRENT bump) to the full 5.0.0 feature. Forward-compatible SDK surface and wire format. `experimentalSetAttributes` is optional on the World interface so third-party worlds keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
🦋 Changeset detectedLatest commit: 45aa4c8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests▲ Vercel Production (1 failed)nextjs-turbopack (1 failed):
💻 Local Development (14 failed)nextjs-webpack-canary (7 failed):
nextjs-webpack-stable-lazy-discovery-disabled (7 failed):
Details by Category❌ ▲ Vercel Production
❌ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
Implements the V5 Workflow Attributes MVP per the changelog plan:
- @workflow/world: shared validation + apply helpers; optional
experimentalSetAttributes on Storage.runs; attributes field on
WorkflowRunBaseSchema. Optional so third-party worlds keep working.
- @workflow/core: setAttributes() helper. Detects workflow VM vs step
context, normalizes undefined→null, validates client-side, dispatches
via an internal "use step" function. Feature-detects the world method
and no-ops with a one-time warning if missing.
- @workflow/world-local: file-backed impl with a per-run async mutex
so concurrent writes do not lose updates within a process. Threads
attributes through the run lifecycle event reconstructions so they
survive subsequent run_started/_completed/_failed/_cancelled writes.
- @workflow/world-postgres: jsonb column with SQL-side atomic merge
(jsonb_set / `-`); 0013 migration.
- @workflow/world-vercel: HTTP wrapper posting the documented
{ changes: [...] } body to /v2/runs/:runId/attributes.
Tests: 18 validation unit + 10 SDK unit + 10 world-local integration +
3 world-postgres integration. End-to-end coverage in the workbench is
deferred until the paired workflow-server endpoint is deployed to a
preview.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous design used a 'use step' indirection inside @workflow/core so setAttributes could be called from both workflow and step bodies via a single SDK surface. That broke nextjs-webpack Local Dev: the deferred- entries discoverer in webpack dev mode walks transitive imports from 'use step' files, and putting a step file inside @workflow/core/dist pulled host-side world adapters and @vercel/queue into the step-discovery graph. Webpack's regex-based import extractor then blew the call stack with "RangeError: Maximum call stack size exceeded at RegExpStringIterator.next" on tarball-installed deployments. runtime/start.ts and runtime/run.ts get away with the same directive because they're never reachable from packages/core/src/workflow/index.ts (the VM bundle entry); ours was. A host-side bridge comparable to sleep would have fixed it but is substantial wiring for a feature whose end state (event-sourced attr_set) replaces the bridge mechanism entirely. Pragmatic MVP path: restrict to step body and let users wrap in a step explicitly. Full 5.0.0 lifts the restriction via attr_set events through the workflow controller; SDK signature is stable across the cutover. - Workflow-VM-side setAttributes throws FatalError with wrap-in-step instructions - Step-side setAttributes works (validates, dispatches, world-detects) - set-attributes-shared.ts is now pure validation; no 'use step', no world imports - Test updated to assert the FatalError on workflow-body calls - Changelog MDX updated with the new scope + a "Why workflow-body dispatch is deferred" implementation note Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… setAttributes is "supported via the host-side bridge in `set-attributes.ts`, which calls into an actual step via `registerStepFunction`" — no such bridge or registerStepFunction usage exists. This commit fixes the issue reported at packages/core/src/set-attributes-shared.ts:25 **Bug:** The comment on lines 24-26 of `packages/core/src/set-attributes-shared.ts` describes an intermediate design approach that was abandoned before the final implementation. It states: "workflow-body use is supported only via the host-side bridge in `set-attributes.ts`, which calls into an actual step via `registerStepFunction`." In the actual final implementation: 1. `packages/core/src/workflow/set-attributes.ts` (the workflow-VM-side export) unconditionally throws `FatalError` — there is no bridge support at all. 2. `packages/core/src/set-attributes.ts` (the host-side export) explicitly checks for workflow-body context and throws `FatalError` with a message telling users to wrap the call in a `'use step'` function. 3. A grep for `registerStepFunction` in combination with `setAttributes` returns zero results — no such wiring exists. The comment is misleading to any developer reading the codebase: it implies workflow-body use works via a bridge mechanism, when in fact it throws a fatal error. **Fix:** Updated lines 24-26 to accurately describe the actual behavior: "workflow-body use throws FatalError — users must wrap the call in their own `'use step'` function." This aligns with the implementation in both `set-attributes.ts` and `workflow/set-attributes.ts`, and with the JSDoc on `setAttributes` which explicitly documents the step-body-only restriction and the workaround pattern. Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: VaguelySerious <mittgfu@gmail.com>
For local e2e validation against the workflow-server attributes-mvp preview deployment. Do not merge — this constant must be empty on main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n step Workflow-body `setAttributes` calls now dispatch through an internal `__builtin_set_attributes` step rather than throwing FatalError. The workflow-VM helper validates input and then invokes a host-side useStep dispatcher pre-bound under WORKFLOW_SET_ATTRIBUTES; the step body forwards to the same world adapter call the step-body path uses. Putting the 'use step' directive inside `packages/workflow/src/internal/builtins.ts` (next to the existing `__builtin_response_*` builtins) instead of `@workflow/core/dist/` avoids the deferred-entry discoverer hazard that motivated the prior MVP-only step-body restriction. - Add `WORKFLOW_SET_ATTRIBUTES` symbol + workflow.ts wiring - New `step-set-attributes.ts` host helper (`applySetAttributesChanges`) shared between step-body and workflow-body paths - `__builtin_set_attributes` builtin step - Refactor workflow-side `setAttributes` to dispatch via the bridge - Update changelog MDX + add tests (16 unit, 2 e2e) - e2e workflows `setAttributesFromStepWorkflow` and `setAttributesFromWorkflowBodyWorkflow` cover both paths Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The comment described the prior FatalError-on-workflow-body workaround; update it to reflect the current bridge-via-builtins design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…verer
`__builtin_set_attributes` dynamically imported
`@workflow/core/_step-set-attributes` as a literal string. The Next.js
deferred-entries discoverer in `@workflow/next/builder-deferred.ts`
matches `import('...')` regex-style and walks the resolved file's
transitive imports — which reach the world adapter and `@vercel/queue`,
triggering `RangeError: Maximum call stack size exceeded` inside
`RegExpStringIterator.next` on tarball-installed nextjs-webpack
builds. The build never completes, so dev-mode e2e tests time out
across the board.
Assemble the specifier at runtime (same trick `get-world-lazy.ts`
uses for `./world.js`) so the discoverer doesn't see a literal target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idge The runtime-built specifier broke step bundle resolution across every framework — `Cannot find package '@workflow/core' imported from .../node_modules/.nitro/workflow/steps.mjs`. Bundlers can't statically resolve a concatenated string, so the dependency never lands in the step bundle and Node's loader fails at runtime. Reverts fbb0c59. The original webpack-dev discoverer-overflow it tried to fix only affected 3 jobs; this regression took down 30+ jobs across all frameworks. The discoverer issue needs a different fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngle dispatch path
Drops step-body support for the MVP. The architecture becomes:
- workflow VM `setAttributes` validates inline and dispatches via the
standard `globalThis[WORKFLOW_USE_STEP]('__builtin_set_attributes')`
mechanism — same as every other step call from a workflow body
- host-side `setAttributes` is a stub that throws FatalError telling
callers to use a workflow body
- `__builtin_set_attributes` step body reads world + run id directly
from `globalThis` symbols populated by the runtime, with no imports
from `@workflow/core`
This deletes the bridge plumbing the previous design needed:
- `WORKFLOW_SET_ATTRIBUTES` global symbol + the workflow.ts pre-bind
- `packages/core/src/set-attributes-shared.ts` (normalize helper)
- `packages/core/src/step-set-attributes.ts` (host-side helper)
- the `@workflow/core/_step-set-attributes` package export and the
dynamic import that pulled it in from the step bundle
Side benefit: the Next.js deferred-entries discoverer can no longer
walk from `__builtin_set_attributes` into the world adapter / queue
chain that broke webpack-dev builds in the previous shape, because the
step body holds zero @workflow/core imports.
Workbench example and e2e tests reduced to a single
`setAttributesWorkflow` covering workflow-body dispatch. World-side
implementations are unchanged; step-body support can be added later
without touching the workflow-body contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| const WORKFLOW_SERVER_URL_OVERRIDE = | ||
| 'https://workflow-server-git-peter-attributes-mvp.vercel.sh'; |
Summary
Implements the Workflow Attributes MVP — a minimal, write-only attributes API designed to land before the full event-sourced attributes feature in #1933 (which requires a
SPEC_VERSION_CURRENTbump and coordinated rollout across worlds, builders, and runtime).User surface:
Attributes are stored plaintext on the
WorkflowRunentity and visible viaworld.runs.get()/world.runs.list()(and any observability surface built on top). The wire format mirrors the futureattr_setevent'seventData.changes, so the SDK signature and wire body shape are stable across MVP → 5.0.0.setAttributesis callable from a workflow body only. The call is dispatched through an internal__builtin_set_attributesstep bridge so the mutation gets astep_created → step_completedevent pair without inventing a new event type. The host-side export (resolved from step bodies or plain host code) throwsFatalErrordirecting the caller back to a workflow body — step-body support can be added later without breaking the workflow-body contract.See
docs/content/docs/v5/changelog/attributes-mvp.mdxfor the full design, trade-offs, and implementation notes — including decisions made during build-out (endpoint namespacing, concurrency semantics, usage-fact schema choice, optional-world fallback behavior, etc.).Paired with vercel/workflow-server#442 which adds the server-side
POST /api/v2/runs/:runId/attributesendpoint, ElectroDB column, and the newWORKFLOW_ATTRIBUTEusage-fact carrying the post-merge map.What's in this PR
@workflow/worldapplyAttributeChangeshelper. OptionalexperimentalSetAttributesonStorage.runs. Optionalattributesfield onWorkflowRunBaseSchema.@workflow/coresetAttributes(record). VM-side helper validates inline and dispatches via the standardWORKFLOW_USE_STEPmechanism — no new global symbols, no bridge plumbing. Host-side export is aFatalError-throwing stub for non-workflow-body callers.workflowpackage__builtin_set_attributesstep body ininternal/builtins.ts. Reads world + run id directly fromglobalThissymbols populated by the runtime; zero imports from@workflow/coreso it stays a true leaf in the deferred-entries graph.@workflow/world-localattributesthrough the run lifecycle event reconstructions.@workflow/world-postgresattributes jsonbcolumn (migration0013_add_attributes.sql). SQL-side atomic merge usingjsonb_set/-operators in a singleUPDATE.@workflow/world-vercel{ changes: [...] }to/v2/runs/:runId/attributes.docs/content/docs/v5/changelog/attributes-mvp.mdxwith full design + implementation notes.Architecture (workflow-body dispatch)
setAttributes(attrs)from'use workflow'body.workflowpackage-exports condition topackages/core/src/workflow/set-attributes.ts, which validates the input inline and produces canonicalAttributeChange[].globalThis[WORKFLOW_USE_STEP]('__builtin_set_attributes')(changes)— same mechanism every other step call uses.__builtin_set_attributes(inpackages/workflow/src/internal/builtins.ts). The step body reads the world fromglobalThis[Symbol.for('@workflow/world//cache')]and the active run id fromglobalThis[Symbol.for('WORKFLOW_STEP_CONTEXT_STORAGE')], then callsworld.runs.experimentalSetAttributes(runId, changes). No imports from@workflow/core— this is what keeps the Next.js deferred-entries discoverer from walking into world adapters and triggering webpack's regex-extractor stack overflow.What's NOT in this PR (not in MVP)
setAttributesfrom a step body or plain host code (throwsFatalError; can be added later)getAttribute/getAttributes)start(workflow, input, { attributes })(initial attributes at run creation)runs.list({ attributes }),listAttributeKeys,listAttributeValues)attr_setevent type$-prefixed) namespace (just blocked at validation today)