Skip to content

[feat] Experimental write-only tags/attributes#2088

Draft
VaguelySerious wants to merge 7 commits into
mainfrom
peter/attributes-mvp-plan
Draft

[feat] Experimental write-only tags/attributes#2088
VaguelySerious wants to merge 7 commits into
mainfrom
peter/attributes-mvp-plan

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented May 22, 2026

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_CURRENT bump and coordinated rollout across worlds, builders, and runtime).

User surface:

import { setAttributes } from 'workflow';

// from a workflow body or a step body
await setAttributes({ phase: 'processing', orderId: 'ord_123' });
await setAttributes({ orderId: undefined }); // remove a key

Attributes are stored plaintext on the WorkflowRun entity and visible via world.runs.get() / world.runs.list() (and any observability surface built on top). The wire format mirrors the future attr_set event's eventData.changes, so the SDK signature and wire body shape are stable across MVP → 5.0.0.

See docs/content/docs/v5/changelog/attributes-mvp.mdx for 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/attributes endpoint and ElectroDB column.

What's in this PR

Layer Change
@workflow/world Shared validation + applyAttributeChanges helper. Optional experimentalSetAttributes on Storage.runs. Optional attributes field on WorkflowRunBaseSchema.
@workflow/core New setAttributes(record) 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 single console.warn if missing.
@workflow/world-local Filesystem impl with a per-run async mutex so concurrent writes within a process do not lose updates. Threads attributes through the run lifecycle event reconstructions.
@workflow/world-postgres New attributes jsonb column (migration 0013_add_attributes.sql). SQL-side atomic merge using jsonb_set / - operators in a single UPDATE.
@workflow/world-vercel Pure HTTP wrapper posting { changes: [...] } to /v2/runs/:runId/attributes.
Docs Changelog entry at docs/content/docs/v5/changelog/attributes-mvp.mdx with full design + implementation notes.

