Skip to content

[core] Combine flow+step bundle and process steps eagerly#1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 11, 2026

🦋 Changeset detected

Latest commit: 234fce4

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

This PR includes changesets to release 22 packages
Name Type
@workflow/world-postgres Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nest Patch
@workflow/sveltekit Patch
@workflow/nitro Patch
@workflow/astro Patch
@workflow/world Patch
workflow Patch
tarballs Patch
@workflow/cli Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/world-testing Patch
@workflow/rollup Patch
@workflow/vite Patch
@workflow/nuxt Patch
@workflow/world-local Patch
@workflow/world-vercel Patch
@workflow/ai 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 Mar 11, 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 4, 2026 6:29am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 4, 2026 6:29am
example-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-astro-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-express-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-fastify-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-hono-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-nitro-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-nuxt-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workbench-vite-workflow Ready Ready Preview, Comment May 4, 2026 6:29am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 4, 2026 6:29am
workflow-nest Ready Ready Preview, Comment May 4, 2026 6:29am
workflow-swc-playground Ready Ready Preview, Comment May 4, 2026 6:29am
workflow-tarballs Ready Ready Preview, Comment May 4, 2026 6:29am
workflow-web Ready Ready Preview, Comment May 4, 2026 6:29am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 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 🥇 Nitro 0.033s (-24.6% 🟢) 1.005s (~) 0.972s 10 1.00x
💻 Local Express 0.034s (-23.3% 🟢) 1.006s (~) 0.972s 10 1.05x
🐘 Postgres Next.js (Turbopack) 0.049s 1.013s 0.964s 10 1.50x
💻 Local Next.js (Turbopack) 0.049s 1.005s 0.956s 10 1.51x
🐘 Postgres Nitro 0.051s (-46.8% 🟢) 1.012s (-3.0%) 0.961s 10 1.56x
🌐 Redis Next.js (Turbopack) 0.055s 1.005s 0.951s 10 1.68x
🌐 MongoDB Next.js (Turbopack) 0.109s 1.007s 0.899s 10 3.35x
🐘 Postgres Express 0.177s (+205.2% 🔺) 1.107s (+9.5% 🔺) 0.930s 10 5.45x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.236s (-42.4% 🟢) 1.939s (-22.7% 🟢) 1.703s 10 1.00x
▲ Vercel Express 0.241s (+2.2%) 2.009s (-5.9% 🟢) 1.768s 10 1.02x
▲ Vercel Next.js (Turbopack) 0.550s (+118.6% 🔺) 2.715s (+16.4% 🔺) 2.165s 10 2.33x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.069s (-5.0% 🟢) 2.006s (~) 0.937s 10 1.00x
💻 Local Nitro 1.073s (-5.1% 🟢) 2.006s (~) 0.933s 10 1.00x
🐘 Postgres Nitro 1.084s (-4.9%) 2.010s (~) 0.927s 10 1.01x
🌐 Redis Next.js (Turbopack) 1.113s 2.006s 0.893s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.114s 2.007s 0.893s 10 1.04x
💻 Local Next.js (Turbopack) 1.121s 2.007s 0.886s 10 1.05x
🐘 Postgres Express 1.143s (~) 2.021s (+0.5%) 0.878s 10 1.07x
🌐 MongoDB Next.js (Turbopack) 1.162s 2.009s 0.847s 10 1.09x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.498s (-20.1% 🟢) 3.691s (-3.1%) 2.193s 10 1.00x
▲ Vercel Nitro 1.524s (-60.9% 🟢) 3.710s (-37.2% 🟢) 2.186s 10 1.02x
▲ Vercel Next.js (Turbopack) 1.942s (-4.6%) 4.457s (+16.4% 🔺) 2.516s 10 1.30x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.409s (-4.2%) 11.018s (~) 0.609s 3 1.00x
💻 Local Nitro 10.413s (-4.9%) 11.022s (~) 0.608s 3 1.00x
💻 Local Express 10.436s (-4.5%) 11.021s (~) 0.586s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.568s 11.013s 0.445s 3 1.02x
🌐 Redis Next.js (Turbopack) 10.657s 11.024s 0.367s 3 1.02x
💻 Local Next.js (Turbopack) 10.685s 11.022s 0.337s 3 1.03x
🐘 Postgres Express 10.745s (-2.0%) 11.419s (+3.6%) 0.674s 3 1.03x
🌐 MongoDB Next.js (Turbopack) 10.789s 11.018s 0.229s 3 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 14.059s (-40.8% 🟢) 15.700s (-37.5% 🟢) 1.641s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.845s (-14.3% 🟢) 16.872s (-13.0% 🟢) 2.027s 2 1.06x
▲ Vercel Express 15.996s (-5.8% 🟢) 18.196s (-9.1% 🟢) 2.200s 2 1.14x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.431s (-10.8% 🟢) 14.027s (-12.5% 🟢) 0.596s 5 1.00x
🐘 Postgres Nitro 13.434s (-8.0% 🟢) 14.013s (-6.8% 🟢) 0.579s 5 1.00x
💻 Local Express 13.490s (-9.9% 🟢) 14.027s (-6.7% 🟢) 0.537s 5 1.00x
🐘 Postgres Next.js (Turbopack) 13.820s 14.018s 0.198s 5 1.03x
💻 Local Next.js (Turbopack) 14.076s 15.030s 0.954s 4 1.05x
🌐 Redis Next.js (Turbopack) 14.088s 15.030s 0.942s 4 1.05x
🌐 MongoDB Next.js (Turbopack) 14.283s 15.019s 0.736s 4 1.06x
🐘 Postgres Express 14.537s (~) 15.276s (+1.7%) 0.739s 4 1.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 20.821s (-58.6% 🟢) 22.773s (-56.7% 🟢) 1.952s 3 1.00x
▲ Vercel Nitro 21.829s (-66.1% 🟢) 23.502s (-64.7% 🟢) 1.673s 3 1.05x
▲ Vercel Next.js (Turbopack) 23.199s (-55.9% 🟢) 25.174s (-53.9% 🟢) 1.975s 3 1.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.846s (-29.4% 🟢) 12.022s (-29.4% 🟢) 0.176s 8 1.00x
🐘 Postgres Nitro 11.923s (-14.6% 🟢) 12.392s (-13.4% 🟢) 0.469s 8 1.01x
💻 Local Express 11.997s (-27.7% 🟢) 12.398s (-27.2% 🟢) 0.401s 8 1.01x
🌐 Redis Next.js (Turbopack) 13.015s 13.454s 0.439s 7 1.10x
💻 Local Next.js (Turbopack) 13.089s 14.026s 0.937s 7 1.10x
🐘 Postgres Next.js (Turbopack) 13.215s 13.585s 0.370s 7 1.12x
🌐 MongoDB Next.js (Turbopack) 13.301s 14.021s 0.720s 7 1.12x
🐘 Postgres Express 15.027s (+7.3% 🔺) 15.400s (+5.5% 🔺) 0.373s 6 1.27x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 31.095s (-74.3% 🟢) 33.159s (-73.2% 🟢) 2.064s 3 1.00x
▲ Vercel Nitro 31.508s (-92.5% 🟢) 33.124s (-92.2% 🟢) 1.615s 3 1.01x
▲ Vercel Next.js (Turbopack) 31.987s (-91.9% 🟢) 33.996s (-91.4% 🟢) 2.009s 3 1.03x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.140s (-10.6% 🟢) 2.007s (~) 0.868s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.168s 2.007s 0.838s 15 1.03x
💻 Local Nitro 1.172s (-28.2% 🟢) 2.006s (-3.3%) 0.834s 15 1.03x
💻 Local Express 1.187s (-20.3% 🟢) 2.006s (~) 0.820s 15 1.04x
🐘 Postgres Express 1.237s (-1.9%) 2.018s (~) 0.781s 15 1.09x
🌐 Redis Next.js (Turbopack) 1.238s 2.006s 0.768s 15 1.09x
💻 Local Next.js (Turbopack) 1.279s 2.005s 0.726s 15 1.12x
🌐 MongoDB Next.js (Turbopack) 2.048s 2.917s 0.869s 11 1.80x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.551s (-10.8% 🟢) 4.187s (-9.4% 🟢) 1.636s 8 1.00x
▲ Vercel Nitro 2.674s (-5.1% 🟢) 4.258s (-1.5%) 1.584s 8 1.05x
▲ Vercel Next.js (Turbopack) 4.193s (+23.4% 🔺) 6.817s (+38.2% 🔺) 2.624s 5 1.64x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.225s (-47.9% 🟢) 2.007s (-33.3% 🟢) 0.782s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.282s 2.006s 0.724s 15 1.05x
🐘 Postgres Express 1.483s (-37.2% 🟢) 2.019s (-32.9% 🟢) 0.536s 15 1.21x
💻 Local Nitro 1.726s (-45.1% 🟢) 2.072s (-46.7% 🟢) 0.346s 15 1.41x
💻 Local Next.js (Turbopack) 1.831s 2.149s 0.318s 14 1.50x
💻 Local Express 1.905s (-35.5% 🟢) 2.150s (-37.8% 🟢) 0.244s 14 1.56x
🌐 Redis Next.js (Turbopack) 2.364s 3.008s 0.643s 10 1.93x
🌐 MongoDB Next.js (Turbopack) 3.568s 4.008s 0.440s 8 2.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.844s (-5.1% 🟢) 5.284s (-10.8% 🟢) 1.440s 6 1.00x
▲ Vercel Express 3.884s (+7.3% 🔺) 6.116s (+19.7% 🔺) 2.232s 5 1.01x
▲ Vercel Next.js (Turbopack) 5.098s (-28.2% 🟢) 7.574s (-15.0% 🟢) 2.476s 4 1.33x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.419s (-59.2% 🟢) 2.007s (-49.9% 🟢) 0.588s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.493s 2.007s 0.514s 15 1.05x
🐘 Postgres Express 2.153s (-38.2% 🟢) 2.861s (-28.7% 🟢) 0.708s 11 1.52x
🌐 Redis Next.js (Turbopack) 3.634s 4.010s 0.376s 8 2.56x
💻 Local Nitro 4.547s (-45.5% 🟢) 5.013s (-44.4% 🟢) 0.466s 6 3.20x
💻 Local Express 4.829s (-42.1% 🟢) 5.512s (-38.9% 🟢) 0.683s 6 3.40x
💻 Local Next.js (Turbopack) 5.581s 6.213s 0.631s 5 3.93x
🌐 MongoDB Next.js (Turbopack) 6.289s 7.012s 0.723s 5 4.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.147s (+21.4% 🔺) 7.340s (+19.8% 🔺) 2.193s 5 1.00x
▲ Vercel Nitro 5.410s (+53.5% 🔺) 7.160s (+29.4% 🔺) 1.750s 5 1.05x
▲ Vercel Next.js (Turbopack) 7.157s (-19.7% 🟢) 8.877s (-19.0% 🟢) 1.721s 4 1.39x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.170s (-6.9% 🟢) 2.007s (~) 0.837s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.173s 2.007s 0.835s 15 1.00x
🌐 Redis Next.js (Turbopack) 1.233s 2.006s 0.774s 15 1.05x
💻 Local Next.js (Turbopack) 1.327s 2.006s 0.679s 15 1.13x
💻 Local Nitro 1.380s (-26.0% 🟢) 2.006s (-14.3% 🟢) 0.626s 15 1.18x
💻 Local Express 1.393s (-26.5% 🟢) 2.006s (-15.1% 🟢) 0.613s 15 1.19x
🐘 Postgres Express 1.408s (+12.0% 🔺) 2.062s (+2.7%) 0.654s 15 1.20x
🌐 MongoDB Next.js (Turbopack) 2.036s 2.916s 0.880s 11 1.74x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.271s (-7.6% 🟢) 4.045s (-3.0%) 1.774s 8 1.00x
▲ Vercel Express 2.336s (-9.5% 🟢) 3.766s (-13.4% 🟢) 1.430s 8 1.03x
▲ Vercel Next.js (Turbopack) 4.170s (+42.2% 🔺) 6.038s (+30.1% 🔺) 1.869s 5 1.84x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.251s (-46.5% 🟢) 2.007s (-33.3% 🟢) 0.757s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.282s 2.006s 0.724s 15 1.03x
🐘 Postgres Express 1.393s (-40.5% 🟢) 2.035s (-32.4% 🟢) 0.642s 15 1.11x
💻 Local Nitro 1.957s (-36.2% 🟢) 2.316s (-40.4% 🟢) 0.359s 13 1.56x
💻 Local Express 2.022s (-35.4% 🟢) 2.508s (-33.3% 🟢) 0.485s 12 1.62x
💻 Local Next.js (Turbopack) 2.130s 2.826s 0.696s 11 1.70x
🌐 Redis Next.js (Turbopack) 2.350s 3.008s 0.658s 10 1.88x
🌐 MongoDB Next.js (Turbopack) 3.543s 4.007s 0.464s 8 2.83x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.338s (+35.9% 🔺) 6.011s (+25.4% 🔺) 1.673s 5 1.00x
▲ Vercel Nitro 4.664s (+44.2% 🔺) 6.658s (+31.1% 🔺) 1.994s 5 1.08x
▲ Vercel Next.js (Turbopack) 4.714s (+50.0% 🔺) 6.542s (+44.7% 🔺) 1.827s 5 1.09x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.380s (-60.3% 🟢) 2.007s (-49.9% 🟢) 0.627s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.483s 2.008s 0.525s 15 1.07x
🐘 Postgres Express 1.982s (-43.4% 🟢) 2.737s (-31.8% 🟢) 0.755s 11 1.44x
🌐 Redis Next.js (Turbopack) 3.571s 4.010s 0.439s 8 2.59x
💻 Local Nitro 4.894s (-46.5% 🟢) 5.348s (-46.7% 🟢) 0.454s 6 3.55x
💻 Local Express 5.939s (-32.5% 🟢) 6.417s (-30.8% 🟢) 0.478s 5 4.30x
💻 Local Next.js (Turbopack) 6.115s 6.416s 0.301s 5 4.43x
🌐 MongoDB Next.js (Turbopack) 6.302s 7.013s 0.710s 5 4.57x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.380s (+5.6% 🔺) 7.062s (+3.6%) 1.682s 5 1.00x
▲ Vercel Next.js (Turbopack) 5.690s (-15.8% 🟢) 7.416s (-13.2% 🟢) 1.726s 5 1.06x
▲ Vercel Express 5.700s (-11.2% 🟢) 7.539s (-7.8% 🟢) 1.840s 5 1.06x

