Skip to content

[core] Allow settings attributes from inside step functions#2157

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/step-attributes
May 29, 2026
Merged

[core] Allow settings attributes from inside step functions#2157
VaguelySerious merged 1 commit into
mainfrom
peter/step-attributes

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented May 29, 2026

Expands on #2134 to allow running experimental_setAttributes() from inside a "use step" function.

Shares SDK-side attribute normalization/validation between workflow and step entrypoints. Add unit and e2e coverage for step-side writes, and updates docs.

This is forward-compatible with event-based attributes if we accept attributes set from a step-level potentially racing with other calls, and accept not supporting deterministic getAttribute calls from within a workflow context. This will need to be documented when we move to the new implementation.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 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 29, 2026 11:55am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 29, 2026 11:55am
example-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-astro-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-express-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-fastify-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-hono-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-nitro-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-nuxt-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workbench-vite-workflow Ready Ready Preview, Comment May 29, 2026 11:55am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 29, 2026 11:55am
workflow-swc-playground Ready Ready Preview, Comment May 29, 2026 11:55am
workflow-tarballs Ready Ready Preview, Comment May 29, 2026 11:55am
workflow-web Ready Ready Preview, Comment May 29, 2026 11:55am

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 79bbfce

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 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.041s (-7.0% 🟢) 1.007s (~) 0.966s 10 1.00x
💻 Local Nitro 0.042s (-2.6%) 1.006s (~) 0.964s 10 1.02x
🐘 Postgres Express 0.055s (-4.8%) 1.012s (~) 0.956s 10 1.34x
🐘 Postgres Nitro 0.059s (-38.4% 🟢) 1.012s (-3.0%) 0.953s 10 1.42x
🐘 Postgres Next.js (Turbopack) 0.061s 1.013s 0.952s 10 1.47x
💻 Local Next.js (Turbopack) 0.063s 1.006s 0.943s 10 1.53x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.347s (+47.6% 🔺) 2.211s (+3.5%) 1.863s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.544s (+116.3% 🔺) 2.346s (+0.6%) 1.802s 10 1.57x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.086s (-3.5%) 2.006s (~) 0.920s 10 1.00x
💻 Local Nitro 1.097s (-3.0%) 2.006s (~) 0.909s 10 1.01x
🐘 Postgres Express 1.107s (-3.4%) 2.009s (~) 0.902s 10 1.02x
💻 Local Next.js (Turbopack) 1.131s 2.007s 0.875s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.148s 2.008s 0.860s 10 1.06x
🐘 Postgres Nitro 1.236s (+8.4% 🔺) 2.141s (+6.5% 🔺) 0.904s 10 1.14x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.588s (-22.0% 🟢) 3.265s (-14.8% 🟢) 1.677s 10 1.00x
▲ Vercel Express 1.664s (-11.2% 🟢) 3.733s (-2.0%) 2.068s 10 1.05x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 10.503s (-3.8%) 11.022s (~) 0.519s 3 1.00x
💻 Local Nitro 10.526s (-3.8%) 11.023s (~) 0.497s 3 1.00x
🐘 Postgres Nitro 10.529s (-3.1%) 11.022s (~) 0.493s 3 1.00x
🐘 Postgres Express 10.545s (-3.8%) 11.018s (~) 0.473s 3 1.00x
💻 Local Next.js (Turbopack) 10.785s 11.022s 0.237s 3 1.03x
🐘 Postgres Next.js (Turbopack) 11.049s 11.377s 0.328s 3 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.588s (-20.0% 🟢) 15.386s (-23.1% 🟢) 1.798s 2 1.00x
▲ Vercel Next.js (Turbopack) 13.804s (-20.3% 🟢) 15.183s (-21.7% 🟢) 1.379s 2 1.02x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.707s (-9.0% 🟢) 14.027s (-12.5% 🟢) 0.320s 5 1.00x
💻 Local Express 13.742s (-8.2% 🟢) 14.027s (-6.7% 🟢) 0.284s 5 1.00x
🐘 Postgres Express 13.838s (-5.1% 🟢) 14.017s (-6.7% 🟢) 0.179s 5 1.01x
🐘 Postgres Nitro 13.968s (-4.3%) 14.220s (-5.4% 🟢) 0.253s 5 1.02x
💻 Local Next.js (Turbopack) 14.294s 15.029s 0.735s 4 1.04x
🐘 Postgres Next.js (Turbopack) 14.648s 15.039s 0.391s 4 1.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 21.658s (-57.0% 🟢) 24.002s (-54.3% 🟢) 2.344s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.778s (-56.7% 🟢) 24.044s (-56.0% 🟢) 1.266s 3 1.05x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 12.381s (-11.4% 🟢) 13.017s (-9.0% 🟢) 0.636s 7 1.00x
💻 Local Express 12.430s (-25.1% 🟢) 13.025s (-23.5% 🟢) 0.595s 7 1.00x
💻 Local Nitro 12.436s (-25.9% 🟢) 13.023s (-23.5% 🟢) 0.587s 7 1.00x
🐘 Postgres Express 12.735s (-9.1% 🟢) 13.161s (-9.8% 🟢) 0.426s 7 1.03x
💻 Local Next.js (Turbopack) 13.552s 14.028s 0.475s 7 1.09x
🐘 Postgres Next.js (Turbopack) 15.224s 15.527s 0.303s 6 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 32.528s (-73.2% 🟢) 34.863s (-71.8% 🟢) 2.335s 3 1.00x
▲ Vercel Next.js (Turbopack) 33.524s (-91.5% 🟢) 35.875s (-90.9% 🟢) 2.351s 3 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.177s (-6.6% 🟢) 2.008s (~) 0.831s 15 1.00x
🐘 Postgres Nitro 1.183s (-7.2% 🟢) 2.006s (~) 0.824s 15 1.00x
💻 Local Nitro 1.216s (-25.5% 🟢) 2.006s (-3.3%) 0.790s 15 1.03x
💻 Local Express 1.231s (-17.3% 🟢) 2.006s (~) 0.775s 15 1.05x
🐘 Postgres Next.js (Turbopack) 1.279s 2.014s 0.735s 15 1.09x
💻 Local Next.js (Turbopack) 1.338s 2.006s 0.668s 15 1.14x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.781s (-18.1% 🟢) 4.314s (-12.5% 🟢) 1.533s 7 1.00x
▲ Vercel Express 3.517s (+23.0% 🔺) 5.310s (+14.8% 🔺) 1.792s 7 1.26x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.233s (-47.8% 🟢) 2.006s (-33.3% 🟢) 0.773s 15 1.00x
🐘 Postgres Nitro 1.258s (-46.5% 🟢) 2.008s (-33.3% 🟢) 0.751s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.460s 2.178s 0.718s 14 1.18x
💻 Local Express 1.715s (-41.9% 🟢) 2.006s (-41.9% 🟢) 0.291s 15 1.39x
💻 Local Nitro 1.759s (-44.0% 🟢) 2.006s (-48.4% 🟢) 0.248s 15 1.43x
💻 Local Next.js (Turbopack) 1.897s 2.222s 0.324s 14 1.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.937s (-44.6% 🟢) 5.503s (-38.2% 🟢) 1.567s 6 1.00x
▲ Vercel Express 4.903s (+35.5% 🔺) 6.440s (+26.0% 🔺) 1.538s 5 1.25x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.343s (-61.5% 🟢) 2.007s (-50.0% 🟢) 0.664s 15 1.00x
🐘 Postgres Nitro 1.412s (-59.4% 🟢) 2.009s (-49.9% 🟢) 0.597s 15 1.05x
🐘 Postgres Next.js (Turbopack) 1.847s 2.475s 0.627s 13 1.38x
💻 Local Nitro 4.680s (-43.9% 🟢) 5.178s (-42.6% 🟢) 0.498s 6 3.48x
💻 Local Next.js (Turbopack) 4.971s 5.344s 0.373s 6 3.70x
💻 Local Express 5.031s (-39.7% 🟢) 5.681s (-37.1% 🟢) 0.649s 6 3.75x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.918s (+39.6% 🔺) 7.648s (+24.8% 🔺) 1.730s 4 1.00x
▲ Vercel Next.js (Turbopack) 9.703s (+8.8% 🔺) 11.652s (+6.3% 🔺) 1.948s 3 1.64x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.180s (-6.1% 🟢) 2.007s (~) 0.827s 15 1.00x
🐘 Postgres Nitro 1.184s (-5.8% 🟢) 2.008s (~) 0.823s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.379s 2.031s 0.652s 15 1.17x
💻 Local Next.js (Turbopack) 1.423s 2.073s 0.650s 15 1.21x
💻 Local Nitro 1.554s (-16.7% 🟢) 2.006s (-14.3% 🟢) 0.453s 15 1.32x
💻 Local Express 1.569s (-17.2% 🟢) 2.073s (-12.3% 🟢) 0.504s 15 1.33x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.512s (-2.7%) 4.051s (-6.9% 🟢) 1.539s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.736s (-6.7% 🟢) 4.245s (-8.6% 🟢) 1.509s 8 1.09x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.228s (-47.6% 🟢) 2.010s (-33.3% 🟢) 0.782s 15 1.00x
🐘 Postgres Nitro 1.267s (-45.8% 🟢) 2.009s (-33.3% 🟢) 0.741s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.610s 2.110s 0.501s 15 1.31x
💻 Local Express 1.896s (-39.5% 🟢) 2.294s (-39.0% 🟢) 0.398s 14 1.54x
💻 Local Next.js (Turbopack) 2.024s 2.675s 0.651s 12 1.65x
💻 Local Nitro 2.062s (-32.7% 🟢) 2.508s (-35.5% 🟢) 0.446s 12 1.68x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.804s (+19.2% 🔺) 5.424s (+13.2% 🔺) 1.619s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.447s (+41.5% 🔺) 5.763s (+27.4% 🔺) 1.315s 6 1.17x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.334s (-61.9% 🟢) 2.007s (-49.9% 🟢) 0.674s 15 1.00x
🐘 Postgres Nitro 1.383s (-60.3% 🟢) 2.010s (-49.9% 🟢) 0.627s 15 1.04x
🐘 Postgres Next.js (Turbopack) 1.845s 2.517s 0.672s 12 1.38x
💻 Local Nitro 5.586s (-38.9% 🟢) 6.213s (-38.0% 🟢) 0.627s 5 4.19x
💻 Local Next.js (Turbopack) 5.624s 6.215s 0.591s 5 4.22x
💻 Local Express 5.679s (-35.5% 🟢) 6.216s (-33.0% 🟢) 0.538s 5 4.26x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.652s (-16.3% 🟢) 7.583s (-11.3% 🟢) 1.930s 5 1.00x
▲ Vercel Express 5.927s (-7.6% 🟢) 7.693s (-5.9% 🟢) 1.766s 4 1.05x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.576s (-31.4% 🟢) 1.023s (~) 0.448s 59 1.00x
🐘 Postgres Nitro 0.578s (-29.5% 🟢) 1.023s (+1.7%) 0.445s 59 1.00x
💻 Local Express 0.585s (-40.6% 🟢) 1.005s (-6.6% 🟢) 0.420s 60 1.02x
💻 Local Nitro 0.590s (-39.9% 🟢) 1.005s (-8.2% 🟢) 0.415s 60 1.02x
🐘 Postgres Next.js (Turbopack) 0.834s 1.145s 0.311s 53 1.45x
💻 Local Next.js (Turbopack) 0.861s 1.039s 0.178s 58 1.50x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.613s (-61.3% 🟢) 6.842s (-57.5% 🟢) 1.229s 9 1.00x
▲ Vercel Express 5.725s (-69.9% 🟢) 7.533s (-64.7% 🟢) 1.808s 9 1.02x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.341s (-30.4% 🟢) 2.030s (-3.3%) 0.689s 45 1.00x
🐘 Postgres Express 1.347s (-31.8% 🟢) 2.007s (-11.1% 🟢) 0.660s 45 1.00x
💻 Local Express 1.470s (-51.3% 🟢) 2.006s (-44.1% 🟢) 0.536s 45 1.10x
💻 Local Nitro 1.502s (-50.5% 🟢) 2.028s (-46.0% 🟢) 0.527s 45 1.12x
💻 Local Next.js (Turbopack) 2.051s 2.911s 0.859s 31 1.53x
🐘 Postgres Next.js (Turbopack) 2.699s 3.174s 0.475s 29 2.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 14.798s (-57.1% 🟢) 17.019s (-53.8% 🟢) 2.221s 6 1.00x
▲ Vercel Next.js (Turbopack) 15.167s (-69.5% 🟢) 17.136s (-66.9% 🟢) 1.969s 6 1.02x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.653s (-35.4% 🟢) 3.033s (-34.1% 🟢) 0.381s 40 1.00x
🐘 Postgres Express 2.718s (-31.9% 🟢) 3.111s (-28.8% 🟢) 0.393s 39 1.02x
💻 Local Nitro 3.247s (-65.1% 🟢) 4.009s (-60.0% 🟢) 0.762s 30 1.22x
💻 Local Express 3.379s (-63.3% 🟢) 4.147s (-58.6% 🟢) 0.768s 29 1.27x
💻 Local Next.js (Turbopack) 4.242s 5.009s 0.767s 24 1.60x
🐘 Postgres Next.js (Turbopack) 4.269s 4.866s 0.597s 25 1.61x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 30.021s (-76.9% 🟢) 32.852s (-75.1% 🟢) 2.831s 4 1.00x
▲ Vercel Next.js (Turbopack) 30.866s (-71.2% 🟢) 32.904s (-69.8% 🟢) 2.038s 4 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.207s (-27.0% 🟢) 1.006s (~) 0.799s 60 1.00x
🐘 Postgres Express 0.228s (-19.4% 🟢) 1.006s (~) 0.778s 60 1.10x
🐘 Postgres Next.js (Turbopack) 0.423s 1.043s 0.621s 58 2.05x
💻 Local Nitro 0.429s (-29.0% 🟢) 1.004s (-1.7%) 0.575s 60 2.08x
💻 Local Express 0.548s (-2.3%) 1.115s (+11.1% 🔺) 0.568s 54 2.65x
💻 Local Next.js (Turbopack) 0.562s 1.022s 0.460s 59 2.72x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.382s (+21.9% 🔺) 4.078s (+12.1% 🔺) 1.696s 15 1.00x
▲ Vercel Next.js (Turbopack) 46.327s (+2190.6% 🔺) 47.594s (+1154.6% 🔺) 1.267s 7 19.45x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.326s (-36.1% 🟢) 1.006s (~) 0.680s 90 1.00x
🐘 Postgres Nitro 0.346s (-30.3% 🟢) 1.007s (~) 0.661s 90 1.06x
🐘 Postgres Next.js (Turbopack) 0.477s 1.022s 0.545s 89 1.46x
💻 Local Nitro 2.109s (-16.9% 🟢) 2.637s (-12.4% 🟢) 0.527s 35 6.48x
💻 Local Express 2.213s (-11.9% 🟢) 2.715s (-9.8% 🟢) 0.502s 34 6.80x
💻 Local Next.js (Turbopack) 2.281s 3.010s 0.729s 30 7.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.971s (+63.2% 🔺) 6.820s (+41.9% 🔺) 1.849s 14 1.00x
▲ Vercel Next.js (Turbopack) 6.013s (+70.1% 🔺) 7.503s (+44.5% 🔺) 1.491s 13 1.21x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.611s (-25.3% 🟢) 1.006s (-1.1%) 0.394s 120 1.00x
🐘 Postgres Nitro 0.653s (-17.4% 🟢) 1.006s (~) 0.353s 120 1.07x
🐘 Postgres Next.js (Turbopack) 0.847s 1.238s 0.391s 97 1.39x
💻 Local Nitro 9.442s (-15.6% 🟢) 10.111s (-13.3% 🟢) 0.669s 12 15.44x
💻 Local Express 9.561s (-14.6% 🟢) 10.027s (-16.0% 🟢) 0.466s 12 15.64x
💻 Local Next.js (Turbopack) 10.096s 10.861s 0.765s 12 16.51x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 15.112s (+103.7% 🔺) 17.658s (+91.0% 🔺) 2.546s 7 1.00x
▲ Vercel Next.js (Turbopack) 19.091s (+84.8% 🔺) 20.788s (+69.2% 🔺) 1.697s 7 1.26x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.157s (+481.1% 🔺) 2.005s (+99.6% 🔺) 0.011s (-11.6% 🟢) 2.018s (+98.2% 🔺) 0.861s 10 1.00x
💻 Local Nitro 1.161s (+443.2% 🔺) 2.005s (+99.6% 🔺) 0.011s (-8.0% 🟢) 2.019s (+98.1% 🔺) 0.858s 10 1.00x
🐘 Postgres Nitro 1.162s (+466.8% 🔺) 2.001s (+100.2% 🔺) 0.001s (-20.0% 🟢) 2.010s (+98.8% 🔺) 0.848s 10 1.00x
🐘 Postgres Express 1.162s (+466.7% 🔺) 2.001s (+100.4% 🔺) 0.001s (-25.0% 🟢) 2.010s (+98.7% 🔺) 0.848s 10 1.00x
💻 Local Next.js (Turbopack) 1.204s 2.004s 0.012s 2.020s 0.815s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.246s 2.001s 0.001s 2.013s 0.768s 10 1.08x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.238s (-67.3% 🟢) 2.943s (-66.0% 🟢) 3.523s (+457.5% 🔺) 6.871s (-29.8% 🟢) 4.633s 10 1.00x
▲ Vercel Express 2.298s (-8.3% 🟢) 3.341s (-18.3% 🟢) 2.155s (+124.3% 🔺) 6.004s (+7.4% 🔺) 3.706s 10 1.03x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.565s (+106.7% 🔺) 2.009s (+95.3% 🔺) 0.011s (+16.5% 🔺) 2.022s (+94.4% 🔺) 0.457s 30 1.00x
🐘 Postgres Nitro 1.578s (+152.8% 🔺) 2.004s (+99.1% 🔺) 0.004s (-3.3%) 2.025s (+98.1% 🔺) 0.447s 30 1.01x
💻 Local Nitro 1.592s (+89.8% 🔺) 2.009s (+98.6% 🔺) 0.010s (+8.9% 🔺) 2.021s (+81.1% 🔺) 0.430s 30 1.02x
🐘 Postgres Express 1.617s (+156.6% 🔺) 2.006s (+99.3% 🔺) 0.003s (-11.2% 🟢) 2.024s (+97.8% 🔺) 0.407s 30 1.03x
💻 Local Next.js (Turbopack) 1.716s 2.008s 0.011s 2.021s 0.305s 30 1.10x
🐘 Postgres Next.js (Turbopack) 1.959s 2.406s 0.002s 2.424s 0.464s 25 1.25x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.968s (-64.7% 🟢) 7.233s (-60.3% 🟢) 0.279s (+31.9% 🔺) 7.984s (-57.8% 🟢) 2.017s 8 1.00x
▲ Vercel Express 6.090s (-6.4% 🟢) 7.303s (-8.8% 🟢) 0.484s (+18.5% 🔺) 8.346s (-5.5% 🟢) 2.256s 8 1.02x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.675s (-29.8% 🟢) 1.016s (-20.5% 🟢) 0.000s (-22.0% 🟢) 1.027s (-21.3% 🟢) 0.352s 59 1.00x
🐘 Postgres Nitro 0.720s (-25.7% 🟢) 1.033s (-17.2% 🟢) 0.000s (-100.0% 🟢) 1.048s (-16.7% 🟢) 0.328s 58 1.07x
🐘 Postgres Next.js (Turbopack) 0.995s 1.392s 0.000s 1.428s 0.433s 43 1.47x
💻 Local Nitro 1.392s (+13.8% 🔺) 2.013s (~) 0.001s (+533.3% 🔺) 2.016s (~) 0.624s 30 2.06x
💻 Local Express 1.410s (+15.1% 🔺) 2.014s (~) 0.000s (-70.0% 🟢) 2.016s (~) 0.606s 30 2.09x
💻 Local Next.js (Turbopack) 1.472s 2.013s 0.000s 2.015s 0.543s 30 2.18x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.824s (-62.4% 🟢) 5.060s (-56.1% 🟢) 0.002s (+Infinity% 🔺) 5.461s (-54.7% 🟢) 1.637s 11 1.00x
▲ Vercel Express 3.832s (+2.5%) 5.091s (~) 0.000s (-100.0% 🟢) 5.621s (+1.6%) 1.789s 11 1.00x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

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.392s (-22.3% 🟢) 2.097s (-2.1%) 0.000s (+93.1% 🔺) 2.115s (-2.7%) 0.723s 29 1.00x
🐘 Postgres Express 1.431s (-19.2% 🟢) 2.069s (-5.0%) 0.000s (+Infinity% 🔺) 2.085s (-5.2% 🟢) 0.653s 29 1.03x
🐘 Postgres Next.js (Turbopack) 2.206s 2.685s 0.000s 2.730s 0.524s 22 1.58x
💻 Local Next.js (Turbopack) 2.764s 3.357s 0.001s 3.362s 0.598s 18 1.99x
💻 Local Nitro 2.994s (-11.6% 🟢) 3.675s (-8.9% 🟢) 0.001s (+65.4% 🔺) 3.678s (-8.9% 🟢) 0.683s 17 2.15x
💻 Local Express 3.376s (-2.6%) 3.827s (-5.1% 🟢) 0.001s (+41.7% 🔺) 4.164s (+3.2%) 0.789s 15 2.43x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.848s (+4.1%) 7.019s (+0.5%) 0.000s (-100.0% 🟢) 7.397s (-1.9%) 1.550s 9 1.00x
▲ Vercel Express 5.974s (+30.2% 🔺) 7.405s (+23.0% 🔺) 0.000s (NaN%) 7.926s (+22.8% 🔺) 1.953s 8 1.02x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 14/21
Next.js (Turbopack) 🐘 Postgres 13/21
Nitro 🐘 Postgres 16/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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1266 0 219 1485
✅ 💻 Local Development 1671 0 219 1890
✅ 📦 Local Production 1671 0 219 1890
✅ 🐘 Local Postgres 1671 0 219 1890
✅ 🪟 Windows 135 0 0 135
✅ 📋 Other 769 0 176 945
Total 7183 0 1052 8235