What's NOT in this PR (not in MVP)

  • Reading attributes inside a workflow or step (getAttribute / getAttributes)
  • start(workflow, input, { attributes }) (initial attributes at run creation)
  • Filtering / enumerating runs by attribute (runs.list({ attributes }), listAttributeKeys, listAttributeValues)
  • Writer attribution (workflow vs step + attempt) — needs the attr_set event type
  • Non-string value types
  • Reserved-key ($-prefixed) namespace (just blocked at validation today)

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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.030s (-31.4% 🟢) 1.006s (~) 0.975s 10 1.00x
💻 Local Nitro 0.032s (-26.0% 🟢) 1.005s (~) 0.973s 10 1.05x
🐘 Postgres Nitro 0.046s (-51.5% 🟢) 1.011s (-3.1%) 0.965s 10 1.52x
💻 Local Next.js (Turbopack) 0.050s 1.005s 0.956s 10 1.63x
🐘 Postgres Express 0.052s (-9.7% 🟢) 1.012s (~) 0.959s 10 1.72x
🐘 Postgres Next.js (Turbopack) 0.058s 1.011s 0.953s 10 1.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.354s (-13.6% 🟢) 2.274s (-9.4% 🟢) 1.920s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.074s (-4.5%) 2.006s (~) 0.932s 10 1.00x
💻 Local Nitro 1.075s (-5.0% 🟢) 2.005s (~) 0.931s 10 1.00x
🐘 Postgres Express 1.082s (-5.6% 🟢) 2.009s (~) 0.927s 10 1.01x
🐘 Postgres Nitro 1.084s (-4.9%) 2.010s (~) 0.925s 10 1.01x
💻 Local Next.js (Turbopack) 1.108s 2.006s 0.898s 10 1.03x
🐘 Postgres Next.js (Turbopack) 1.121s 2.009s 0.889s 10 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.571s (-59.6% 🟢) 3.227s (-45.4% 🟢) 1.656s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.405s (-5.1% 🟢) 11.015s (~) 0.610s 3 1.00x
💻 Local Express 10.411s (-4.7%) 11.022s (~) 0.611s 3 1.00x
🐘 Postgres Nitro 10.424s (-4.1%) 11.016s (~) 0.592s 3 1.00x
💻 Local Nitro 10.425s (-4.8%) 11.022s (~) 0.597s 3 1.00x
💻 Local Next.js (Turbopack) 10.654s 11.021s 0.367s 3 1.02x
🐘 Postgres Next.js (Turbopack) 10.742s 11.019s 0.278s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 12.946s (-45.4% 🟢) 14.168s (-43.6% 🟢) 1.222s 3 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.447s (-7.8% 🟢) 14.017s (-6.7% 🟢) 0.570s 5 1.00x
🐘 Postgres Nitro 13.489s (-7.6% 🟢) 14.019s (-6.7% 🟢) 0.530s 5 1.00x
💻 Local Express 13.492s (-9.9% 🟢) 14.027s (-6.7% 🟢) 0.535s 5 1.00x
💻 Local Nitro 13.576s (-9.9% 🟢) 14.027s (-12.5% 🟢) 0.451s 5 1.01x
💻 Local Next.js (Turbopack) 14.075s 15.029s 0.954s 4 1.05x
🐘 Postgres Next.js (Turbopack) 14.197s 15.011s 0.814s 4 1.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 20.011s (-69.0% 🟢) 21.438s (-67.8% 🟢) 1.428s 3 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 11.894s (-15.1% 🟢) 12.142s (-16.8% 🟢) 0.248s 8 1.00x
💻 Local Express 11.961s (-28.0% 🟢) 12.147s (-28.7% 🟢) 0.186s 8 1.01x
🐘 Postgres Nitro 11.968s (-14.3% 🟢) 12.390s (-13.4% 🟢) 0.422s 8 1.01x
💻 Local Nitro 12.304s (-26.7% 🟢) 13.167s (-22.7% 🟢) 0.863s 7 1.03x
💻 Local Next.js (Turbopack) 12.967s 13.024s 0.057s 7 1.09x
🐘 Postgres Next.js (Turbopack) 13.187s 14.016s 0.829s 7 1.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 28.418s (-93.3% 🟢) 30.458s (-92.8% 🟢) 2.040s 3 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.138s (-9.7% 🟢) 2.007s (~) 0.869s 15 1.00x
🐘 Postgres Nitro 1.139s (-10.7% 🟢) 2.007s (~) 0.868s 15 1.00x
💻 Local Nitro 1.148s (-29.6% 🟢) 2.005s (-3.3%) 0.857s 15 1.01x
💻 Local Express 1.176s (-21.0% 🟢) 2.006s (~) 0.830s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.223s 2.008s 0.785s 15 1.07x
💻 Local Next.js (Turbopack) 1.246s 2.006s 0.760s 15 1.09x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.370s (-15.9% 🟢) 3.993s (-7.6% 🟢) 1.623s 8 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.192s (-49.5% 🟢) 2.008s (-33.3% 🟢) 0.816s 15 1.00x
🐘 Postgres Nitro 1.206s (-48.7% 🟢) 2.008s (-33.2% 🟢) 0.803s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.361s 2.007s 0.646s 15 1.14x
💻 Local Nitro 1.596s (-49.2% 🟢) 2.005s (-48.4% 🟢) 0.408s 15 1.34x
💻 Local Next.js (Turbopack) 1.713s 2.073s 0.360s 15 1.44x
💻 Local Express 1.770s (-40.0% 🟢) 2.008s (-41.9% 🟢) 0.237s 15 1.49x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.259s (-19.6% 🟢) 4.637s (-21.7% 🟢) 1.378s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.305s (-62.6% 🟢) 2.007s (-50.0% 🟢) 0.702s 15 1.00x
🐘 Postgres Nitro 1.333s (-61.7% 🟢) 2.008s (-49.9% 🟢) 0.675s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.613s 2.009s 0.396s 15 1.24x
💻 Local Nitro 3.954s (-52.6% 🟢) 4.439s (-50.8% 🟢) 0.485s 7 3.03x
💻 Local Next.js (Turbopack) 4.519s 5.012s 0.492s 6 3.46x
💻 Local Express 5.183s (-37.8% 🟢) 5.514s (-38.9% 🟢) 0.331s 6 3.97x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.202s (+75.9% 🔺) 7.603s (+37.4% 🔺) 1.401s 4 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.154s (-8.2% 🟢) 2.009s (~) 0.856s 15 1.00x
🐘 Postgres Nitro 1.154s (-8.2% 🟢) 2.009s (~) 0.855s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.204s 2.007s 0.803s 15 1.04x
💻 Local Next.js (Turbopack) 1.291s 2.005s 0.714s 15 1.12x
💻 Local Nitro 1.376s (-26.3% 🟢) 2.006s (-14.3% 🟢) 0.630s 15 1.19x
💻 Local Express 1.391s (-26.6% 🟢) 2.007s (-15.1% 🟢) 0.616s 15 1.21x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.319s (-5.7% 🟢) 3.471s (-16.7% 🟢) 1.152s 9 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.200s (-48.8% 🟢) 2.008s (-33.3% 🟢) 0.809s 15 1.00x
🐘 Postgres Nitro 1.205s (-48.5% 🟢) 2.007s (-33.3% 🟢) 0.802s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.339s 2.008s 0.669s 15 1.12x
💻 Local Nitro 1.784s (-41.8% 🟢) 2.221s (-42.9% 🟢) 0.437s 14 1.49x
💻 Local Next.js (Turbopack) 1.969s 2.507s 0.538s 12 1.64x
💻 Local Express 2.042s (-34.8% 🟢) 2.508s (-33.3% 🟢) 0.466s 12 1.70x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.167s (-2.0%) 4.432s (-12.7% 🟢) 1.264s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.317s (-62.4% 🟢) 2.007s (-50.0% 🟢) 0.690s 15 1.00x
🐘 Postgres Nitro 1.337s (-61.6% 🟢) 2.007s (-49.9% 🟢) 0.670s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.620s 2.011s 0.391s 15 1.23x
💻 Local Nitro 4.735s (-48.2% 🟢) 5.177s (-48.4% 🟢) 0.442s 6 3.60x
💻 Local Next.js (Turbopack) 4.837s 5.350s 0.513s 6 3.67x
💻 Local Express 5.367s (-39.0% 🟢) 6.016s (-35.1% 🟢) 0.649s 5 4.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.562s (-10.4% 🟢) 5.957s (-12.6% 🟢) 1.395s 6 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.452s (-44.9% 🟢) 1.006s (~) 0.554s 60 1.00x
💻 Local Nitro 0.460s (-53.1% 🟢) 1.004s (-8.2% 🟢) 0.544s 60 1.02x
🐘 Postgres Express 0.464s (-44.7% 🟢) 1.006s (-1.6%) 0.543s 60 1.03x
💻 Local Express 0.485s (-50.8% 🟢) 1.004s (-6.7% 🟢) 0.519s 60 1.07x
🐘 Postgres Next.js (Turbopack) 0.669s 1.006s 0.337s 60 1.48x
💻 Local Next.js (Turbopack) 0.708s 1.004s 0.296s 60 1.57x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.797s (-78.2% 🟢) 6.355s (-73.6% 🟢) 1.558s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.016s (-48.6% 🟢) 1.435s (-36.5% 🟢) 0.418s 63 1.00x
🐘 Postgres Nitro 1.027s (-46.7% 🟢) 1.724s (-17.9% 🟢) 0.697s 53 1.01x
💻 Local Nitro 1.145s (-62.3% 🟢) 2.005s (-46.7% 🟢) 0.860s 45 1.13x
💻 Local Express 1.211s (-59.9% 🟢) 2.005s (-44.1% 🟢) 0.795s 45 1.19x
🐘 Postgres Next.js (Turbopack) 1.624s 2.007s 0.383s 45 1.60x
💻 Local Next.js (Turbopack) 1.837s 2.005s 0.168s 45 1.81x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 11.641s (-70.5% 🟢) 13.176s (-68.1% 🟢) 1.535s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.988s (-50.2% 🟢) 2.335s (-46.6% 🟢) 0.347s 52 1.00x
🐘 Postgres Nitro 2.099s (-48.8% 🟢) 2.582s (-43.9% 🟢) 0.483s 47 1.06x
💻 Local Nitro 2.593s (-72.1% 🟢) 3.007s (-70.0% 🟢) 0.414s 40 1.30x
💻 Local Express 2.731s (-70.3% 🟢) 3.058s (-69.5% 🟢) 0.327s 40 1.37x
🐘 Postgres Next.js (Turbopack) 3.155s 4.009s 0.853s 30 1.59x
💻 Local Next.js (Turbopack) 3.825s 4.007s 0.182s 30 1.92x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 24.357s (-74.9% 🟢) 26.608s (-73.0% 🟢) 2.252s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.165s (-41.5% 🟢) 1.005s (~) 0.840s 60 1.00x
🐘 Postgres Nitro 0.176s (-37.9% 🟢) 1.006s (~) 0.830s 60 1.06x
🐘 Postgres Next.js (Turbopack) 0.236s 1.006s 0.770s 60 1.43x
💻 Local Express 0.417s (-25.7% 🟢) 1.005s (~) 0.588s 60 2.52x
💻 Local Nitro 0.480s (-20.6% 🟢) 1.095s (+7.2% 🔺) 0.614s 55 2.91x
💻 Local Next.js (Turbopack) 0.510s 1.004s 0.494s 60 3.09x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.991s (+19.9% 🔺) 3.632s (+8.4% 🔺) 1.641s 17 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.291s (-42.9% 🟢) 1.006s (~) 0.715s 90 1.00x
🐘 Postgres Nitro 0.303s (-39.0% 🟢) 1.006s (~) 0.703s 90 1.04x
🐘 Postgres Next.js (Turbopack) 0.438s 1.006s 0.568s 90 1.51x
💻 Local Nitro 1.981s (-22.0% 🟢) 2.467s (-18.0% 🟢) 0.487s 37 6.81x
💻 Local Next.js (Turbopack) 2.156s 2.884s 0.728s 32 7.41x
💻 Local Express 2.159s (-14.1% 🟢) 2.884s (-4.2%) 0.725s 32 7.42x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.643s (+74.9% 🔺) 7.237s (+50.1% 🔺) 1.594s 13 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.598s (-27.0% 🟢) 1.006s (-1.1%) 0.408s 120 1.00x
🐘 Postgres Nitro 0.624s (-21.1% 🟢) 1.006s (~) 0.382s 120 1.04x
🐘 Postgres Next.js (Turbopack) 0.892s 1.078s 0.187s 112 1.49x
💻 Local Nitro 9.101s (-18.7% 🟢) 9.639s (-17.4% 🟢) 0.539s 13 15.22x
💻 Local Express 10.191s (-8.9% 🟢) 10.776s (-9.8% 🟢) 0.585s 12 17.05x
💻 Local Next.js (Turbopack) 10.538s 11.029s 0.491s 11 17.63x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 12.982s (+68.1% 🔺) 14.519s (+54.4% 🔺) 1.536s 9 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.127s (+427.2% 🔺) 2.004s (+99.5% 🔺) 0.009s (-25.6% 🟢) 2.016s (+97.9% 🔺) 0.889s 10 1.00x
🐘 Postgres Express 1.130s (+451.0% 🔺) 2.004s (+100.6% 🔺) 0.001s (-31.3% 🟢) 2.011s (+98.8% 🔺) 0.881s 10 1.00x
💻 Local Express 1.134s (+469.5% 🔺) 2.005s (+99.6% 🔺) 0.012s (+2.5%) 2.020s (+98.4% 🔺) 0.886s 10 1.01x
🐘 Postgres Nitro 1.143s (+457.7% 🔺) 1.999s (+100.0% 🔺) 0.001s (-6.7% 🟢) 2.011s (+98.8% 🔺) 0.867s 10 1.01x
💻 Local Next.js (Turbopack) 1.181s 2.003s 0.010s 2.017s 0.837s 10 1.05x
🐘 Postgres Next.js (Turbopack) 1.216s 2.001s 0.002s 2.012s 0.796s 10 1.08x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.270s (-40.8% 🟢) 3.095s (-41.3% 🟢) 1.979s (+166.6% 🔺) 5.508s (-15.0% 🟢) 3.237s 10 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.510s (+139.6% 🔺) 2.005s (+99.2% 🔺) 0.004s (+7.9% 🔺) 2.027s (+98.1% 🔺) 0.517s 30 1.00x
💻 Local Express 1.528s (+101.9% 🔺) 2.011s (+95.5% 🔺) 0.010s (+10.5% 🔺) 2.023s (+94.6% 🔺) 0.495s 30 1.01x
🐘 Postgres Nitro 1.540s (+146.7% 🔺) 2.004s (+99.0% 🔺) 0.004s (-6.5% 🟢) 2.026s (+98.2% 🔺) 0.487s 30 1.02x
💻 Local Next.js (Turbopack) 1.649s 2.009s 0.009s 2.021s 0.372s 30 1.09x
🐘 Postgres Next.js (Turbopack) 1.668s 2.009s 0.004s 2.024s 0.356s 30 1.11x
💻 Local Nitro 1.900s (+126.5% 🔺) 2.009s (+98.6% 🔺) 0.010s (+5.0% 🔺) 2.422s (+117.0% 🔺) 0.522s 25 1.26x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.693s (-80.7% 🟢) 6.781s (-78.0% 🟢) 0.210s (+87.7% 🔺) 7.399s (-76.7% 🟢) 1.706s 9 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.656s (-31.7% 🟢) 1.050s (-17.8% 🟢) 0.000s (-19.3% 🟢) 1.060s (-18.8% 🟢) 0.404s 57 1.00x
🐘 Postgres Nitro 0.673s (-30.5% 🟢) 1.050s (-15.8% 🟢) 0.000s (-57.9% 🟢) 1.060s (-15.7% 🟢) 0.387s 57 1.03x
🐘 Postgres Next.js (Turbopack) 0.762s 1.053s 0.000s 1.061s 0.299s 57 1.16x
💻 Local Nitro 1.284s (+5.0% 🔺) 2.013s (~) 0.000s (+100.0% 🔺) 2.015s (~) 0.731s 30 1.96x
💻 Local Express 1.345s (+9.8% 🔺) 2.014s (~) 0.000s (-30.0% 🟢) 2.016s (~) 0.671s 30 2.05x
💻 Local Next.js (Turbopack) 1.400s 2.013s 0.000s 2.016s 0.616s 30 2.13x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.259s (+39.6% 🔺) 5.238s (+19.2% 🔺) 0.003s (+3563.6% 🔺) 5.622s (+16.9% 🔺) 1.364s 11 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.368s (-23.7% 🟢) 2.065s (-3.6%) 0.000s (-100.0% 🟢) 2.085s (-4.1%) 0.717s 29 1.00x
🐘 Postgres Express 1.407s (-20.6% 🟢) 2.102s (-3.5%) 0.000s (NaN%) 2.115s (-3.8%) 0.707s 29 1.03x
🐘 Postgres Next.js (Turbopack) 1.560s 2.181s 0.000s 2.188s 0.628s 28 1.14x
💻 Local Next.js (Turbopack) 2.693s 3.186s 0.001s 3.191s 0.498s 19 1.97x
💻 Local Nitro 2.993s (-11.7% 🟢) 3.614s (-10.4% 🟢) 0.000s (-55.9% 🟢) 3.618s (-10.4% 🟢) 0.625s 17 2.19x
💻 Local Express 3.499s (+0.9%) 3.963s (-1.8%) 0.000s (-41.7% 🟢) 4.300s (+6.5% 🔺) 0.801s 15 2.56x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.731s (+40.0% 🔺) 7.073s (+31.6% 🔺) 0.000s (-54.2% 🟢) 7.580s (+30.8% 🔺) 1.849s 8 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 12/21
🐘 Postgres Express 18/21
▲ Vercel Nitro 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 19/21
Next.js (Turbopack) 🐘 Postgres 14/21
Nitro 🐘 Postgres 18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: 7f45c2c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
workflow Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment May 23, 2026 7:14am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 23, 2026 7:14am
example-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-astro-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-express-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-fastify-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-hono-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-nitro-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-nuxt-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workbench-vite-workflow Ready Ready Preview, Comment May 23, 2026 7:14am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 23, 2026 7:14am
workflow-swc-playground Ready Ready Preview, Comment May 23, 2026 7:14am
workflow-tarballs Ready Ready Preview, Comment May 23, 2026 7:14am
workflow-web Ready Ready Preview, Comment May 23, 2026 7:14am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 907 0 141 1048
✅ 💻 Local Development 1615 0 219 1834
✅ 📦 Local Production 1615 0 219 1834
✅ 🐘 Local Postgres 1615 0 219 1834
✅ 🪟 Windows 131 0 0 131
✅ 📋 Other 636 0 150 786
Total 6519 0 948 7467

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ example 105 0 26
✅ express 105 0 26
✅ nextjs-turbopack 129 0 2
✅ nextjs-webpack 129 0 2
✅ nitro 105 0 26
✅ nuxt 105 0 26
✅ sveltekit 124 0 7
✅ vite 105 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 131 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 106 0 25
✅ e2e-local-dev-tanstack-start- 106 0 25
✅ e2e-local-postgres-nest-stable 106 0 25
✅ e2e-local-postgres-tanstack-start- 106 0 25
✅ e2e-local-prod-nest-stable 106 0 25
✅ e2e-local-prod-tanstack-start- 106 0 25

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

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>
Comment thread packages/core/src/set-attributes-shared.ts Outdated
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant