Skip to content

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

Draft
VaguelySerious wants to merge 89 commits intomainfrom
peter/v2-flow
Draft

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious wants to merge 89 commits intomainfrom
peter/v2-flow

Conversation

@VaguelySerious
Copy link
Member

@VaguelySerious VaguelySerious commented Mar 11, 2026

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2026

🦋 Changeset detected

Latest commit: 14d84f2

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/builders Patch
@workflow/next Patch
@workflow/nest Patch
@workflow/sveltekit Patch
@workflow/nitro Patch
@workflow/astro Patch
@workflow/world Patch
workflow Patch
@workflow/cli Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/world-testing Patch
@workflow/rollup Patch
@workflow/vite Patch
@workflow/nuxt Patch
@workflow/world-local Patch
@workflow/world-postgres 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
Contributor

vercel bot commented Mar 11, 2026

@github-actions
Copy link
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.040s (-4.7%) 1.006s (~) 0.965s 10 1.00x
💻 Local Express 0.045s (+1.3%) 1.006s (-1.9%) 0.960s 10 1.12x
🐘 Postgres Express 0.060s (-4.6%) 1.011s (~) 0.950s 10 1.50x
🐘 Postgres Nitro 0.063s (+1.4%) 1.013s (~) 0.950s 10 1.57x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.823s (-13.6% 🟢) 2.531s (-14.8% 🟢) 1.708s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.096s (-2.5%) 2.006s (~) 0.909s 10 1.00x
💻 Local Nitro 1.096s (-2.5%) 2.005s (~) 0.909s 10 1.00x
🐘 Postgres Express 1.125s (-1.6%) 2.012s (~) 0.887s 10 1.03x
🐘 Postgres Nitro 1.131s (-1.6%) 2.013s (~) 0.882s 10 1.03x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.732s (+27.2% 🔺) 4.019s (+5.3% 🔺) 1.286s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.577s (-3.0%) 11.021s (~) 0.444s 3 1.00x
💻 Local Express 10.605s (-2.8%) 11.021s (~) 0.416s 3 1.00x
🐘 Postgres Express 10.707s (-1.9%) 11.041s (~) 0.334s 3 1.01x
🐘 Postgres Nitro 10.747s (-1.7%) 11.046s (~) 0.300s 3 1.02x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 18.364s (+4.8%) 19.746s (+2.7%) 1.382s 2 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 26.380s (-4.0%) 27.051s (-3.6%) 0.670s 3 1.00x
💻 Local Express 26.497s (-3.6%) 27.048s (-3.6%) 0.550s 3 1.00x
🐘 Postgres Express 26.654s (-2.1%) 27.062s (-3.6%) 0.408s 3 1.01x
🐘 Postgres Nitro 26.734s (-1.7%) 27.074s (-3.5%) 0.340s 3 1.01x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 125.009s (+184.3% 🔺) 126.639s (+172.6% 🔺) 1.630s 1 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 52.800s (-6.8% 🟢) 53.098s (-7.0% 🟢) 0.298s 2 1.00x
💻 Local Express 52.904s (-6.6% 🟢) 53.090s (-7.0% 🟢) 0.186s 2 1.00x
🐘 Postgres Express 53.200s (-1.9%) 54.121s (-1.8%) 0.921s 2 1.01x
🐘 Postgres Nitro 53.350s (-1.8%) 54.116s (-1.8%) 0.766s 2 1.01x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 165.958s (+72.1% 🔺) 167.889s (+70.3% 🔺) 1.931s 1 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.404s (-2.7%) 2.011s (~) 0.607s 15 1.00x
🐘 Postgres Express 1.407s (-5.1% 🟢) 2.011s (~) 0.604s 15 1.00x
💻 Local Nitro 1.474s (-1.1%) 2.006s (~) 0.531s 15 1.05x
💻 Local Express 1.509s (+2.0%) 2.005s (~) 0.496s 15 1.07x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.950s (+96.8% 🔺) 6.191s (+51.1% 🔺) 1.241s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 2.620s (-11.4% 🟢) 3.108s (-6.0% 🟢) 0.488s 10 1.00x
💻 Local Nitro 2.709s (-5.2% 🟢) 3.109s (-7.0% 🟢) 0.400s 10 1.03x
🐘 Postgres Express 2.817s (+8.3% 🔺) 3.345s (+11.0% 🔺) 0.528s 9 1.08x
🐘 Postgres Nitro 2.868s (+10.7% 🔺) 3.348s (+11.1% 🔺) 0.480s 9 1.09x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.662s (+112.4% 🔺) 6.906s (+49.0% 🔺) 1.244s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 7.694s (-5.6% 🟢) 8.267s (-5.7% 🟢) 0.573s 4 1.00x
💻 Local Nitro 7.856s (-3.8%) 8.265s (-8.4% 🟢) 0.409s 4 1.02x
🐘 Postgres Express 22.660s (+467.7% 🔺) 23.565s (+429.7% 🔺) 0.905s 2 2.95x
🐘 Postgres Nitro 23.697s (+494.2% 🔺) 24.566s (+419.3% 🔺) 0.869s 2 3.08x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 7.724s (+103.2% 🔺) 9.292s (+68.3% 🔺) 1.568s 4 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.379s (-3.0%) 2.010s (~) 0.631s 15 1.00x
🐘 Postgres Express 1.388s (~) 2.012s (~) 0.623s 15 1.01x
💻 Local Nitro 1.579s (+3.1%) 2.072s (+3.3%) 0.493s 15 1.14x
💻 Local Express 1.647s (+7.6% 🔺) 2.072s (+3.3%) 0.425s 15 1.19x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.610s (+106.6% 🔺) 6.170s (+63.7% 🔺) 1.560s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.484s (-6.7% 🟢) 3.013s (-3.3%) 0.529s 10 1.00x
🐘 Postgres Express 2.650s (+5.3% 🔺) 3.113s (+3.4%) 0.464s 10 1.07x
💻 Local Express 2.761s (-7.6% 🟢) 3.342s (-6.3% 🟢) 0.580s 9 1.11x
💻 Local Nitro 2.783s (-8.8% 🟢) 3.342s (-14.0% 🟢) 0.559s 9 1.12x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 7.007s (+180.4% 🔺) 8.492s (+113.4% 🔺) 1.485s 4 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 8.274s (-10.9% 🟢) 8.766s (-12.5% 🟢) 0.492s 4 1.00x
💻 Local Express 8.458s (-3.1%) 9.018s (~) 0.560s 4 1.02x
🐘 Postgres Express 20.116s (+409.0% 🔺) 20.567s (+348.4% 🔺) 0.452s 2 2.43x
🐘 Postgres Nitro 31.007s (+659.0% 🔺) 31.079s (+598.9% 🔺) 0.072s 2 3.75x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.946s (+95.4% 🔺) 7.796s (+42.8% 🔺) 0.851s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: 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 🥇 Nitro 0.165s (-17.0% 🟢) 1.003s (~) 0.011s (-6.0% 🟢) 1.017s (~) 0.852s 10 1.00x
💻 Local Express 0.172s (-11.7% 🟢) 1.003s (~) 0.010s (-9.6% 🟢) 1.016s (~) 0.844s 10 1.04x
🐘 Postgres Express 0.198s (-10.5% 🟢) 0.995s (~) 0.001s (+16.7% 🔺) 1.012s (~) 0.814s 10 1.21x
🐘 Postgres Nitro 0.200s (-9.2% 🟢) 0.997s (~) 0.002s (+15.4% 🔺) 1.013s (~) 0.813s 10 1.22x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 33.433s (+1847.7% 🔺) 34.470s (+1169.7% 🔺) 0.004s (-8.3% 🟢) 35.098s (+898.9% 🔺) 1.666s 10 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 8/12
🐘 Postgres Express 9/12
▲ Vercel Next.js (Turbopack) 12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 9/12
Next.js (Turbopack) ▲ Vercel 12/12
Nitro 💻 Local 9/12
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
Contributor