Details by Category

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

📋 View full workflow run

@VaguelySerious VaguelySerious marked this pull request as ready for review May 29, 2026 12:24
@VaguelySerious VaguelySerious requested a review from a team as a code owner May 29, 2026 12:24
## Usage

Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function. Calling it from a `"use step"` function or plain application code is not supported.
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function. Plain application code is not supported because there is no active workflow run to attach attributes to.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function. Plain application code is not supported because there is no active workflow run to attach attributes to.
Call [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) from a `"use workflow"` function or a `"use step"` function.

- Setting attributes is currently slower than the final API will be, because each write goes through an internal workflow step. Prefer batching related attributes in one call.
- Workflow-body storage errors are logged after retries, but do not fail the workflow run.
- Step-body storage errors throw from `experimental_setAttributes` like any other step-side network write. Catch the error inside the step if the attribute is best-effort.
- Setting attributes from a workflow body is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Setting attributes from a workflow body is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.
- Setting attributes from within a workflow function (as opposed to a step function) is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.

@VaguelySerious VaguelySerious changed the title Allow experimental attributes inside steps [core] Allow settings attributes from inside step functions May 29, 2026
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve — clean follow-up to the attributes MVP, picks up the scope cut from #2088

This is the step-body support that #2088 deliberately left out, plus a nice refactor that pulls validation into a shared helper. The implementation is straightforward:

  • Workflow-body path unchanged: still dispatches through __builtin_set_attributes step bridge for event-log materialization.
  • Step-body path is new: the host-side set-attributes.ts (which used to throw FatalError) now reads workflowRunId from contextStorage, validates, and posts directly to world.runs.experimentalSetAttributes(). No nested step needed — step bodies already run in host context.
  • Plain host code path: still throws FatalError because there's no active workflow run.