🔍 Observability: Nitro | 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 🥇 Nitro 0.459s (-44.1% 🟢) 1.022s (+1.6%) 0.564s 59 1.00x
💻 Local Nitro 0.472s (-51.8% 🟢) 1.021s (-6.7% 🟢) 0.548s 59 1.03x
💻 Local Express 0.482s (-51.1% 🟢) 1.004s (-6.7% 🟢) 0.522s 60 1.05x
🐘 Postgres Next.js (Turbopack) 0.599s 1.040s 0.442s 58 1.31x
🌐 Redis Next.js (Turbopack) 0.619s 1.004s 0.386s 60 1.35x
🐘 Postgres Express 0.644s (-23.2% 🟢) 1.140s (+11.4% 🔺) 0.496s 53 1.40x
💻 Local Next.js (Turbopack) 0.719s 1.004s 0.285s 60 1.57x
🌐 MongoDB Next.js (Turbopack) 0.733s 1.006s 0.272s 60 1.60x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.661s (-75.5% 🟢) 6.339s (-70.3% 🟢) 1.677s 10 1.00x
▲ Vercel Nitro 5.218s (-76.3% 🟢) 7.258s (-69.8% 🟢) 2.039s 10 1.12x
▲ Vercel Next.js (Turbopack) 6.455s (-55.5% 🟢) 7.922s (-50.7% 🟢) 1.466s 8 1.38x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.085s (-43.7% 🟢) 1.705s (-18.8% 🟢) 0.620s 53 1.00x
🐘 Postgres Express 1.162s (-41.2% 🟢) 1.588s (-29.7% 🟢) 0.426s 57 1.07x
💻 Local Express 1.197s (-60.3% 🟢) 2.006s (-44.1% 🟢) 0.809s 45 1.10x
💻 Local Nitro 1.220s (-59.8% 🟢) 2.005s (-46.6% 🟢) 0.786s 45 1.12x
🌐 Redis Next.js (Turbopack) 1.476s 2.006s 0.530s 45 1.36x
🐘 Postgres Next.js (Turbopack) 1.707s 2.387s 0.679s 38 1.57x
💻 Local Next.js (Turbopack) 1.753s 2.005s 0.253s 45 1.61x
🌐 MongoDB Next.js (Turbopack) 1.813s 2.007s 0.194s 45 1.67x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 11.733s (-70.3% 🟢) 13.729s (-66.8% 🟢) 1.996s 7 1.00x
▲ Vercel Express 12.793s (-62.9% 🟢) 14.645s (-60.2% 🟢) 1.852s 7 1.09x
▲ Vercel Next.js (Turbopack) 14.268s (-71.4% 🟢) 16.580s (-67.9% 🟢) 2.313s 6 1.22x