github-actions bot commented Mar 11, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 202 0 11 213
✅ 💻 Local Development 642 0 210 852
✅ 📦 Local Production 642 0 210 852
✅ 🐘 Local Postgres 580 0 201 781
✅ 🪟 Windows 68 0 3 71
❌ 🌍 Community Worlds 116 55 15 186
✅ 📋 Other 96 0 46 142
Total 2346 55 696 3097

❌ Failed Tests

🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ nextjs-turbopack 69 0 2
✅ nextjs-webpack 69 0 2
✅ sveltekit 64 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 48 0 23
✅ express-stable 48 0 23
✅ fastify-stable 48 0 23
✅ hono-stable 48 0 23
✅ nextjs-turbopack-canary 54 0 17
✅ nextjs-turbopack-stable 68 0 3
✅ nextjs-webpack-canary 54 0 17
✅ nextjs-webpack-stable 68 0 3
✅ nitro-stable 48 0 23
✅ nuxt-stable 48 0 23
✅ sveltekit-stable 62 0 9
✅ vite-stable 48 0 23
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 48 0 23
✅ express-stable 48 0 23
✅ fastify-stable 48 0 23
✅ hono-stable 48 0 23
✅ nextjs-turbopack-canary 54 0 17
✅ nextjs-turbopack-stable 68 0 3
✅ nextjs-webpack-canary 54 0 17
✅ nextjs-webpack-stable 68 0 3
✅ nitro-stable 48 0 23
✅ nuxt-stable 48 0 23
✅ sveltekit-stable 62 0 9
✅ vite-stable 48 0 23
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 48 0 23
✅ express-stable 48 0 23
✅ fastify-stable 48 0 23
✅ hono-stable 48 0 23
✅ nextjs-turbopack-canary 54 0 17
✅ nextjs-turbopack-stable 68 0 3
✅ nextjs-webpack-canary 54 0 17
✅ nextjs-webpack-stable 68 0 3
✅ nitro-stable 48 0 23
✅ nuxt-stable 48 0 23
✅ vite-stable 48 0 23
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 68 0 3
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 2
❌ mongodb 51 3 3
✅ redis-dev 3 0 2
❌ redis 52 2 3
✅ turso-dev 3 0 2
❌ turso 4 50 3
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 48 0 23
✅ e2e-local-prod-nest-stable 48 0 23