The attribute-changes.ts shared helper extraction is exactly the right move — the workflow-side and host-side both need the same input normalization + validation + AttributeValidationError → FatalError conversion, and DRY'ing them up prevents future drift.

What I verified locally

  • pnpm install --frozen-lockfile
  • pnpm turbo run build --filter @workflow/core
  • pnpm --filter @workflow/core test ✓ (1051/1051)
  • Specifically ran set-attributes.test.ts — 5/5 pass, covering: plain host code throws FatalError, step context posts directly, allowReservedAttributes forwarded, validation rejects before world call, warn-once for unsupported worlds.
  • Confirmed WorldCacheKey is the same symbol (@workflow/world//cache) used by both getWorldLazy() and the existing __builtin_set_attributes builtin — no symbol-mismatch concern.
  • Rebased onto current main cleanly. The "deletions" of trace-viewer files in the GitHub diff are stale-branch artifacts from being forked before #2144 landed; squash merge produces a clean commit.

E2E test design is sharp

The test specifically asserts that NO __builtin_set_attributes step events are created when calling from a step body:

expect(
  events.some(
    (e) => (e.eventType === 'step_created' || e.eventType === 'step_completed') &&
      stepName.includes('__builtin_set_attributes')
  )
).toBe(false);

That's the right invariant — it documents that the step-body path is genuinely a direct write, not a nested step dispatch.

CI

109 success, 1 failure (Benchmark Vercel (nitro-v3) — recurring infra flake, fails regularly on main too).

One inline comment

The PR body acknowledges a forward-compat asymmetry that the docs don't yet capture — see inline. Step-body writes will be inherently non-deterministic from the workflow's perspective when V1's getAttribute() lands, while workflow-body writes will be cleanly event-sourceable. Worth documenting up-front so users don't write step-body setAttributes calls expecting deterministic reads later.

Non-blocker — the implementation is correct and the asymmetry isn't introduced by this PR (it's inherent to where step bodies execute). Just want the docs to match reality before users adopt the pattern.