🔍 Observability: Nitro | 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.108s (-48.6% 🟢) 2.696s (-41.4% 🟢) 0.588s 45 1.00x
🐘 Postgres Express 2.634s (-34.0% 🟢) 3.074s (-29.6% 🟢) 0.440s 40 1.25x
🐘 Postgres Next.js (Turbopack) 2.659s 3.084s 0.425s 39 1.26x
💻 Local Nitro 2.660s (-71.4% 🟢) 3.007s (-70.0% 🟢) 0.348s 40 1.26x
💻 Local Express 2.743s (-70.2% 🟢) 3.033s (-69.7% 🟢) 0.290s 40 1.30x
🌐 Redis Next.js (Turbopack) 2.992s 3.191s 0.200s 38 1.42x
💻 Local Next.js (Turbopack) 3.748s 4.007s 0.259s 30 1.78x
🌐 MongoDB Next.js (Turbopack) 4.178s 5.012s 0.833s 24 1.98x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 35.192s (-72.9% 🟢) 37.772s (-71.4% 🟢) 2.580s 4 1.00x
▲ Vercel Nitro 36.812s (-62.0% 🟢) 39.053s (-60.3% 🟢) 2.241s 4 1.05x
▲ Vercel Next.js (Turbopack) 43.007s (-59.9% 🟢) 46.755s (-57.1% 🟢) 3.748s 3 1.22x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.158s (-44.2% 🟢) 1.007s (~) 0.850s 60 1.00x
🐘 Postgres Nitro 0.182s (-35.8% 🟢) 1.006s (~) 0.824s 60 1.15x
🐘 Postgres Next.js (Turbopack) 0.190s 1.005s 0.815s 60 1.21x
🌐 Redis Next.js (Turbopack) 0.316s 1.021s 0.705s 59 2.00x
💻 Local Nitro 0.412s (-31.9% 🟢) 1.004s (-1.7%) 0.592s 60 2.61x
💻 Local Express 0.452s (-19.3% 🟢) 1.004s (~) 0.552s 60 2.87x
💻 Local Next.js (Turbopack) 0.603s 1.039s 0.436s 58 3.83x
🌐 MongoDB Next.js (Turbopack) 1.029s 1.749s 0.720s 35 6.53x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.178s (+11.5% 🔺) 4.588s (+26.2% 🔺) 2.410s 14 1.00x
▲ Vercel Nitro 2.258s (+36.0% 🔺) 4.485s (+33.9% 🔺) 2.227s 14 1.04x
▲ Vercel Next.js (Turbopack) 3.716s (+83.7% 🔺) 5.458s (+43.9% 🔺) 1.742s 11 1.71x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.304s (-38.8% 🟢) 1.006s (~) 0.702s 90 1.00x
🌐 Redis Next.js (Turbopack) 0.423s 1.004s 0.581s 90 1.39x
🐘 Postgres Express 0.520s (+2.0%) 1.112s (+10.5% 🔺) 0.592s 82 1.71x
🐘 Postgres Next.js (Turbopack) 0.705s 1.318s 0.613s 69 2.32x
💻 Local Nitro 2.170s (-14.5% 🟢) 2.821s (-6.3% 🟢) 0.651s 32 7.14x
💻 Local Express 2.186s (-13.0% 🟢) 2.944s (-2.2%) 0.758s 31 7.19x
💻 Local Next.js (Turbopack) 2.424s 3.043s 0.619s 30 7.97x
🌐 MongoDB Next.js (Turbopack) 2.595s 3.007s 0.412s 30 8.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.618s (+87.2% 🔺) 8.356s (+60.9% 🔺) 1.738s 11 1.00x
▲ Vercel Nitro 7.134s (+121.1% 🔺) 9.355s (+94.0% 🔺) 2.221s 10 1.08x
▲ Vercel Express 7.527s (+147.1% 🔺) 9.552s (+98.7% 🔺) 2.025s 10 1.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.637s (-19.4% 🟢) 1.006s (~) 0.369s 120 1.00x
🐘 Postgres Next.js (Turbopack) 0.730s 1.013s 0.284s 119 1.15x
🐘 Postgres Express 0.790s (-3.5%) 1.192s (+17.1% 🔺) 0.402s 101 1.24x
🌐 Redis Next.js (Turbopack) 0.797s 1.004s 0.207s 120 1.25x
🌐 MongoDB Next.js (Turbopack) 5.387s 6.013s 0.626s 20 8.46x
💻 Local Nitro 9.790s (-12.5% 🟢) 10.276s (-11.9% 🟢) 0.486s 12 15.37x
💻 Local Express 10.085s (-9.9% 🟢) 10.782s (-9.7% 🟢) 0.697s 12 15.84x
💻 Local Next.js (Turbopack) 11.478s 12.128s 0.650s 10 18.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 16.821s (+117.8% 🔺) 19.141s (+103.6% 🔺) 2.319s 7 1.00x
▲ Vercel Next.js (Turbopack) 18.466s (+78.8% 🔺) 20.419s (+66.2% 🔺) 1.953s 7 1.10x
▲ Vercel Express 20.247s (+172.9% 🔺) 22.460s (+143.0% 🔺) 2.213s 6 1.20x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

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.133s (+430.1% 🔺) 2.005s (+99.6% 🔺) 0.010s (-19.2% 🟢) 2.017s (+98.0% 🔺) 0.884s 10 1.00x
🐘 Postgres Nitro 1.134s (+453.4% 🔺) 2.001s (+100.2% 🔺) 0.001s (-26.7% 🟢) 2.010s (+98.7% 🔺) 0.875s 10 1.00x
💻 Local Express 1.136s (+470.5% 🔺) 2.005s (+99.6% 🔺) 0.012s (~) 2.020s (+98.4% 🔺) 0.884s 10 1.00x
💻 Local Next.js (Turbopack) 1.172s 2.003s 0.012s 2.019s 0.847s 10 1.03x
🐘 Postgres Express 1.232s (+500.5% 🔺) 2.005s (+100.8% 🔺) 0.001s (-37.5% 🟢) 2.041s (+101.8% 🔺) 0.810s 10 1.09x
🐘 Postgres Next.js (Turbopack) 2.024s 2.601s 0.220s 2.874s 0.850s 10 1.79x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.200s (-42.6% 🟢) 3.478s (-34.1% 🟢) 1.562s (+110.5% 🔺) 5.432s (-16.2% 🟢) 3.232s 10 1.00x
▲ Vercel Express 2.336s (-6.8% 🟢) 3.935s (-3.8%) 1.087s (+13.1% 🔺) 5.425s (-3.0%) 3.089s 10 1.06x
▲ Vercel Next.js (Turbopack) 4.802s (-29.9% 🟢) 4.900s (-43.4% 🟢) 0.740s (+17.0% 🔺) 7.346s (-25.0% 🟢) 2.544s 10 2.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.512s (+142.2% 🔺) 2.005s (+99.2% 🔺) 0.004s (-8.9% 🟢) 2.024s (+97.9% 🔺) 0.512s 30 1.00x
💻 Local Nitro 1.514s (+80.5% 🔺) 2.011s (+98.7% 🔺) 0.010s (+2.8%) 2.022s (+81.2% 🔺) 0.509s 30 1.00x
💻 Local Express 1.721s (+127.3% 🔺) 2.011s (+95.5% 🔺) 0.010s (+2.1%) 2.202s (+111.7% 🔺) 0.481s 28 1.14x
🐘 Postgres Next.js (Turbopack) 1.722s 2.107s 0.003s 2.123s 0.401s 30 1.14x
💻 Local Next.js (Turbopack) 1.845s 2.011s 0.009s 2.203s 0.358s 28 1.22x
🐘 Postgres Express 1.983s (+214.8% 🔺) 2.330s (+131.5% 🔺) 0.003s (-20.7% 🟢) 2.395s (+134.1% 🔺) 0.412s 26 1.31x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.656s (-13.0% 🟢) 7.281s (-9.1% 🟢) 0.179s (-56.1% 🟢) 8.330s (-5.7% 🟢) 2.675s 8 1.00x
▲ Vercel Nitro 5.796s (-80.3% 🟢) 7.455s (-75.8% 🟢) 0.170s (+52.1% 🔺) 8.071s (-74.6% 🟢) 2.275s 8 1.02x
▲ Vercel Next.js (Turbopack) 12.255s (-27.6% 🟢) 13.600s (-25.4% 🟢) 0.201s (-4.9%) 15.301s (-19.2% 🟢) 3.046s 4 2.17x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.651s (-32.8% 🟢) 1.033s (-17.2% 🟢) 0.000s (-100.0% 🟢) 1.042s (-17.2% 🟢) 0.390s 58 1.00x
🐘 Postgres Express 1.031s (+7.3% 🔺) 1.390s (+8.7% 🔺) 0.000s (+9.5% 🔺) 1.437s (+10.0% 🔺) 0.406s 42 1.58x
🐘 Postgres Next.js (Turbopack) 1.333s 1.618s 0.000s 1.715s 0.382s 37 2.05x
💻 Local Nitro 1.337s (+9.4% 🔺) 2.015s (~) 0.000s (+166.7% 🔺) 2.017s (~) 0.680s 30 2.05x
💻 Local Express 1.368s (+11.7% 🔺) 2.015s (~) 0.000s (-60.0% 🟢) 2.017s (~) 0.649s 30 2.10x
💻 Local Next.js (Turbopack) 1.436s 2.013s 0.000s 2.016s 0.581s 30 2.20x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.922s (+28.6% 🔺) 5.598s (+27.4% 🔺) 0.000s (-100.0% 🟢) 6.027s (+25.3% 🔺) 2.105s 10 1.00x
▲ Vercel Express 4.013s (+7.3% 🔺) 5.879s (+15.2% 🔺) 0.000s (-100.0% 🟢) 6.416s (+16.0% 🔺) 2.404s 10 1.02x
▲ Vercel Next.js (Turbopack) 5.465s (-46.3% 🟢) 6.707s (-41.8% 🟢) 0.000s (+Infinity% 🔺) 7.645s (-36.6% 🟢) 2.181s 8 1.39x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

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.327s (-25.9% 🟢) 2.066s (-3.5%) 0.000s (-100.0% 🟢) 2.081s (-4.3%) 0.754s 29 1.00x
🐘 Postgres Next.js (Turbopack) 1.844s 2.402s 0.000s 2.460s 0.616s 25 1.39x
🐘 Postgres Express 2.377s (+34.2% 🔺) 3.042s (+39.7% 🔺) 0.000s (NaN%) 3.091s (+40.6% 🔺) 0.714s 20 1.79x
💻 Local Next.js (Turbopack) 2.776s 3.362s 0.000s 3.365s 0.589s 18 2.09x
💻 Local Nitro 3.068s (-9.4% 🟢) 3.838s (-4.8%) 0.000s (-53.1% 🟢) 3.843s (-4.8%) 0.775s 16 2.31x
💻 Local Express 3.103s (-10.5% 🟢) 3.775s (-6.4% 🟢) 0.000s (-76.6% 🟢) 3.781s (-6.3% 🟢) 0.679s 16 2.34x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.059s (+10.3% 🔺) 6.788s (+12.7% 🔺) 0.000s (+Infinity% 🔺) 7.571s (+17.3% 🔺) 2.512s 8 1.00x
▲ Vercel Nitro 5.089s (+24.3% 🔺) 6.536s (+21.6% 🔺) 0.000s (-18.5% 🟢) 7.002s (+20.9% 🔺) 1.913s 9 1.01x
▲ Vercel Next.js (Turbopack) 8.675s (+54.5% 🔺) 10.065s (+44.2% 🔺) 0.000s (-100.0% 🟢) 11.283s (+49.6% 🔺) 2.608s 6 1.71x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 17/21
🐘 Postgres Nitro 19/21
▲ Vercel Express 11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 859 0 219 1078
✅ 💻 Local Development 957 0 219 1176
✅ 📦 Local Production 957 0 219 1176
✅ 🐘 Local Postgres 957 0 219 1176
✅ 🪟 Windows 98 0 0 98
✅ 📋 Other 438 0 150 588
Total 4266 0 1026 5292

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 72 0 26
✅ example 72 0 26
✅ express 72 0 26
✅ fastify 72 0 26
✅ hono 72 0 26
✅ nextjs-turbopack 96 0 2
✅ nextjs-webpack 96 0 2
✅ nitro 72 0 26
✅ nuxt 72 0 26
✅ sveltekit 91 0 7
✅ vite 72 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 73 0 25
✅ express-stable 73 0 25
✅ fastify-stable 73 0 25
✅ hono-stable 73 0 25
✅ nextjs-turbopack-canary 79 0 19
✅ nextjs-turbopack-stable 98 0 0
✅ nextjs-webpack-canary 79 0 19
✅ nextjs-webpack-stable 98 0 0
✅ nitro-stable 73 0 25
✅ nuxt-stable 73 0 25
✅ sveltekit-stable 92 0 6
✅ vite-stable 73 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 73 0 25
✅ express-stable 73 0 25
✅ fastify-stable 73 0 25
✅ hono-stable 73 0 25
✅ nextjs-turbopack-canary 79 0 19
✅ nextjs-turbopack-stable 98 0 0
✅ nextjs-webpack-canary 79 0 19
✅ nextjs-webpack-stable 98 0 0
✅ nitro-stable 73 0 25
✅ nuxt-stable 73 0 25
✅ sveltekit-stable 92 0 6
✅ vite-stable 73 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 73 0 25
✅ express-stable 73 0 25
✅ fastify-stable 73 0 25
✅ hono-stable 73 0 25
✅ nextjs-turbopack-canary 79 0 19
✅ nextjs-turbopack-stable 98 0 0
✅ nextjs-webpack-canary 79 0 19
✅ nextjs-webpack-stable 98 0 0
✅ nitro-stable 73 0 25
✅ nuxt-stable 73 0 25
✅ sveltekit-stable 92 0 6
✅ vite-stable 73 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 98 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 73 0 25
✅ e2e-local-dev-tanstack-start-stable 73 0 25
✅ e2e-local-postgres-nest-stable 73 0 25
✅ e2e-local-postgres-tanstack-start-stable 73 0 25
✅ e2e-local-prod-nest-stable 73 0 25
✅ e2e-local-prod-tanstack-start-stable 73 0 25