📋 View full workflow run

TooTallNate and others added 3 commits March 11, 2026 17:08
…nd step handler

Transient network errors (ECONNRESET, etc.) during infrastructure calls
(event listing, event creation) were caught by a shared try/catch that
also handles user code errors, incorrectly marking runs as run_failed
or steps as step_failed instead of letting the queue redeliver.

- runtime.ts: Move infrastructure calls outside the user-code try/catch
  so errors propagate to the queue handler for automatic retry
- step-handler.ts: Same structural separation — only stepFn.apply() is
  wrapped in the try/catch that produces step_failed/step_retrying
- helpers.ts: Add isTransientNetworkError() and update withServerErrorRetry
  to retry network errors in addition to 5xx responses
- helpers.test.ts: Add tests for network error detection and retry
Merge flow and step routes into a single combined handler that executes
steps inline when possible, reducing function invocations and queue
overhead. Serial workflows can now complete in a single function
invocation instead of 2N+1 invocations.

Key changes:
- Add `combinedEntrypoint()` to core runtime with inline step execution loop
- Extract reusable step execution logic into `step-executor.ts`
- Add `handleSuspensionV2()` that creates events without queuing steps
- Add `stepId` field to `WorkflowInvokePayload` for background step dispatch
- Add `createCombinedBundle()` to base builder
- Update Next.js builder to generate combined route at v1/flow
- Update health check e2e tests for single-route architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VaguelySerious and others added 3 commits March 14, 2026 09:36
The steps bundle only contains side-effect code (registerStepFunction
calls) with no exports. When rollup processes the combined route that
imports this module, it tree-shakes the entire module away because it
has no used exports.

Fix: add a sentinel export (__steps_registered) to the steps bundle and
import it in the combined route. This gives rollup a used binding to
track, preventing it from dropping the module and its side effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the V2 inline execution loop advances ahead (e.g., completes batch 1
inline and creates step_created events for batch 2), concurrent replays
from batch 1 background step continuations may encounter batch 2's events
without matching subscribers.

Fix: the EventsConsumer's onUnconsumedEvent callback now returns true to
skip step lifecycle events (step_created, step_started, step_completed,
step_failed, step_retrying) that have a corresponding step_created event
in the log — confirming they're from a legitimate concurrent handler.
Orphaned events with unknown correlationIds still error.

Also: steps bundle exports __steps_registered sentinel to prevent rollup
from tree-shaking the side-effect-only module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VaguelySerious and others added 15 commits March 15, 2026 12:59
…ation

