[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[codex] Capture world contract failures as fatal - #2060

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure
May 21, 2026
Merged

[codex] Capture world contract failures as fatal#2060
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/runtime-schema-validation-failure

Conversation

@pranaygp

@pranaygppranaygp commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add WORLD_CONTRACT_ERROR as a distinct run failure code for non-retryable world/SDK contract failures
  • mark malformed Vercel world response bodies with stable WorkflowWorldError.code values (SCHEMA_VALIDATION / PARSE_ERROR) and classify them as WORLD_CONTRACT_ERROR
  • record fatal world contract failures during setup, event loading, replay event writes, and background-step prep as run_failed instead of letting the queue callback redrive until max deliveries
  • keep transient world failures on the existing retry path by only handling known fatal world contract failures
  • add regression coverage for setup schema validation, setup response parsing, event-list schema validation, classifier behavior, and user-facing error descriptions

Why

When the Vercel world validates a malformed response, it throws a non-retryable WorkflowWorldError. The workflow queue handler previously let some of those errors escape, so Q kept redelivering the message until max deliveries. This failure is not user code and not a transient backend retry case; it is a fatal world/client contract mismatch.

Conflict resolution

Rebased onto origin/main and preserved the new CORRUPTED_EVENT_LOG classification from base alongside the new WORLD_CONTRACT_ERROR handling.

Validation

  • pnpm exec biome check --write packages/core/src/classify-error.ts packages/core/src/classify-error.test.ts packages/core/src/runtime.ts packages/core/src/runtime.test.ts packages/world-vercel/src/utils.ts
  • pnpm turbo build --filter=@workflow/core --filter=@workflow/world-vercel
  • pnpm vitest run packages/core/src/runtime.test.ts packages/core/src/describe-error.test.ts packages/core/src/classify-error.test.ts
  • pnpm changeset status --since=main

Backport

This needs to be backported after merge in main.

Note: local commands emitted the repo engine warning because this machine is on Node v25.2.1 while the repo supports Node 18/20/22/24.

@vercel

vercelBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a85d52c

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/errorsPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-33.2% 🟢)1.005s (~)0.976s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.05x
🐘 PostgresExpress0.046s (-20.9% 🟢)1.013s (~)0.967s101.55x
🐘 PostgresNitro0.048s (-49.3% 🟢)1.012s (-2.9%)0.964s101.63x
🐘 PostgresNext.js (Turbopack)0.056s1.011s0.955s101.91x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.261s (+10.9% 🔺)1.803s (-15.6% 🟢)1.542s101.00x
▲ VercelNitro0.528s (+28.8% 🔺)2.064s (-17.8% 🟢)1.536s102.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.071s (-4.8%)2.006s (~)0.935s101.00x
💻 LocalNitro1.077s (-4.8%)2.007s (~)0.930s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.928s101.01x
🐘 PostgresNitro1.083s (-5.0% 🟢)2.009s (~)0.926s101.01x
🐘 PostgresNext.js (Turbopack)1.119s2.009s0.890s101.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.549s (-60.2% 🟢)3.213s (-45.6% 🟢)1.664s101.00x
▲ VercelExpress1.645s (-12.3% 🟢)3.770s (-1.0%)2.125s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.395s (-5.2% 🟢)11.019s (~)0.623s31.00x
🐘 PostgresNitro10.400s (-4.3%)11.015s (~)0.616s31.00x
💻 LocalExpress10.402s (-4.8%)11.021s (~)0.618s31.00x
💻 LocalNitro10.425s (-4.8%)11.023s (~)0.597s31.00x
🐘 PostgresNext.js (Turbopack)10.773s11.019s0.246s31.04x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.421s (-43.4% 🟢)16.060s (-36.1% 🟢)2.639s21.00x
▲ VercelExpress13.959s (-17.8% 🟢)16.216s (-19.0% 🟢)2.258s21.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.429s (-10.3% 🟢)14.027s (-6.7% 🟢)0.598s51.00x
🐘 PostgresExpress13.451s (-7.8% 🟢)14.018s (-6.7% 🟢)0.567s51.00x
🐘 PostgresNitro13.462s (-7.8% 🟢)14.016s (-6.7% 🟢)0.554s51.00x
💻 LocalNitro13.514s (-10.3% 🟢)14.027s (-12.5% 🟢)0.513s51.01x
🐘 PostgresNext.js (Turbopack)14.231s15.022s0.790s41.06x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.060s (-62.7% 🟢)25.631s (-61.5% 🟢)1.571s31.00x
▲ VercelExpress25.009s (-50.3% 🟢)26.850s (-48.9% 🟢)1.841s31.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.858s (-28.6% 🟢)12.022s (-29.4% 🟢)0.164s81.00x
🐘 PostgresExpress11.958s (-14.6% 🟢)12.391s (-15.1% 🟢)0.433s81.01x
💻 LocalNitro11.962s (-28.7% 🟢)12.273s (-27.9% 🟢)0.310s81.01x
🐘 PostgresNitro11.964s (-14.3% 🟢)12.392s (-13.4% 🟢)0.427s81.01x
🐘 PostgresNext.js (Turbopack)13.444s14.162s0.718s71.13x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.789s (-92.2% 🟢)34.820s (-91.8% 🟢)2.031s31.00x
▲ VercelExpress34.696s (-71.4% 🟢)37.071s (-70.0% 🟢)2.375s31.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.145s (-10.1% 🟢)2.008s (~)0.862s151.00x
🐘 PostgresExpress1.147s (-9.0% 🟢)2.007s (~)0.860s151.00x
💻 LocalExpress1.172s (-21.3% 🟢)2.006s (~)0.834s151.02x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.827s151.03x
🐘 PostgresNext.js (Turbopack)1.235s2.008s0.773s151.08x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.851s (+1.2%)4.530s (+4.8%)1.679s71.00x
▲ VercelExpress3.267s (+14.2% 🔺)4.731s (+2.3%)1.464s81.15x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (-49.3% 🟢)2.007s (-33.3% 🟢)0.815s151.00x
🐘 PostgresExpress1.205s (-49.0% 🟢)2.007s (-33.3% 🟢)0.803s151.01x
🐘 PostgresNext.js (Turbopack)1.379s2.007s0.628s151.16x
💻 LocalExpress1.684s (-43.0% 🟢)2.005s (-41.9% 🟢)0.322s151.41x
💻 LocalNitro1.828s (-41.8% 🟢)2.074s (-46.6% 🟢)0.245s151.53x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.575s (+26.4% 🔺)6.287s (+23.0% 🔺)1.712s51.00x
▲ VercelNitro4.698s (+15.9% 🔺)6.626s (+11.9% 🔺)1.928s51.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-62.5% 🟢)2.007s (-50.0% 🟢)0.701s151.00x
🐘 PostgresNitro1.316s (-62.2% 🟢)2.007s (-49.9% 🟢)0.692s151.01x
🐘 PostgresNext.js (Turbopack)1.610s2.006s0.397s151.23x
💻 LocalExpress4.878s (-41.5% 🟢)5.344s (-40.8% 🟢)0.467s63.74x
💻 LocalNitro5.661s (-32.2% 🟢)6.414s (-28.9% 🟢)0.753s54.34x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.010s (+41.8% 🔺)7.543s (+23.1% 🔺)1.532s41.00x
▲ VercelNitro6.148s (+74.4% 🔺)8.083s (+46.1% 🔺)1.935s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.2% 🟢)2.007s (~)0.866s151.00x
🐘 PostgresNitro1.152s (-8.4% 🟢)2.009s (~)0.857s151.01x
🐘 PostgresNext.js (Turbopack)1.200s2.008s0.809s151.05x
💻 LocalExpress1.394s (-26.4% 🟢)2.006s (-15.2% 🟢)0.611s151.22x
💻 LocalNitro1.408s (-24.5% 🟢)2.006s (-14.3% 🟢)0.599s151.23x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.675s (+8.8% 🔺)4.409s (+5.7% 🔺)1.734s71.00x
▲ VercelExpress3.059s (+18.5% 🔺)4.918s (+13.1% 🔺)1.859s71.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.192s (-49.1% 🟢)2.009s (-33.3% 🟢)0.817s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.008s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.328s2.007s0.679s151.11x
💻 LocalExpress1.890s (-39.7% 🟢)2.294s (-39.0% 🟢)0.404s141.59x
💻 LocalNitro1.965s (-35.9% 🟢)2.392s (-38.4% 🟢)0.427s131.65x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.967s (+22.7% 🔺)5.871s (+15.6% 🔺)1.904s61.00x
▲ VercelExpress4.280s (+34.1% 🔺)5.865s (+22.4% 🔺)1.585s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.292s (-63.1% 🟢)2.009s (-49.9% 🟢)0.717s151.00x
🐘 PostgresNitro1.327s (-61.9% 🟢)2.008s (-49.9% 🟢)0.682s151.03x
🐘 PostgresNext.js (Turbopack)1.601s2.008s0.408s151.24x
💻 LocalExpress5.090s (-42.2% 🟢)5.683s (-38.7% 🟢)0.593s63.94x
💻 LocalNitro5.742s (-37.2% 🟢)6.217s (-38.0% 🟢)0.475s54.44x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.023s (+37.9% 🔺)8.853s (+29.9% 🔺)1.829s41.00x
▲ VercelExpress82.204s (+1181.0% 🔺)84.045s (+927.7% 🔺)1.842s411.70x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.416s (-50.4% 🟢)1.007s (-1.6%)0.590s601.00x
💻 LocalExpress0.460s (-53.2% 🟢)1.004s (-6.7% 🟢)0.544s601.11x
🐘 PostgresNitro0.462s (-43.7% 🟢)1.007s (~)0.545s601.11x
💻 LocalNitro0.488s (-50.2% 🟢)1.004s (-8.2% 🟢)0.516s601.17x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.331s601.62x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.870s (-74.4% 🟢)6.522s (-69.4% 🟢)1.652s101.00x
▲ VercelNitro5.150s (-76.7% 🟢)6.879s (-71.4% 🟢)1.730s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.031s (-47.8% 🟢)1.451s (-35.8% 🟢)0.420s631.00x
🐘 PostgresNitro1.070s (-44.5% 🟢)1.674s (-20.3% 🟢)0.604s541.04x
💻 LocalExpress1.176s (-61.0% 🟢)2.005s (-44.1% 🟢)0.829s451.14x
💻 LocalNitro1.188s (-60.9% 🟢)2.006s (-46.6% 🟢)0.818s451.15x
🐘 PostgresNext.js (Turbopack)1.646s2.007s0.361s451.60x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro12.985s (-67.1% 🟢)15.133s (-63.4% 🟢)2.147s61.00x
▲ VercelExpress13.350s (-61.3% 🟢)15.228s (-58.6% 🟢)1.878s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.977s (-50.4% 🟢)2.399s (-45.1% 🟢)0.422s511.00x
🐘 PostgresNitro2.194s (-46.5% 🟢)2.799s (-39.2% 🟢)0.605s431.11x
💻 LocalExpress2.644s (-71.3% 🟢)3.008s (-70.0% 🟢)0.363s401.34x
💻 LocalNitro2.699s (-71.0% 🟢)3.008s (-70.0% 🟢)0.309s401.36x
🐘 PostgresNext.js (Turbopack)3.220s4.009s0.789s301.63x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express39.088s (-69.9% 🟢)41.504s (-68.6% 🟢)2.416s31.00x
▲ VercelNitro40.857s (-57.8% 🟢)43.359s (-55.9% 🟢)2.502s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.163s (-42.3% 🟢)1.006s (~)0.843s601.00x
🐘 PostgresNitro0.168s (-40.6% 🟢)1.006s (~)0.838s601.03x
🐘 PostgresNext.js (Turbopack)0.231s1.006s0.775s601.42x
💻 LocalExpress0.387s (-31.0% 🟢)1.004s (~)0.617s602.37x
💻 LocalNitro0.414s (-31.6% 🟢)1.004s (-1.7%)0.591s602.54x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.150s (+10.0% 🔺)3.915s (+7.6% 🔺)1.764s161.00x
▲ VercelNitro2.540s (+53.0% 🔺)4.298s (+28.3% 🔺)1.758s141.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.289s (-43.3% 🟢)1.006s (~)0.717s901.00x
🐘 PostgresNitro0.300s (-39.5% 🟢)1.006s (~)0.705s901.04x
🐘 PostgresNext.js (Turbopack)0.454s1.006s0.552s901.57x
💻 LocalExpress2.105s (-16.2% 🟢)2.580s (-14.3% 🟢)0.475s357.28x
💻 LocalNitro2.140s (-15.7% 🟢)2.685s (-10.8% 🟢)0.546s347.40x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.144s (+200.1% 🔺)10.945s (+127.7% 🔺)1.802s91.00x
▲ VercelNitro9.476s (+193.8% 🔺)11.309s (+134.5% 🔺)1.832s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-26.4% 🟢)1.006s (-1.1%)0.403s1201.00x
🐘 PostgresNitro0.629s (-20.4% 🟢)1.006s (~)0.378s1201.04x
🐘 PostgresNext.js (Turbopack)0.923s1.195s0.272s1011.53x
💻 LocalExpress9.702s (-13.3% 🟢)10.279s (-13.9% 🟢)0.577s1216.10x
💻 LocalNitro10.229s (-8.6% 🟢)10.862s (-6.9% 🟢)0.633s1216.98x
💻 LocalNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express23.814s (+220.9% 🔺)25.940s (+180.6% 🔺)2.126s51.00x
▲ VercelNitro24.081s (+211.8% 🔺)26.239s (+179.1% 🔺)2.158s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+468.2% 🔺)2.005s (+99.6% 🔺)0.010s (-15.7% 🟢)2.017s (+98.2% 🔺)0.886s101.00x
💻 LocalNitro1.135s (+431.1% 🔺)2.005s (+99.6% 🔺)0.012s (-3.2%)2.020s (+98.2% 🔺)0.885s101.00x
🐘 PostgresNitro1.141s (+456.4% 🔺)1.995s (+99.5% 🔺)0.001s (-26.7% 🟢)2.010s (+98.8% 🔺)0.870s101.01x
🐘 PostgresExpress1.148s (+459.5% 🔺)1.998s (+100.1% 🔺)0.001s (-25.0% 🟢)2.010s (+98.7% 🔺)0.862s101.01x
🐘 PostgresNext.js (Turbopack)1.231s2.002s0.001s2.011s0.780s101.09x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.389s (-4.6%)3.696s (-9.6% 🟢)2.437s (+153.6% 🔺)6.614s (+18.3% 🔺)4.225s101.00x
▲ VercelNitro2.397s (-37.4% 🟢)3.486s (-33.9% 🟢)2.169s (+192.3% 🔺)6.161s (-5.0%)3.764s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.511s (+139.9% 🔺)2.006s (+99.3% 🔺)0.004s (+7.9% 🔺)2.029s (+98.3% 🔺)0.518s301.00x
💻 LocalExpress1.512s (+99.8% 🔺)2.012s (+95.6% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.512s301.00x
💻 LocalNitro1.547s (+84.4% 🔺)2.010s (+98.6% 🔺)0.011s (+13.1% 🔺)2.023s (+81.3% 🔺)0.476s301.02x
🐘 PostgresNitro1.585s (+153.9% 🔺)2.006s (+99.2% 🔺)0.004s (-2.5%)2.026s (+98.2% 🔺)0.441s301.05x
🐘 PostgresNext.js (Turbopack)1.714s2.011s0.003s2.024s0.310s301.13x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.227s (-78.8% 🟢)7.746s (-74.9% 🟢)0.186s (+66.0% 🔺)8.422s (-73.5% 🟢)2.195s81.00x
▲ VercelExpress6.324s (-2.8%)7.707s (-3.8%)0.198s (-51.5% 🟢)8.368s (-5.3% 🟢)2.044s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.655s (-31.8% 🟢)1.030s (-19.4% 🟢)0.000s (-60.3% 🟢)1.044s (-20.0% 🟢)0.389s581.00x
🐘 PostgresNitro0.697s (-28.1% 🟢)1.032s (-17.3% 🟢)0.000s (-15.8% 🟢)1.063s (-15.4% 🟢)0.366s571.06x
🐘 PostgresNext.js (Turbopack)0.782s1.071s0.000s1.079s0.297s571.19x
💻 LocalExpress1.327s (+8.3% 🔺)2.015s (~)0.000s (-70.0% 🟢)2.017s (~)0.690s302.02x
💻 LocalNitro1.366s (+11.8% 🔺)2.015s (~)0.000s (+100.0% 🔺)2.017s (~)0.650s302.08x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.472s (+13.8% 🔺)4.600s (+4.7%)0.000s (-100.0% 🟢)5.118s (+6.4% 🔺)1.647s121.00x
▲ VercelExpress4.075s (+9.0% 🔺)5.246s (+2.8%)0.000s (~)5.729s (+3.6%)1.654s111.17x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.290s (-28.0% 🟢)2.099s (-2.0%)0.000s (-100.0% 🟢)2.130s (-2.0%)0.840s291.00x
🐘 PostgresExpress1.305s (-26.4% 🟢)1.998s (-8.3% 🟢)0.000s (+Infinity% 🔺)2.009s (-8.6% 🟢)0.704s301.01x
🐘 PostgresNext.js (Turbopack)1.580s2.111s0.000s2.178s0.598s281.22x
💻 LocalExpress3.066s (-11.6% 🟢)3.734s (-7.4% 🟢)0.000s (-70.6% 🟢)3.737s (-7.4% 🟢)0.672s172.38x
💻 LocalNitro3.172s (-6.4% 🟢)3.901s (-3.2%)0.001s (+28.9% 🔺)3.907s (-3.2%)0.735s162.46x
💻 LocalNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.236s (+27.9% 🔺)6.713s (+24.9% 🔺)0.000s (-100.0% 🟢)7.182s (+24.0% 🔺)1.946s91.00x
▲ VercelExpress6.094s (+32.8% 🔺)7.327s (+21.7% 🔺)0.000s (NaN%)7.834s (+21.3% 🔺)1.740s81.16x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress21/21
🐘 PostgresExpress17/21
▲ VercelNitro12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres21/21
Nitro🐘 Postgres16/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.

@pranaygp
pranaygpforce-pushed the pranaygp/codex/runtime-schema-validation-failure branch from 744b7a6 to 16a2f7bCompareMay 21, 2026 19:14
@pranaygppranaygp changed the title [codex] Capture schema validation as runtime failure[codex] Capture world schema validation as fatalMay 21, 2026

@TooTallNateTooTallNate left a comment

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.

Approve — sound fix for a real production problem

The bug is real and the fix is targeted: when world-vercel returns a malformed run_started response, the runtime catches WorkflowWorldError with a schema validation message/Zod cause, records run_failed with the new WORLD_CONTRACT_ERROR code, and returns 204 to ack the queue message. No more redelivery loops to max deliveries on a non-retryable failure.

The shape mirrors the just-merged #2046 (CORRUPTED_EVENT_LOG): new code in error-codes.ts, classification in the appropriate runtime catch block, description coverage in describe-error.ts. Clean separation.

What I verified

  • pnpm turbo build --filter @workflow/core
  • pnpm vitest run runtime.test.ts describe-error.test.ts classify-error.test.ts ✓ (974 tests pass including the new regression)
  • Traced the WorkflowWorldError throw sites in world-vercel/src/utils.ts: the new isWorldSchemaValidationError helper correctly distinguishes schema validation (status=undefined + matching message/cause) from transient failures (timeouts, HTTP errors with status set).
  • The narrow detection (message.startsWith('Schema validation failed for ') || cause.name === 'ZodError') does NOT match timeouts (which also have status === undefined but a different message) — so we don't accidentally treat retryable failures as fatal.
  • CI: 34 success, 1 failure (E2E Local Prod Tests (nextjs-turbopack - canary) — unrelated; main is also failing it), 75 in-progress.

Five things worth thinking about (none are blockers)

1. Narrow scope: schema validation only, not parse-body failures

world-vercel/src/utils.ts throws WorkflowWorldError with status === undefined in two non-retryable cases:

// utils.ts:424 — body parse errorthrownewWorkflowWorldError(`Failed to parse response body for ${request.method}${endpoint}...`,{ url,cause: error});// utils.ts:443 — schema validation error (caught by this PR)thrownewWorkflowWorldError(`Schema validation failed for ${method}${endpoint}...`,{ url,cause: validationResult.error});

Both are equally non-retryable (the server is sending malformed data; retrying doesn't help). The PR catches only the second. The body-parse case will still loop to max deliveries.

Worth either expanding the detection to include 'Failed to parse response body for ', or filing a follow-up to extend.

2. No WorldContractError class — asymmetric with CorruptedEventLogError

PR #2046 introduced a dedicated CorruptedEventLogError class. This PR adds the code but no class. The asymmetry has one practical consequence: describeError(liveError) cannot recognize a world-contract failure from a live WorkflowWorldError instance — only describeRunError({errorCode}) can detect it post-persistence.

In the current code path that's fine (the runtime translates to the code at emit time), but a class would make describeError symmetric and would localize the message-prefix matching to one place. Could be a follow-up.

3. getWorkflowSetupErrorCode uses instanceof, not name-based checks

functiongetWorkflowSetupErrorCode(err: unknown): RunErrorCode|null{if(errinstanceofWorkflowRuntimeError){returnRUN_ERROR_CODES.RUNTIME_ERROR;}if(isWorldSchemaValidationError(err)){returnRUN_ERROR_CODES.WORLD_CONTRACT_ERROR;}returnnull;}

Compare to classifyRunError, which checks CorruptedEventLogError.is(err)first before falling through to WorkflowRuntimeError (because CorruptedEventLogError extends WorkflowRuntimeError, an instanceof check on the parent would mask the more specific class).

For the setup block today this doesn't matter because CorruptedEventLogError isn't thrown from setup paths — it's thrown from step.ts/hook.ts/sleep.ts/abort-controller.ts during replay, after setup completes. So the regression is theoretical. But if someone in the future throws CorruptedEventLogError from setup, they'd silently get RUNTIME_ERROR instead of CORRUPTED_EVENT_LOG.

Easiest fix: check CorruptedEventLogError.is(err) first (consistent with classifyRunError).

4. Setup-only scope

Schema validation can happen on ANY world call — events.list, events.create for wait_completed, etc. Those failures currently still loop to max deliveries. Worth a follow-up to broaden the fatal-error handling, or at least note the scope limitation in the PR body.

5. Code name WORLD_CONTRACT_ERROR vs detection logic

The code name is broad ("contract" covers schema + parse + other client-side world errors). The detection only catches schema validation. The name is slightly aspirational vs. the implementation.

That's OK — better to introduce a forward-looking code now than have to introduce another code later. But worth a code comment like // Today only detects schema validation; extend isWorldSchemaValidationError to cover other contract violations as they're discovered.

Backport stance

Not addressed in PR body. This is a bug fix touching error classification and runtime control flow — a candidate for backport to stable per AGENTS.md ("Bug fixes to existing functionality that at least partially exists on stable"). The backport action's AI should pick this up automatically; worth checking after merge.

Approving.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: posting inline comments from a regression-focused pass. Nothing I'd block on — five follow-ups to consider.

Comment threadpackages/core/src/runtime.ts Outdated
runtimeLogger.error(
'Fatal runtime error during workflow setup',
{ workflowRunId: runId, error: err.message }
'Fatal error during workflow setup',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the log message rename from 'Fatal runtime error during workflow setup''Fatal error during workflow setup' is a silent observability regression. Any external alert or dashboard keyed on the old literal string will stop firing without warning. Low blast radius, but worth a quick check that nothing in datadog/elastic/etc. matches on the old wording before this lands.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Restored the original Fatal runtime error during workflow setup literal while preserving the errorCode field for distinguishing WORLD_CONTRACT_ERROR from regular runtime setup failures.

Comment threadpackages/core/src/runtime.ts Outdated

const cause = 'cause' in err ? err.cause : undefined;
return (
err.message.startsWith('Schema validation failed for ') ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: this string-prefix check is tightly coupled to the throw site at packages/world-vercel/src/utils.ts:444. If that message is ever reworded, only the ZodError-cause fallback below remains, and detection silently degrades. A sturdier shape would be a typed marker — e.g. a dedicated WorldSchemaValidationError subclass of WorkflowWorldError, or a code: 'SCHEMA_VALIDATION' field on the existing class — so the runtime detects it structurally rather than by message prefix. Not blocking.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done. The runtime no longer depends only on the string prefix for the primary world-vercel path: schema failures now carry code: "SCHEMA_VALIDATION", parse failures carry code: "PARSE_ERROR", and the string/cause checks remain as compatibility fallbacks.

return;
} else if (err instanceof WorkflowRuntimeError) {
} else {
const errorCode = getWorkflowSetupErrorCode(err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the fix is scoped to the run_started setup path, but the same WorkflowWorldError schema-validation failure can come out of other world calls in this handler — events.list, runs.get, and subsequent events.create calls during replay. Those still bubble up and redrive the queue to max deliveries, which is the same bug class. Worth a sentence in the PR description acknowledging the scope, or a follow-up issue to extend the same treatment to the other call sites.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Broadened the handling in a85d52c: world contract errors now classify through the normal run failure path, so events.list and later replay events.create failures record run_failed with WORLD_CONTRACT_ERROR; the background-step prep path also records the fatal run error instead of redriving. Added an events.list regression test.

Comment threadpackages/core/src/runtime.ts Outdated
setWorld,
} from './runtime/world.js';

function isWorldSchemaValidationError(err: unknown): err is WorkflowWorldError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: the sibling throw at packages/world-vercel/src/utils.ts:424 (Failed to parse response body for ...) has the same shape as schema validation — WorkflowWorldError with no status, non-retryable in practice (the response body is malformed CBOR/JSON, retrying won't help) — but isn't caught here because the cause is a JSON/CBOR parse error rather than a ZodError. If we're declaring "world contract violation" as a fatal category, parse failures probably belong in it too. Worth deciding explicitly whether to include them now or in a follow-up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Included this now. world-vercel marks malformed response bodies with WorkflowWorldError.code = "PARSE_ERROR", and core treats that as WORLD_CONTRACT_ERROR alongside schema validation. Added a run-start parse failure regression test.

/** Workflow replay exceeded the maximum allowed duration */
REPLAY_TIMEOUT: 'REPLAY_TIMEOUT',
/** World response violated the SDK contract and cannot be retried safely */
WORLD_CONTRACT_ERROR: 'WORLD_CONTRACT_ERROR',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: classifyRunError in packages/core/src/classify-error.ts isn't updated to recognize WORLD_CONTRACT_ERROR. The describe-error path is fine in this PR because the runtime passes the explicit code, but if a schema-validation WorkflowWorldError ever flows through classifyRunError from outside the setup path, it'll be classified as USER_ERROR (wrong attribution). Symmetry would be nice — adding a WorkflowWorldError.is-style check there that returns WORLD_CONTRACT_ERROR when the schema-validation heuristic matches.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done in a85d52c: classifyRunError now uses a shared isWorldContractError helper and returns WORLD_CONTRACT_ERROR for schema/parse world contract failures. Added classifier coverage for both shapes.

const MAX_DELIVERIES_HINT =
'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.';
const WORLD_CONTRACT_HINT =
'The workflow backend returned data that violated the SDK contract. This is not retryable; please report it with the stack trace and runId.';

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

"This usually indicates a version incompatibility bug"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. PR #2060 is already merged, so I cannot update this branch in-place anymore; this should be a small follow-up wording cleanup to change the world-contract hint toward “This usually indicates a version incompatibility bug”.

@github-actionsgithub-actionsBot mentioned this pull request May 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2064. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

4 participants

@pranaygp@TooTallNate@karthikscale3