📋 View full workflow run

Comment thread packages/ai/package.json Outdated
Comment thread packages/builders/src/constants.ts Outdated
Comment thread packages/core/e2e/e2e.test.ts Outdated
Comment thread packages/core/e2e/utils.ts Outdated
Comment thread packages/core/src/runtime/suspension-handler.ts
Comment thread packages/next/src/builder-deferred.ts Outdated
Comment thread packages/next/src/builder-deferred.ts Outdated
Comment thread packages/core/src/runtime.ts
Comment thread packages/core/src/runtime.ts Outdated
Comment thread packages/core/src/runtime.ts
Comment thread packages/core/src/runtime/run.ts Outdated
Comment thread packages/vitest/src/index.ts
Comment thread packages/core/src/runtime/run.ts Outdated
Comment thread packages/core/src/workflow.ts Outdated
Comment thread packages/builders/src/base-builder.ts
Comment thread packages/core/src/runtime/get-world-lazy.ts
Copy link
Copy Markdown
Member Author

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs
Copy link
Copy Markdown

shtefcs commented Apr 26, 2026

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

const existingIds = new Set(cachedEvents.map((e) => e.eventId));
for (const e of loaded.events) {
  if (!existingIds.has(e.eventId)) {
    cachedEvents.push(e);
  }
}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if (currentAttempt >= maxRetries + 1) { /* step_failed */ }

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1 without step.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySerious and others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.