Notable trade-off worth flagging (also non-blocker)

FatalError on validation still applies to both workflow-body and step-body calls (because both go through the shared normalizeAttributeChanges helper). Karthik flagged on PR #2088 that this is potentially too heavy-handed for telemetry — "I moved a tag call into a step → my prod run dies". The host-side wrong-call-site path now has a defensible answer (no run exists, so the call is meaningless), but the validation-error case still kills the run for what is fundamentally observability metadata. Not made worse by this PR. Worth a follow-up discussion eventually.

Approving.

- World implementations emit a side-channel observability record per successful write (in `world-vercel`, this hooks into the same observability/analytics pipeline already used for other run lifecycle events)

Calling `experimental_setAttributes` from a step body or plain host code is intentionally not supported in the MVP — the host-side export throws `FatalError` directing callers back to a workflow body. This keeps the implementation a single dispatch path; step-body support can be added later without breaking the workflow-body contract.
Calling `experimental_setAttributes` from a step body was intentionally not supported in the MVP, but step-body calls are now supported as a follow-up. Plain host code remains unsupported because there is no active workflow run to attach attributes to.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forward-compat caveat in the PR body should make it into the docs.

The PR description says:

This is forward-compatible with event-based attributes if we accept attributes set from a step-level potentially racing with other calls, and accept not supporting deterministic getAttribute calls from within a workflow context. This will need to be documented when we move to the new implementation.

