[feat] Experimental write-only tags/attributes#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: Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro 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: 7f45c2c 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✅ All tests passed Summary
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>
CI build error: the umbrella workflow package re-exports \`@workflow/core/_workflow\`, which (via my earlier setAttributes export) transitively pulled \`step/context-storage\` and therefore \`node:async_hooks\`. The workflow VM bundle's no-Node-module constraint rejected it. Split into three modules: - set-attributes-shared.ts: validation + 'use step' dispatcher. No contextStorage import, safe in both bundles. - set-attributes.ts (step/host): looks up runId via contextStorage, falls back to WORKFLOW_CONTEXT_SYMBOL. Re-exported from core/index. - workflow/set-attributes.ts (VM): reads only WORKFLOW_CONTEXT_SYMBOL. Re-exported from core/_workflow. Mirrors the same dual-context layout as getWorkflowMetadata. 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>
Summary
Implements the V5 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.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 and ElectroDB column.What's in this PR
@workflow/worldapplyAttributeChangeshelper. OptionalexperimentalSetAttributesonStorage.runs. Optionalattributesfield onWorkflowRunBaseSchema.@workflow/coresetAttributes(record)helper. Detects workflow VM vs step context, normalizesundefined → null, validates client-side, dispatches via an internal"use step"function. Feature-detects the world method and no-ops with a singleconsole.warnif missing.@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.What's NOT in this PR (not in MVP)
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)