A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.

Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
  Each loop iteration: load events incrementally, replay workflow,
  inline-execute one owned step on suspension, loop. Background steps
  (from Promise.all) are queued back to the same handler with stepId so
  the in-flight handler advances the loop without queue round-trips
  whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
  with workflowEntrypoint(workflowCode) plus a side-effect
  __step_registrations bundle. ESM-by-default with createRequire banner
  for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
  fastify, vite: framework builders updated to use createCombinedBundle.
  The step/ directory is no longer generated.

Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
  (per-step in-process mutex in world-local; SQL-level guards in
  postgres/vercel) means exactly one handler claims ownership of each
  step. Inline execution gates on ownership; queue dispatch is
  unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
  unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
  removed every code path that produced orphaned step lifecycle events
  during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
  flushable-stream so the V2 step-executor's ops-settle race typically
  resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
  is detected by scanning the loaded event log for terminal run events
  instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
  fibonacci/recursive parent→child polling case is now handled by
  raising world-postgres default queueConcurrency from 10 to 50, with
  the limitation documented at three call sites (fibonacciWorkflow's
  JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
  chain to world.ts so step bundles don't transitively pull in
  world-vercel + cbor-x + the VQS dev-handler resolver.

World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
  lifecycle events on a (runId, correlationId) key. step_started is
  rejected only when the step is already terminal — duplicate
  step_started on a non-terminal step is allowed (preserves retry
  semantics after SIGKILL).

Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
  previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
  into one helper.

Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
  architecture, edge cases, and the fixes applied during the V2 work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:

1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
   were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
   + createStepsBundle) and expecting two world handlers (workflow +
   step). Updated the mocks to expose createCombinedBundle and
   asserted a single combined handler at __wkf_workflow_, which is
   what the V2 vitest builder actually emits.