Two fixes:

1. Step executor: await ops (stream writes) with a 5-second bounded wait
   before creating step_completed. In V1, each step was a separate function
   invocation and waitUntil ensured ops completed before the function ended.
   In V2 inline execution, the handler loop continues immediately, leaving
   stream data uncommitted. The bounded wait ensures data reaches S3 before
   proceeding, with waitUntil as a safety net for ops that need more time.

2. Step executor: only enforce max retries when step.error exists (actual
   retry after failure). V2 concurrent replays can inflate the attempt
   counter via simultaneous step_started calls without any prior failure.
   With N parallel steps completing, up to N concurrent continuations may
   race to start the same step, each incrementing attempt. The first
   completion wins (step_completed idempotency), but premature "exceeded
   max retries" failures must be prevented.

Also documents all integration challenges in the changelog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a step has pending background operations (e.g., WritableStream data
being piped to S3), the V2 inline execution loop must not continue to
the next replay iteration. Instead, queue a continuation and return to
give waitUntil time to flush the ops.

In V1, each step ran in a separate function invocation. After the step
completed, the function returned and waitUntil flushed the stream writes
to S3 before the test could read the data. In V2, the inline loop
continues processing, keeping the function alive and preventing
waitUntil from flushing. The test's stream reader blocks forever
because S3 data never arrives.

Fix: executeStep now returns hasPendingOps when the step had background
ops. The V2 handler checks this and breaks the loop, queueing a
continuation instead of looping inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add changelog entries for:
- Concurrent step_started inflating attempt counter (promiseRaceStressTest fix)
- Inline step execution with pending stream operations (outputStream fix)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
When DEBUG=workflow:* is set:

1. Runtime emits timing logs for:
   - Each event page load (page number, event count, ms)
   - Total event loading (total events, pages, total ms)
   - Incremental event loading (new events since cursor, ms)
   - Workflow replay start/completion/suspension (ms, event count)
   - Suspension handling duration (ms, pending steps, timeout)

2. World-vercel emits timing logs for every HTTP request:
   - Method, endpoint, status code, duration (ms)
   - Activated by DEBUG env containing "workflow:"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds debug logs to understand why the V2 inline loop exits early
on Vercel. Logs when a step has pending ops (showing ops count)
and when the loop breaks due to hasPendingOps, causing a
continuation to be queued instead of processing inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The V2 inline loop was breaking on every step with stream serialization
ops (hasPendingOps=true), causing a queue round-trip per step. AI agent
workflows with WritableStream parameters triggered this on every step,
defeating inline execution entirely.

Fix: await ops inline with a 500ms timeout in the step executor. Most
flushable pipe ops resolve within ~200ms (100ms lock-release polling +
flush). If ops settle within 500ms, hasPendingOps=false and the loop
continues inline. Only if ops don't settle (e.g., WritableStream kept
open across steps) does the loop break for waitUntil to handle.

This reduces an AI agent workflow from ~5 invocations (1 per step) to
~1-2 invocations, matching the V2 design goal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update the changelog to describe the three-tier ops handling strategy:
- Simple steps: no ops, no overhead, loop continues
- AI agent steps: ops settle inline within 500ms, loop continues
- Streaming output steps: ops don't settle, loop breaks for waitUntil

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The V2 inline loop called world.runs.get(runId) on every iteration to
check run status. This added 20-70ms per iteration for a redundant HTTP
call — the run stays 'running' during inline processing. The status
check and run_started transition only matter on the first pass.

Move the runs.get() and run_started logic above the while loop. The
loop now only handles event loading, replay, suspension, and step
execution. For a 5-step AI agent workflow, this saves ~120ms total
(5 iterations × ~24ms average).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When ops settled within the 500ms inline timeout, waitUntil was skipped.
On Vercel, the function can be garbage collected after returning — even
if the ops promise resolved, in-flight HTTP responses (S3 write acks)
may be dropped without waitUntil extending the function lifetime.

This caused outputStreamWorkflow to time out: the step wrote stream data
to S3, the ops promise resolved (lock released), but the function ended
before S3 fully acknowledged the write. The test reader never received
the data.