That second part is the bigger semantic asymmetry — and it's the thing a user is most likely to be surprised by when V1 lands with getAttribute():

  • Workflow-body writes go through __builtin_set_attributes (step boundary), and when V1 lands they convert cleanly to attr_set events that the VM replays deterministically. getAttribute() from inside the workflow body will see them.
  • Step-body writes post directly to the world from host context, outside the VM. When V1 lands, these can't be event-sourced (there's no spot in the workflow's event log where they were issued — they're step-side side effects). So getAttribute() from inside the workflow body won't see them deterministically; they're a race with whatever else writes to the same key.

For the MVP this asymmetry is invisible because there's no getAttribute() yet. But the contract is being set now and will be load-bearing when V1 ships. Worth documenting up-front so users don't write step-body setAttributes calls and then expect deterministic reads from the workflow body later.

Suggested addition under the existing "When the full feature ships" paragraph or a new note in attributes-mvp.mdx:

Step-body vs workflow-body forward-compat asymmetry. Workflow-body writes go through a step boundary and will be event-sourced as attr_set events when V1 lands, so future getAttribute() calls from the same workflow body will see them deterministically. Step-body writes post directly to the World outside the VM and are inherently non-deterministic from the workflow's perspective — they won't appear in event replay. Use step-body writes for observability metadata that you don't intend to read back from inside the run; use workflow-body writes for anything you might want to read deterministically later.

Non-blocker (the implementation is correct and the asymmetry is real, not invented by this PR), but worth landing the docs change before users start adopting step-body writes for the wrong reasons.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We likely won't ship getAttribute, so this is nil

@VaguelySerious VaguelySerious merged commit 409b103 into main May 29, 2026
183 of 192 checks passed
@VaguelySerious VaguelySerious deleted the peter/step-attributes branch May 29, 2026 17:38
@github-actions
Copy link
Copy Markdown
Contributor

No backport to stable for 409b103 (AI decision).

This commit extends the experimental_setAttributes API (originally added in #2134) to support step-body calls. The underlying attributes feature does not exist on stablepackages/core/src/set-attributes.ts, packages/core/src/workflow/set-attributes.ts, and the attribute docs under docs/content/docs/v5/ are all absent on stable. Backporting would require the entire attributes MVP first, so this change is inherently main-only.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

409b1033d9b7dfab9c26fda9a17494c08e43d0ae

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.

2 participants