2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
   error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
   pre-V2 webpack dev mode (which imported step sources directly).
   V2 inlines the step bundle into the combined flow route, and
   webpack's re-bundling collapses original step filenames out of the
   dev-mode source maps. Extended the existing nextjs-webpack
   carve-out in hasStepSourceMaps() to cover dev as well as prod, and
   updated the source-map follow-up callout in eager-processing.mdx
   to include nextjs-webpack in the matrix that needs source maps
   wired up for V2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:

* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
  Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
  hookDisposeTestWorkflow test in favor of the new top-level
  `waitForHook(token, { runId })` API merged from main. Keep V2's
  event-driven `waitForHookDisposal()` helper, which is stricter than
  main's `await sleep(3_000)` (it polls `getHookByToken` for the
  HookNotFoundError signal rather than waiting a fixed interval).

* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
  V2 per-step async mutex)
  Both branches were closing race conditions in the local world's
  step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
  for `step_created` and `wait_created`; the V2 branch already wraps
  step lifecycle events in an in-process `withStepLock` mutex. They
  compose: the mutex serializes within a single Node process; the
  filesystem lock additionally protects against cross-process races
  (multiple pnpm workers, redelivered queue messages). Resolution
  takes V2's mutex wrap as the base and patches in main's two
  `writeExclusive` claims at the start of the `step_created` and
  `wait_created` handlers, with comments noting the dual-layer
  guarantee.