Fix: always call waitUntil(opsPromise) regardless of settlement. The
inline await still determines hasPendingOps for loop-break decisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WorkflowServerWritableStream uses buffered writes with a 10ms flush
timer. The flushablePipe's pendingOps counter reaches 0 as soon as
the buffered write() returns (before the timer fires and data reaches
S3). pollWritableLock saw pendingOps=0 and resolved immediately,
causing the V2 inline loop to consider ops settled before data was
actually on S3.

Fix: delay pollWritableLock resolution by 20ms after detecting lock
release + pendingOps=0. This allows the 10ms flush timer to fire and
the S3 write to complete before the ops promise resolves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WorkflowServerWritableStream buffers writes and flushes via a 10ms
setTimeout. The flushablePipe's pendingOps reaches 0 when the buffered
write() returns (before the flush timer fires and data reaches S3).
Even though ops appear settled, the S3 HTTP write hasn't started yet.

Fix: after ops settle within the 500ms inline timeout, wait an
additional 150ms to cover the flush timer (10ms) + S3 HTTP round-trip
(~100ms). This ensures stream data is on S3 before the V2 loop
continues and the handler potentially returns.

Reverts the pollWritableLock delay (insufficient) and the
WorkflowServerWritableStream.write() blocking (caused deadlocks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VaguelySerious and others added 10 commits March 16, 2026 15:27
c
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
This reverts commit f7b59ab.
The 500ms inline ops await caused outputStreamWorkflow to consistently
fail on Vercel Prod. The root cause: WorkflowServerWritableStream uses
buffered writes with a 10ms flush timer. The flushablePipe's pendingOps
reaches 0 before data reaches S3 (the buffered write returns instantly).
The ops appear settled but data isn't on S3 yet. Various attempts to
fix this (delayed pollWritableLock resolution, closing the writable,
150ms post-settle delay) all failed because the fundamental timing
between the ops promise resolution and S3 data availability is
non-deterministic on Vercel.

Revert to the proven approach: hasPendingOps = ops.length > 0. Any
step with stream serialization ops breaks the V2 inline loop and
queues a continuation. This gives waitUntil exclusive control to flush
the ops, matching V1 behavior. AI agent workflow optimization (reducing
invocations for stream-using steps) should be addressed separately by
fixing the buffered write timing in WorkflowServerWritableStream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js 16.2.0-canary.100+ has a regression where @workflow/ai step
files are missing from the step bundle, causing "doStreamStep not
found" errors that hang the agent tests until timeout.

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DEV_TEST_CONFIG was only set in the dev test job, so prod and postgres
canary jobs didn't skip agent tests. Add NEXT_CANARY env var to all
three local e2e test jobs (dev, prod, postgres) and use it directly.

Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI's getEnvVars() function reads a fixed list of env vars but was
missing WORKFLOW_LOCAL_BASE_URL. The health check test passes this env
var to tell the CLI which port the dev server is on (Astro uses 4321,
SvelteKit uses 5173). Without it, the CLI always defaults to port 3000,
causing ECONNREFUSED on non-Next.js frameworks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Vercel with parallel steps, each background step completion queues
a continuation. N parallel steps generate N concurrent continuations,
each loading all events (17 pages x ~40ms = ~680ms) + replaying
(~200ms) only to discover the run was already completed by another
handler. A workflow with 20 steps generated 20 concurrent replays,
causing Vercel Prod tests to timeout at 30 minutes.

Fix: add early exit checks in two places:
1. Background step path: skip step execution if run is not running
2. Inline loop: re-check run status on iterations > 1 to detect
   concurrent completion before expensive event loading

This reduces wasted work from ~900ms per redundant replay to ~40ms
(a single runs.get() call).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DurableAgent tests timeout (120s each × 14 tests = 28 minutes) on
Nitro-based Vercel BOA deployments, causing the 30-minute CI job
timeout. The V2 combined handler needs additional work for DurableAgent
support on these frameworks.

Skip agent tests for non-Next.js/SvelteKit apps. On main, these tests
only run on Next.js deployments (they were added after the BOA builders
were last tested). The regular workflow e2e tests still run on all
frameworks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive update to the V2 changelog documenting:
- Current state: what passes and what fails
- The buffered write timing issue (root cause analysis)
- Three failed optimization attempts and why each broke
- The Vercel BOA deployment hang (remaining blocker)
- What we know vs don't know
- Step-by-step debugging plan for the BOA issue

Co-Authored-By: Claude Opus 4.6 (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.

2 participants