`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.

Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).

The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.

Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.

Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:

* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
  PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
  package as a static import and added a structured `Logger`
  interface with `child()` / `forRun()` and `composeLogLine`. The V2
  branch had previously removed the `debug` static import because it
  pulls `debug/src/node` and a dynamic `require('tty')` into the
  Next.js webpack flow route, breaking V2 builds with
  `Dynamic require of "tty" is not supported`.
  Resolution combines both: keep main's `Logger` interface,
  `child()` / `forRun()`, and `composeLogLine` integration; replace
  the `debug` invocations with the V2 lightweight
  `matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
  route still bundles cleanly under webpack. Comment in the file
  documents why `debug` is intentionally absent.

* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
  Multiple regions where main's structured-logging refactor and the
  V2 inline-execution loop made overlapping changes. Took V2's
  control flow as the base — its `if (!workflowRun) { ... }` setup
  block, main replay `while (true)` loop with timeout-based
  re-scheduling, terminal-event short-circuit, and per-iteration
  incremental event load are all preserved as-is — and folded in
  main's contributions: the scoped `runLogger = runtimeLogger.forRun(
  runId, workflowName)` is created once and used for run-level info
  / error logging. Discarded main's V1 wait-completion loop and
  `runWorkflow` user-code error block since V2 handles waits and
  user-code errors inside the inline loop body. Updated
  `buildWorkflowSuspensionMessage()` call to drop a stray `runId`
  first arg that didn't match the helper's 3-param signature.

* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
  No conflict from main's friendly-errors PR, auto-merged. (No
  hookDispose-style conflict this round.)

Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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.

3 participants