Skip to content

Serialize run_failed/step_failed errors through serialization pipeline#1851

Merged
TooTallNate merged 16 commits into
mainfrom
nrajlich/step-error-serialization
May 4, 2026
Merged

Serialize run_failed/step_failed errors through serialization pipeline#1851
TooTallNate merged 16 commits into
mainfrom
nrajlich/step-error-serialization

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented Apr 24, 2026

Summary

Switch run_failed, step_failed, and step_retrying events to persist the full thrown value through the workflow serialization pipeline (as SerializedData / Uint8Array) instead of a lossy { message, stack, code } StructuredError shape. Consumers hydrate via hydrateRunError / hydrateStepError to reconstruct the original thrown value — preserving Error subclass identity, cause chains, and custom properties (e.g. FatalError.fatal, RetryableError.retryAfter).

Breaking changes

  • WorkflowRun.error and Step.error are now SerializedData (Uint8Array) instead of { message, stack?, code? }. Consumers must hydrate via hydrateRunError / hydrateStepError.
  • WorkflowRun gains a top-level errorCode field carrying the previous error.code value as plaintext metadata.
  • WorkflowRunFailedError.cause is now unknown (the hydrated thrown value with its original type identity preserved) instead of a synthesized Error. A new errorCode property exposes the error classification.
  • Event payload for step_failed, step_retrying, and run_failed now contains error: SerializedData.
  • Adds @workflow/world-postgres migration 0010_add_error_code.sql (new error_code column on workflow.workflow_runs).
  • Legacy pre-pipeline records in the deprecated error text column are surfaced as undefined on read (they cannot be hydrated into the original thrown value).

Stack

Notes

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

🦋 Changeset detected

Latest commit: 3025d13

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

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

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 24, 2026

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 903 0 219 1122
✅ 💻 Local Development 1209 0 219 1428
✅ 📦 Local Production 1209 0 219 1428
✅ 🐘 Local Postgres 1209 0 219 1428
✅ 🪟 Windows 102 0 0 102
✅ 📋 Other 538 0 176 714
Total 5170 0 1052 6222

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 76 0 26
✅ example 76 0 26
✅ express 76 0 26
✅ fastify 76 0 26
✅ hono 76 0 26
✅ nextjs-turbopack 100 0 2
✅ nextjs-webpack 100 0 2
✅ nitro 76 0 26
✅ nuxt 76 0 26
✅ sveltekit 95 0 7
✅ vite 76 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 77 0 25
✅ express-stable 77 0 25
✅ fastify-stable 77 0 25
✅ hono-stable 77 0 25
✅ nextjs-turbopack-canary 83 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 102 0 0
✅ nextjs-webpack-canary 83 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 102 0 0
✅ nitro-stable 77 0 25
✅ nuxt-stable 77 0 25
✅ sveltekit-stable 96 0 6
✅ vite-stable 77 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 77 0 25
✅ express-stable 77 0 25
✅ fastify-stable 77 0 25
✅ hono-stable 77 0 25
✅ nextjs-turbopack-canary 83 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 102 0 0
✅ nextjs-webpack-canary 83 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 102 0 0
✅ nitro-stable 77 0 25
✅ nuxt-stable 77 0 25
✅ sveltekit-stable 96 0 6
✅ vite-stable 77 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 77 0 25
✅ express-stable 77 0 25
✅ fastify-stable 77 0 25
✅ hono-stable 77 0 25
✅ nextjs-turbopack-canary 83 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 102 0 0
✅ nextjs-webpack-canary 83 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 102 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 102 0 0
✅ nitro-stable 77 0 25
✅ nuxt-stable 77 0 25
✅ sveltekit-stable 96 0 6
✅ vite-stable 77 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 102 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 77 0 25
✅ e2e-local-dev-tanstack-start- 77 0 25
✅ e2e-local-postgres-nest-stable 77 0 25
✅ e2e-local-postgres-tanstack-start- 77 0 25
✅ e2e-local-prod-nest-stable 77 0 25
✅ e2e-local-prod-tanstack-start- 77 0 25
✅ e2e-vercel-prod-tanstack-start 76 0 26

📋 View full workflow run

Copy link
Copy Markdown
Contributor

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

TypeScript build fails because WorkflowRunFailedError.cause was changed to unknown but consumer code accesses .message, .stack, and .code on it without type narrowing.

Fix on Vercel

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.033s (-23.4% 🟢) 1.005s (~) 0.972s 10 1.00x
💻 Local Express 0.034s (-23.0% 🟢) 1.006s (~) 0.972s 10 1.03x
🐘 Postgres Express 0.048s (-17.9% 🟢) 1.012s (~) 0.964s 10 1.44x
💻 Local Next.js (Turbopack) 0.049s 1.005s 0.956s 10 1.49x
🐘 Postgres Nitro 0.050s (-47.5% 🟢) 1.012s (-2.9%) 0.962s 10 1.52x
🌐 Redis Next.js (Turbopack) 0.057s 1.005s 0.948s 10 1.74x
🐘 Postgres Next.js (Turbopack) 0.058s 1.011s 0.954s 10 1.75x
🌐 MongoDB Next.js (Turbopack) 0.135s 1.007s 0.872s 10 4.09x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.283s (+20.1% 🔺) 2.206s (+3.3%) 1.923s 10 1.00x
▲ Vercel Next.js (Turbopack) 1.002s (+298.2% 🔺) 2.718s (+16.5% 🔺) 1.717s 10 3.54x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.068s (-5.1% 🟢) 2.006s (~) 0.938s 10 1.00x
💻 Local Nitro 1.069s (-5.5% 🟢) 2.006s (~) 0.937s 10 1.00x
🐘 Postgres Nitro 1.082s (-5.1% 🟢) 2.009s (~) 0.927s 10 1.01x
🐘 Postgres Express 1.088s (-5.1% 🟢) 2.010s (~) 0.922s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.113s 2.009s 0.897s 10 1.04x
🌐 Redis Next.js (Turbopack) 1.115s 2.006s 0.892s 10 1.04x
💻 Local Next.js (Turbopack) 1.117s 2.006s 0.888s 10 1.05x
🌐 MongoDB Next.js (Turbopack) 1.171s 2.008s 0.837s 10 1.10x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.546s (-17.5% 🟢) 3.785s (-0.6%) 2.238s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.926s (+43.8% 🔺) 4.251s (+11.0% 🔺) 1.325s 10 1.89x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.399s (-5.0% 🟢) 11.021s (~) 0.623s 3 1.00x
💻 Local Express 10.406s (-4.7%) 11.021s (~) 0.615s 3 1.00x
🐘 Postgres Nitro 10.409s (-4.3%) 11.016s (~) 0.608s 3 1.00x
🐘 Postgres Express 10.439s (-4.8%) 11.017s (~) 0.578s 3 1.00x
💻 Local Next.js (Turbopack) 10.676s 11.024s 0.348s 3 1.03x
🌐 Redis Next.js (Turbopack) 10.703s 11.024s 0.321s 3 1.03x
🐘 Postgres Next.js (Turbopack) 10.718s 11.018s 0.301s 3 1.03x
🌐 MongoDB Next.js (Turbopack) 10.741s 11.021s 0.281s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.730s (-19.1% 🟢) 15.796s (-21.1% 🟢) 2.065s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.377s (-17.0% 🟢) 15.987s (-17.6% 🟢) 1.611s 2 1.05x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.405s (-11.0% 🟢) 14.026s (-12.5% 🟢) 0.621s 5 1.00x
🐘 Postgres Nitro 13.466s (-7.7% 🟢) 14.018s (-6.7% 🟢) 0.553s 5 1.00x
💻 Local Express 13.478s (-10.0% 🟢) 14.027s (-6.7% 🟢) 0.549s 5 1.01x
🐘 Postgres Express 13.483s (-7.5% 🟢) 14.020s (-6.7% 🟢) 0.537s 5 1.01x
💻 Local Next.js (Turbopack) 14.059s 15.029s 0.970s 4 1.05x
🌐 Redis Next.js (Turbopack) 14.110s 15.030s 0.920s 4 1.05x
🐘 Postgres Next.js (Turbopack) 14.140s 15.015s 0.875s 4 1.05x
🌐 MongoDB Next.js (Turbopack) 14.292s 15.022s 0.730s 4 1.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 21.809s (-56.6% 🟢) 23.936s (-54.5% 🟢) 2.127s 3 1.00x
▲ Vercel Next.js (Turbopack) 23.887s (-54.6% 🟢) 26.043s (-52.3% 🟢) 2.155s 3 1.10x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.788s (-29.8% 🟢) 12.022s (-29.4% 🟢) 0.233s 8 1.00x
💻 Local Express 11.902s (-28.3% 🟢) 12.147s (-28.7% 🟢) 0.246s 8 1.01x
🐘 Postgres Nitro 11.997s (-14.1% 🟢) 12.391s (-13.4% 🟢) 0.394s 8 1.02x
🐘 Postgres Express 12.012s (-14.2% 🟢) 12.520s (-14.2% 🟢) 0.508s 8 1.02x
💻 Local Next.js (Turbopack) 12.993s 13.165s 0.172s 7 1.10x
🌐 Redis Next.js (Turbopack) 13.051s 14.028s 0.976s 7 1.11x
🌐 MongoDB Next.js (Turbopack) 13.260s 14.019s 0.759s 7 1.12x
🐘 Postgres Next.js (Turbopack) 13.357s 14.018s 0.661s 7 1.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 33.873s (-72.1% 🟢) 35.695s (-71.1% 🟢) 1.821s 3 1.00x
▲ Vercel Next.js (Turbopack) 34.945s (-91.1% 🟢) 36.193s (-90.8% 🟢) 1.248s 3 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.146s (-10.1% 🟢) 2.007s (~) 0.861s 15 1.00x
🐘 Postgres Express 1.164s (-7.7% 🟢) 2.008s (~) 0.844s 15 1.02x
💻 Local Nitro 1.170s (-28.3% 🟢) 2.005s (-3.3%) 0.835s 15 1.02x
💻 Local Express 1.180s (-20.8% 🟢) 2.006s (~) 0.826s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.211s 2.007s 0.795s 15 1.06x
🌐 Redis Next.js (Turbopack) 1.262s 2.007s 0.744s 15 1.10x
💻 Local Next.js (Turbopack) 1.311s 2.006s 0.695s 15 1.14x
🌐 MongoDB Next.js (Turbopack) 2.026s 2.735s 0.709s 11 1.77x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.461s (-14.0% 🟢) 4.013s (-13.2% 🟢) 1.553s 8 1.00x
▲ Vercel Next.js (Turbopack) 5.938s (+74.7% 🔺) 7.755s (+57.2% 🔺) 1.818s 4 2.41x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.231s (-47.7% 🟢) 2.007s (-33.3% 🟢) 0.777s 15 1.00x
🐘 Postgres Express 1.249s (-47.1% 🟢) 2.007s (-33.3% 🟢) 0.758s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.381s 2.009s 0.628s 15 1.12x
💻 Local Nitro 1.682s (-46.5% 🟢) 2.005s (-48.4% 🟢) 0.323s 15 1.37x
💻 Local Express 1.716s (-41.9% 🟢) 2.006s (-41.9% 🟢) 0.290s 15 1.39x
💻 Local Next.js (Turbopack) 1.867s 2.074s 0.206s 15 1.52x
🌐 Redis Next.js (Turbopack) 2.390s 3.009s 0.619s 10 1.94x
🌐 MongoDB Next.js (Turbopack) 3.570s 4.009s 0.439s 8 2.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.261s (-9.9% 🟢) 5.175s (+1.3%) 1.913s 6 1.00x
▲ Vercel Next.js (Turbopack) 5.559s (-21.7% 🟢) 7.542s (-15.3% 🟢) 1.984s 4 1.70x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.374s (-60.5% 🟢) 2.009s (-49.9% 🟢) 0.635s 15 1.00x
🐘 Postgres Express 1.388s (-60.2% 🟢) 2.008s (-49.9% 🟢) 0.620s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.694s 2.009s 0.315s 15 1.23x
🌐 Redis Next.js (Turbopack) 3.692s 4.010s 0.319s 8 2.69x
💻 Local Nitro 4.560s (-45.4% 🟢) 5.011s (-44.5% 🟢) 0.451s 7 3.32x
💻 Local Express 4.809s (-42.3% 🟢) 5.345s (-40.8% 🟢) 0.535s 6 3.50x
💻 Local Next.js (Turbopack) 5.899s 6.416s 0.517s 5 4.29x
🌐 MongoDB Next.js (Turbopack) 6.271s 7.011s 0.740s 5 4.56x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.314s (-29.2% 🟢) 8.057s (-26.5% 🟢) 1.744s 4 1.00x
▲ Vercel Express 7.976s (+88.1% 🔺) 10.174s (+66.1% 🔺) 2.199s 3 1.26x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.165s (-7.4% 🟢) 2.008s (~) 0.843s 15 1.00x
🐘 Postgres Nitro 1.169s (-7.0% 🟢) 2.007s (~) 0.839s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.219s 2.008s 0.789s 15 1.05x
🌐 Redis Next.js (Turbopack) 1.233s 2.007s 0.774s 15 1.06x
💻 Local Next.js (Turbopack) 1.306s 2.005s 0.699s 15 1.12x
💻 Local Nitro 1.369s (-26.6% 🟢) 2.006s (-14.3% 🟢) 0.637s 15 1.18x
💻 Local Express 1.386s (-26.8% 🟢) 2.006s (-15.1% 🟢) 0.621s 15 1.19x
🌐 MongoDB Next.js (Turbopack) 2.037s 2.826s 0.789s 11 1.75x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.269s (-12.1% 🟢) 3.916s (-10.0% 🟢) 1.647s 8 1.00x
▲ Vercel Next.js (Turbopack) 5.041s (+72.0% 🔺) 6.816s (+46.8% 🔺) 1.775s 5 2.22x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.229s (-47.5% 🟢) 2.008s (-33.3% 🟢) 0.779s 15 1.00x
🐘 Postgres Express 1.249s (-46.6% 🟢) 2.010s (-33.2% 🟢) 0.761s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.356s 2.007s 0.651s 15 1.10x
💻 Local Express 1.950s (-37.7% 🟢) 2.391s (-36.4% 🟢) 0.441s 13 1.59x
💻 Local Nitro 1.962s (-36.0% 🟢) 2.394s (-38.4% 🟢) 0.432s 13 1.60x
💻 Local Next.js (Turbopack) 2.145s 3.009s 0.864s 10 1.75x
🌐 Redis Next.js (Turbopack) 2.363s 3.009s 0.645s 10 1.92x
🌐 MongoDB Next.js (Turbopack) 3.573s 4.009s 0.436s 8 2.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.762s (+17.8% 🔺) 5.712s (+19.2% 🔺) 1.950s 6 1.00x
▲ Vercel Next.js (Turbopack) 5.090s (+62.0% 🔺) 7.086s (+56.7% 🔺) 1.996s 5 1.35x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.373s (-60.5% 🟢) 2.009s (-49.9% 🟢) 0.636s 15 1.00x
🐘 Postgres Express 1.392s (-60.2% 🟢) 2.008s (-49.9% 🟢) 0.616s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.619s 2.009s 0.389s 15 1.18x
🌐 Redis Next.js (Turbopack) 3.596s 4.010s 0.414s 8 2.62x
💻 Local Nitro 5.165s (-43.5% 🟢) 5.514s (-45.0% 🟢) 0.349s 6 3.76x
💻 Local Next.js (Turbopack) 5.597s 6.013s 0.416s 5 4.08x
💻 Local Express 5.742s (-34.8% 🟢) 6.179s (-33.4% 🟢) 0.437s 6 4.18x
🌐 MongoDB Next.js (Turbopack) 6.301s 7.012s 0.711s 5 4.59x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.239s (-18.4% 🟢) 7.347s (-10.2% 🟢) 2.108s 5 1.00x
▲ Vercel Next.js (Turbopack) 6.103s (-9.7% 🟢) 7.924s (-7.3% 🟢) 1.821s 4 1.17x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.448s (-45.4% 🟢) 1.006s (~) 0.558s 60 1.00x
💻 Local Nitro 0.456s (-53.5% 🟢) 1.004s (-8.2% 🟢) 0.547s 60 1.02x
🐘 Postgres Express 0.457s (-45.5% 🟢) 1.007s (-1.6%) 0.549s 60 1.02x
💻 Local Express 0.488s (-50.4% 🟢) 1.004s (-6.7% 🟢) 0.516s 60 1.09x
🌐 Redis Next.js (Turbopack) 0.658s 1.004s 0.346s 60 1.47x
🐘 Postgres Next.js (Turbopack) 0.678s 1.006s 0.328s 60 1.52x
💻 Local Next.js (Turbopack) 0.713s 1.004s 0.292s 60 1.59x
🌐 MongoDB Next.js (Turbopack) 0.742s 1.006s 0.263s 60 1.66x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.414s (-71.5% 🟢) 7.523s (-64.7% 🟢) 2.109s 8 1.00x
▲ Vercel Next.js (Turbopack) 7.255s (-50.0% 🟢) 9.001s (-44.0% 🟢) 1.746s 7 1.34x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.088s (-43.6% 🟢) 1.882s (-10.4% 🟢) 0.794s 48 1.00x
🐘 Postgres Express 1.123s (-43.2% 🟢) 1.944s (-13.9% 🟢) 0.821s 47 1.03x
💻 Local Nitro 1.163s (-61.7% 🟢) 2.006s (-46.6% 🟢) 0.843s 45 1.07x
💻 Local Express 1.230s (-59.2% 🟢) 2.006s (-44.1% 🟢) 0.776s 45 1.13x
🌐 Redis Next.js (Turbopack) 1.578s 2.006s 0.428s 45 1.45x
🐘 Postgres Next.js (Turbopack) 1.676s 2.008s 0.332s 45 1.54x
💻 Local Next.js (Turbopack) 1.777s 2.006s 0.229s 45 1.63x
🌐 MongoDB Next.js (Turbopack) 1.826s 2.007s 0.180s 45 1.68x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.113s (-62.0% 🟢) 15.518s (-57.8% 🟢) 2.405s 6 1.00x
▲ Vercel Next.js (Turbopack) 15.717s (-68.4% 🟢) 17.570s (-66.0% 🟢) 1.853s 6 1.20x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.082s (-49.3% 🟢) 2.617s (-43.2% 🟢) 0.535s 46 1.00x
🐘 Postgres Express 2.260s (-43.4% 🟢) 3.086s (-29.4% 🟢) 0.826s 39 1.09x
💻 Local Nitro 2.667s (-71.3% 🟢) 3.007s (-70.0% 🟢) 0.340s 40 1.28x
💻 Local Express 2.759s (-70.0% 🟢) 3.084s (-69.2% 🟢) 0.326s 39 1.33x
🌐 Redis Next.js (Turbopack) 3.166s 4.009s 0.843s 30 1.52x
🐘 Postgres Next.js (Turbopack) 3.226s 4.009s 0.783s 30 1.55x
💻 Local Next.js (Turbopack) 3.767s 4.008s 0.241s 30 1.81x
🌐 MongoDB Next.js (Turbopack) 4.149s 5.012s 0.863s 24 1.99x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 38.840s (-70.1% 🟢) 41.262s (-68.8% 🟢) 2.423s 3 1.00x
▲ Vercel Next.js (Turbopack) 50.698s (-52.7% 🟢) 53.114s (-51.2% 🟢) 2.416s 3 1.31x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.184s (-34.9% 🟢) 1.006s (~) 0.822s 60 1.00x
🐘 Postgres Express 0.186s (-34.0% 🟢) 1.006s (~) 0.820s 60 1.01x
🐘 Postgres Next.js (Turbopack) 0.244s 1.006s 0.762s 60 1.32x
🌐 Redis Next.js (Turbopack) 0.260s 1.004s 0.745s 60 1.41x
💻 Local Nitro 0.446s (-26.3% 🟢) 1.004s (-1.7%) 0.558s 60 2.42x
💻 Local Express 0.453s (-19.1% 🟢) 1.004s (~) 0.551s 60 2.46x
💻 Local Next.js (Turbopack) 0.549s 1.004s 0.455s 60 2.98x
🌐 MongoDB Next.js (Turbopack) 1.033s 1.825s 0.792s 33 5.60x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.332s (+19.3% 🔺) 4.298s (+18.2% 🔺) 1.966s 14 1.00x
▲ Vercel Next.js (Turbopack) 3.903s (+93.0% 🔺) 5.485s (+44.6% 🔺) 1.582s 12 1.67x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.318s (-35.9% 🟢) 1.006s (~) 0.688s 90 1.00x
🐘 Postgres Express 0.331s (-35.1% 🟢) 1.007s (~) 0.676s 90 1.04x
🌐 Redis Next.js (Turbopack) 0.420s 1.004s 0.584s 90 1.32x
🐘 Postgres Next.js (Turbopack) 0.471s 1.006s 0.535s 90 1.48x
💻 Local Nitro 2.202s (-13.2% 🟢) 2.883s (-4.2%) 0.681s 32 6.92x
💻 Local Next.js (Turbopack) 2.262s 2.977s 0.715s 31 7.11x
💻 Local Express 2.267s (-9.8% 🟢) 3.009s (~) 0.741s 30 7.13x
🌐 MongoDB Next.js (Turbopack) 2.614s 3.007s 0.393s 30 8.22x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.460s (+82.7% 🔺) 7.976s (+53.6% 🔺) 1.516s 12 1.00x
▲ Vercel Express 9.065s (+197.6% 🔺) 10.931s (+127.4% 🔺) 1.866s 9 1.40x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.666s (-15.7% 🟢) 1.006s (~) 0.340s 120 1.00x
🐘 Postgres Express 0.681s (-16.9% 🟢) 1.007s (-1.1%) 0.326s 120 1.02x
🌐 Redis Next.js (Turbopack) 0.782s 1.004s 0.222s 120 1.17x
🐘 Postgres Next.js (Turbopack) 0.952s 1.343s 0.390s 90 1.43x
🌐 MongoDB Next.js (Turbopack) 5.395s 6.012s 0.617s 20 8.10x
💻 Local Nitro 9.890s (-11.6% 🟢) 10.363s (-11.2% 🟢) 0.473s 12 14.85x
💻 Local Express 10.534s (-5.9% 🟢) 11.031s (-7.6% 🟢) 0.497s 11 15.81x
💻 Local Next.js (Turbopack) 11.259s 11.936s 0.677s 11 16.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 25.290s (+144.9% 🔺) 27.075s (+120.4% 🔺) 1.785s 5 1.00x
▲ Vercel Express 28.796s (+288.1% 🔺) 30.758s (+232.7% 🔺) 1.963s 4 1.14x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.129s (+428.2% 🔺) 2.005s (+99.6% 🔺) 0.010s (-18.4% 🟢) 2.017s (+98.0% 🔺) 0.888s 10 1.00x
🐘 Postgres Nitro 1.135s (+453.6% 🔺) 1.999s (+99.9% 🔺) 0.001s (-26.7% 🟢) 2.010s (+98.8% 🔺) 0.875s 10 1.01x
💻 Local Express 1.143s (+474.0% 🔺) 2.005s (+99.6% 🔺) 0.010s (-15.7% 🟢) 2.017s (+98.2% 🔺) 0.875s 10 1.01x
🐘 Postgres Express 1.164s (+467.4% 🔺) 2.001s (+100.3% 🔺) 0.002s (~) 2.010s (+98.7% 🔺) 0.846s 10 1.03x
💻 Local Next.js (Turbopack) 1.175s 2.004s 0.012s 2.020s 0.844s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.198s 2.002s 0.001s 2.010s 0.812s 10 1.06x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.144s (-14.4% 🟢) 3.508s (-14.2% 🟢) 1.595s (+66.1% 🔺) 5.599s (~) 3.455s 10 1.00x
▲ Vercel Next.js (Turbopack) 4.905s (-28.4% 🟢) 4.806s (-44.4% 🟢) 1.190s (+88.3% 🔺) 7.630s (-22.0% 🟢) 2.725s 10 2.29x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.524s (+144.1% 🔺) 2.003s (+98.9% 🔺) 0.004s (-10.5% 🟢) 2.025s (+98.1% 🔺) 0.502s 30 1.00x
💻 Local Express 1.536s (+102.9% 🔺) 2.012s (+95.6% 🔺) 0.009s (-0.9%) 2.024s (+94.6% 🔺) 0.487s 30 1.01x
🐘 Postgres Express 1.568s (+148.8% 🔺) 2.003s (+99.0% 🔺) 0.004s (+1.8%) 2.025s (+98.0% 🔺) 0.458s 30 1.03x
💻 Local Next.js (Turbopack) 1.666s 2.011s 0.010s 2.025s 0.359s 30 1.09x
🐘 Postgres Next.js (Turbopack) 1.689s 2.010s 0.004s 2.026s 0.337s 30 1.11x
💻 Local Nitro 1.691s (+101.6% 🔺) 2.011s (+98.7% 🔺) 0.010s (+3.3%) 2.202s (+97.3% 🔺) 0.511s 28 1.11x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.805s (-10.7% 🟢) 7.348s (-8.3% 🟢) 0.215s (-47.4% 🟢) 8.053s (-8.8% 🟢) 2.248s 8 1.00x
▲ Vercel Next.js (Turbopack) 13.581s (-19.7% 🟢) 13.625s (-25.3% 🟢) 0.244s (+15.4% 🔺) 15.667s (-17.3% 🟢) 2.087s 4 2.34x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.676s (-30.2% 🟢) 1.013s (-18.8% 🟢) 0.000s (-17.2% 🟢) 1.039s (-17.4% 🟢) 0.363s 58 1.00x
🐘 Postgres Express 0.693s (-27.9% 🟢) 1.031s (-19.3% 🟢) 0.000s (+98.3% 🔺) 1.051s (-19.6% 🟢) 0.358s 58 1.02x
🐘 Postgres Next.js (Turbopack) 0.842s 1.071s 0.000s 1.079s 0.237s 56 1.24x
💻 Local Nitro 1.337s (+9.4% 🔺) 2.015s (~) 0.000s (+266.7% 🔺) 2.018s (~) 0.680s 30 1.98x
💻 Local Next.js (Turbopack) 1.440s 2.014s 0.000s 2.017s 0.577s 30 2.13x
💻 Local Express 1.559s (+27.3% 🔺) 2.017s (~) 0.001s (+50.0% 🔺) 2.198s (+8.7% 🔺) 0.639s 28 2.31x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.382s (-9.6% 🟢) 4.785s (-6.2% 🟢) 0.000s (-54.2% 🟢) 5.239s (-5.3% 🟢) 1.857s 12 1.00x
▲ Vercel Next.js (Turbopack) 6.141s (-39.7% 🟢) 6.185s (-46.3% 🟢) 0.000s (+Infinity% 🔺) 7.838s (-35.0% 🟢) 1.697s 8 1.82x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.371s (-22.6% 🟢) 2.102s (-3.5%) 0.000s (+Infinity% 🔺) 2.115s (-3.8%) 0.744s 29 1.00x
🐘 Postgres Nitro 1.392s (-22.3% 🟢) 2.100s (-1.9%) 0.000s (-3.4%) 2.115s (-2.7%) 0.723s 29 1.02x
🐘 Postgres Next.js (Turbopack) 1.801s 2.309s 0.000s 2.318s 0.517s 26 1.31x
💻 Local Next.js (Turbopack) 2.728s 3.291s 0.001s 3.294s 0.567s 19 1.99x
💻 Local Nitro 3.097s (-8.6% 🟢) 3.969s (-1.6%) 0.001s (+99.2% 🔺) 3.971s (-1.6%) 0.875s 16 2.26x
💻 Local Express 3.457s (~) 3.898s (-3.4%) 0.000s (-41.7% 🟢) 4.234s (+4.9%) 0.777s 15 2.52x
🌐 MongoDB Next.js (Turbopack) ⚠️ missing - - - - -
🌐 Redis Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.599s (+22.1% 🔺) 6.940s (+15.2% 🔺) 0.000s (NaN%) 7.384s (+14.4% 🔺) 1.785s 9 1.00x
▲ Vercel Next.js (Turbopack) 8.826s (+57.1% 🔺) 9.330s (+33.6% 🔺) 0.000s (-100.0% 🟢) 10.537s (+39.7% 🔺) 1.710s 6 1.58x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the workflow failure event model so run_failed, step_failed, and step_retrying persist the full thrown value through the existing serialization pipeline (SerializedData / Uint8Array) rather than a lossy { message, stack, code } shape, enabling consumers to hydrate back to the original thrown value (including Error subclass identity, cause chains, and custom properties).

Changes:

  • Persist run/step failure payloads as opaque SerializedData and add errorCode as plaintext metadata for workflow runs.
  • Add dedicated dehydrate{Step,Run}Error / hydrate{Step,Run}Error helpers and wire them into step/workflow handlers and consumer APIs (Run.returnValue, step promise rejection).
  • Update storage backends (local + Postgres + Vercel world), CLI/web UI hydration, docs, and tests; add Postgres migration for workflow_runs.error_code.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
workbench/nextjs-webpack/pages/api/trigger-pages.ts Adjusts workbench API response to handle WorkflowRunFailedError.cause as unknown and surfaces errorCode.
workbench/nextjs-turbopack/pages/api/trigger-pages.ts Same as above for turbopack workbench app.
workbench/example/workflows/99_e2e.ts Adds e2e workflows that validate round-tripping thrown values (FatalError + cause chains; non-Error throws).
packages/world/src/steps.ts Switches Step.error to SerializedData and updates schema/docs accordingly.
packages/world/src/runs.ts Switches WorkflowRun.error to SerializedData and adds top-level errorCode.
packages/world/src/events.ts Updates step_failed/step_retrying/run_failed event payload schemas to carry serialized error (+ errorCode for runs).
packages/world-vercel/src/utils.ts Makes error (de)serialization helpers pass-through for SerializedData wire format.
packages/world-vercel/src/steps.ts Updates step wire schema + deserializer to pass through serialized error payloads.
packages/world-vercel/src/runs.ts Updates run wire schema to accept serialized error payloads and separate errorCode.
packages/world-vercel/src/events.ts Updates event result transformation docs/behavior for serialized error fields.
packages/world-postgres/test/storage.test.ts Updates Postgres storage tests to assert opaque Uint8Array persistence and legacy error handling.
packages/world-postgres/src/storage.ts Updates Postgres storage read/write paths for serialized errors and run errorCode.
packages/world-postgres/src/drizzle/schema.ts Changes CBOR error column typing to SerializedData and adds error_code column.
packages/world-postgres/src/drizzle/migrations/meta/_journal.json Registers the new Postgres migration.
packages/world-postgres/src/drizzle/migrations/0010_add_error_code.sql Adds error_code column to workflow.workflow_runs.
packages/world-local/src/storage/events-storage.ts Updates local world event application to store serialized errors verbatim (+ errorCode).
packages/world-local/src/storage.test.ts Updates local world storage tests for new error payload shape and stripping behavior.
packages/web-shared/src/components/sidebar/attribute-panel.tsx Adds UI display handling for the new errorCode attribute.
packages/errors/src/index.ts Updates WorkflowRunFailedError to use cause: unknown and add errorCode; registers FatalError/RetryableError on globalThis for cross-realm identity.
packages/core/src/step.ts Hydrates step failure payloads via hydrateStepError and rejects with the original thrown value.
packages/core/src/step.test.ts Updates tests to use error de/rehydration pipeline and validate subclass preservation.
packages/core/src/serialization/reducers/common.ts Updates FatalError/RetryableError revivers to resolve constructors via globalThis Symbol registry when available.
packages/core/src/serialization.ts Adds dehydrate{Step,Run}Error / hydrate{Step,Run}Error helpers and integrates with format-prefix + optional encryption.
packages/core/src/serialization.test.ts Adds unit tests covering round-trips for step/run error helpers, subclasses, causes, and encryption.
packages/core/src/serialization-format.ts Extends observability hydration to hydrate error fields on step/run resources.
packages/core/src/runtime/step-handler.ts Writes step_failed/step_retrying errors via dehydrateStepError (encrypted when configured).
packages/core/src/runtime/step-handler.test.ts Updates mocked serialization + assertions to account for binary error payload.
packages/core/src/runtime/runs.test.ts Adds tests ensuring Run.returnValue throws WorkflowRunFailedError with hydrated cause + errorCode.
packages/core/src/runtime/run.ts Hydrates run errors via hydrateRunError before throwing WorkflowRunFailedError.
packages/core/src/runtime.ts Writes run_failed errors via dehydrateRunError and preserves remapped stacks on the serialized thrown value.
packages/core/src/async-deserialization-ordering.test.ts Updates ordering tests to use serialized step errors.
packages/core/e2e/e2e.test.ts Updates/extends e2e coverage for new errorCode + hydrated causes across step/run failures.
packages/cli/src/lib/inspect/hydration.ts Uses core common revivers and adds Error.toJSON shims so --json output includes name/message/stack/cause.
docs/content/docs/foundations/errors-and-retries.mdx Updates docs to reference errorCode and cause: unknown narrowing.
docs/content/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx Updates API reference for WorkflowRunFailedError’s new cause and errorCode.
.changeset/run-step-error-hydration.md Declares major-version breaking changes across affected packages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/world-postgres/src/storage.ts Outdated
Comment thread packages/core/src/serialization.ts
Comment thread packages/core/src/serialization.ts
Copy link
Copy Markdown
Contributor

@karthikscale3 karthikscale3 left a comment

Choose a reason for hiding this comment

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

AI review: Five flagged items from my analysis of this PR, posted as inline comments below.

Comment thread packages/core/src/runtime/step-handler.ts Outdated
Comment thread packages/core/src/runtime.ts Outdated
Comment thread packages/core/src/runtime/step-handler.ts Outdated
Comment thread packages/world-postgres/src/storage.ts
Comment thread packages/core/src/serialization.ts
TooTallNate added 16 commits May 4, 2026 01:04
Switch run_failed, step_failed, and step_retrying events to persist
the full thrown value via the workflow serialization pipeline (as
SerializedData / Uint8Array) instead of a lossy { message, stack, code }
StructuredError shape. Consumers hydrate via hydrateRunError /
hydrateStepError to reconstruct the original thrown value, preserving
Error subclass identity, cause chains, and custom properties.

- WorkflowRun.error and Step.error are now SerializedData
- WorkflowRun gains a top-level errorCode plaintext field
- WorkflowRunFailedError.cause is now the hydrated thrown value
- Adds world-postgres migration 0010_add_error_code.sql
- Legacy pre-pipeline errorJson records surface as undefined on read
cause is now `unknown` (the hydrated thrown value) rather than
`Error & { code }`. Defensively extract Error-shaped fields when the
hydrated value is an Error, otherwise round-trip the raw value, and
expose the new `errorCode` classification field.
The hydrated `cause` is now `unknown` (the original thrown value
through the serialization pipeline) and the error classification has
moved to the top-level `errorCode` property. Update the two affected
docs pages and the `TSDoc` interface to reflect the new shape, and
narrow `cause` with `instanceof Error` before accessing fields.
Unit tests:
- 19 new dehydrate/hydrate{Step,Run}Error round-trip tests covering
  FatalError, plain Error, built-in Error subclasses, non-Error thrown
  values (string, plain object), cause chains, encryption round-trip,
  the binary format prefix contract, and the unserializable / unknown-
  format error paths.
- 5 new tests for Run.returnValue when the run is failed: hydrated
  FatalError + cause as cause, plain Error preservation, non-Error
  thrown values surfaced verbatim, cross-class cause chains, and the
  hydration-failure fallback that still surfaces errorCode.

E2E tests (new, in 99_e2e.ts + e2e.test.ts):
- Step throw → workflow catch round-trips a FatalError with a TypeError
  cause chain, asserting class identity, fatal marker, and cause name +
  message all survive the step_failed event pipeline.
- Workflow throw → run_failed reaches  status with the new
  top-level errorCode metadata exposed (cause-shape coverage lives at
  the unit level, since the SWC plugin's class registration is not
  invoked in the plain-Node e2e runner).
- Workflow throw of a non-Error value round-trips that value verbatim
  as WorkflowRunFailedError.cause.

Adjustments to existing assertions:
- error.cause is now ; tests narrow with
  and use the new top-level  field instead of .
- step.error / run.error from CLI --withData are now hydrated payloads:
  unregistered class instances surface as Instance refs whose
  carries the original message + stack.

Observability hydration:
- hydrateStepIO / hydrateWorkflowIO in serialization-format.ts now
  hydrate the  field via hydrateData, so the CLI and web UI
  continue to surface readable run/step error messages and stacks.
When a workflow runs in a Node `vm` context, its bundled
`@workflow/errors` is a different module instance than the host's
import (separate prototype chains, separate class identity). Calling
`new FatalError(...)` from the host-side reviver produces a
host-realm instance that fails `err instanceof FatalError` checks
in the workflow code — even when the serialized payload was correctly
tagged via the dedicated `FatalError` reducer.

Surfaced by the local-prod e2e "step throw round-trips FatalError"
test on Next.js Turbopack: each route gets its own bundled chunk, so
the flow handler's `@workflow/errors` and the workflow VM bundle's
`@workflow/errors` are two distinct copies of the same module.

Fix:

- Each bundled copy of `@workflow/errors` self-registers its
  `FatalError` and `RetryableError` classes on `globalThis` via
  `Symbol.for("@workflow/errors//FatalError")` /
  `Symbol.for("@workflow/errors//RetryableError")`. First load wins
  per realm; the descriptor is non-writable / non-configurable to make
  accidental clobbering loud.

- The revivers in `@workflow/core`'s common reducers module read the
  consumer's `globalThis` (passed in as `global`) to pick up the
  realm-local class, falling back to the host-imported class when no
  registration is present (e.g. in the CLI / test runner).
The runtime's run-failure path computes a source-map-remapped stack
and then assigns it back onto the thrown value via `if (err
instanceof Error) err.stack = errorStack`. Workflows run inside a
Node `vm` context, so a workflow-thrown error is an instance of the
VM realm's `Error` — `instanceof` against the host realm's
`Error` returns `false`, the assignment is skipped, and the
serialized `run_failed` event carries the un-remapped (bundled-line-
number) stack instead of the source-mapped one.

Switch the gate to `types.isNativeError`, which uses V8's internal
type tag and works across realms — same approach already in place
for the serialization reducers.

Caught by the local-prod e2e "nested function calls preserve message
and stack trace" and "cross-file imports preserve message and stack
trace" tests, which assert that the persisted run-error stack
contains `99_e2e.ts` / `helpers.ts`.
Two issues with the CLI's hand-rolled reviver list:

1. It hadn't been updated for the new first-class Error subclass
   reducers (`TypeError`, `RangeError`, `FatalError`, `RetryableError`,
   etc.). devalue throws "Unknown type X" when it encounters a
   reduced value with no matching reviver, and `hydrateResourceIO`
   swallows that error and surfaces the raw `Uint8Array` payload —
   so `step.error` / `run.error` showed up as raw byte dumps in
   `workflow inspect` output.

2. Even with all the right revivers, `Error.prototype`'s `message`
   / `stack` / `cause` are non-enumerable, so `JSON.stringify`
   (used by `workflow inspect --json`) drops them — leaving the
   subclass-specific enumerable fields (e.g. `FatalError.fatal`)
   visible but the actual error data missing.

Fix:

- Build the CLI reviver set on top of `getCommonRevivers()` from
  `@workflow/core` so the CLI stays in sync with the runtime's
  reducer set automatically. New core reducers/revivers will Just
  Work without any CLI-side change.

- Wrap each Error reviver from the common set with a thin shim that
  attaches a non-enumerable `toJSON` method to the produced
  `Error` instance. `JSON.stringify` calls `toJSON` and gets a
  full object (`name` + `message` + `stack` + `cause` + any
  enumerable subclass fields like `fatal` / `retryAfter` /
  `errors`); `util.inspect` ignores `toJSON` and renders the
  canonical `Error: msg\\n at ...` format. Best of both worlds for
  CLI output without compromising the runtime hydration path.

Caught by the local-prod e2e "basic step error preserves" and
"cross-file step error preserves" tests, which read
`failedStep.error.message` / `.stack` from the CLI's JSON output.
The previous JSDoc described preserving legacy values "for best-effort
hydration" which contradicted the implementation, where legacy errors
are intentionally surfaced as absent (the pre-pipeline shapes can't be
hydrated by the new error revivers). Rewrite the comment so the contract
matches behavior. Also rename the now-unused parameter to `_errorJson`
to reflect that the function ignores it.

Caught by a code review on #1851.
Three review-driven adjustments that all touch the queue handlers and
their interaction with the error serialization pipeline:

1. Memoize the per-run encryption key fetch. The step handler used to
   eagerly fetch + import the key at the top of every step delivery so
   the value would be in scope for every potential dehydrateStepError
   path. That pessimized step-started early-return cases (the fetch
   happens unconditionally even when the step never reaches user code)
   and required duplicating the same boilerplate at four call sites in
   runtime.ts. Introduce `memoizeEncryptionKey(world, run)` in
   runtime/helpers.ts that returns a lazy, single-fetch accessor;
   step-handler / runtime call sites use `await getEncryptionKey()`
   instead. The first caller pays the fetch cost, subsequent callers
   await the cached promise, and steps that fail before any
   encryption-aware work happens skip the fetch entirely.

2. Preserve the prior attempt's serialized error as the cause on the
   defensive max-retries-exceeded `step_failed` re-invocation guard.
   The existing comment explicitly opted out of cause attachment, but
   the symmetric post-failure path below already does this and the
   reviewer is right that consumers shouldn't have to walk the
   step_retrying event history to recover the underlying error. Best-
   effort: if hydration of the prior `step.error` throws, fall back
   to a FatalError without cause rather than letting the event write
   itself fail.

3. Document the intentional `unflatten` throw in
   `hydrateStepError` / `hydrateRunError` for non-Uint8Array input.
   SDK version is pinned per workflow run via skew protection so the
   non-binary branch is dead in production; if a misshapen value
   reaches it, surfacing the throw via the surrounding o11y try/catch
   is more debuggable than masking it. Add a comment so future
   reviewers don't reach for a defensive fallback.

A standalone `falls back to plaintext` suggestion on the run_failed
key fetch was rejected: when encryption is configured we should fail
loudly rather than silently emit plaintext error data. The queue's
redelivery semantics will retry the key fetch; persistent KMS outages
get logged with the existing "persistent error preventing the run from
being terminated" message rather than a security regression.
`hydrateEventData` enumerated the per-event fields that need
hydration (`result`, `input`, `output`, `metadata`, `payload`)
but omitted the new `error` field on `step_failed`,
`step_retrying`, and `run_failed` events. Without this branch,
o11y tools that list events (e.g. `workflow inspect events`) surface
the raw `Uint8Array` payload instead of a hydrated
`{ name, message, stack, … }` object even though the entity-level
`Run.error` / `Step.error` paths already hydrate.

Mirrors the existing per-field branches; the `try/catch` leaves the
field un-hydrated on parse failure rather than failing the whole
event view. Adds a unit test.
Workflows execute inside a separate `vm` realm: the
`WorkflowRuntimeError` class bundled into the workflow code and the
host-imported one are distinct constructors, so an
`err instanceof WorkflowRuntimeError` check on a VM-thrown error
returns `false` and we'd misclassify genuine runtime errors (corrupted
event log, missing timestamps, workflow/step not registered) as user
errors.

Switch to each subclass's `.is()` static (a name-based duck check that
works across realms). Since `WorkflowRuntimeError.is` only matches its
own concrete name, enumerate every concrete subclass we want to
recognize (`StepNotRegisteredError`, `WorkflowNotRegisteredError`)
in a `RUNTIME_ERROR_CHECKS` table; keep that table in sync with the
class hierarchy in `@workflow/errors`.

Existing `classify-error.test.ts` already covers `WorkflowRuntimeError`
and `WorkflowNotRegisteredError` cases — both still pass.
We had `errorWorkflowThrowNonErrorValue` (workflow body throws a plain
object — round-trips verbatim as `WorkflowRunFailedError.cause`) but
no symmetric coverage for the step-throw side. Step-throw goes through
a different code path: non-Error values aren't recognized as
`FatalError` (no `name === 'FatalError'`) nor `RetryableError`,
so they take the transient retry path. After max retries the runtime
wraps the original thrown value as `cause` on a fresh `FatalError`
which the workflow's catch block then sees.

Add a workflow that throws a recognizable plain object from a step
with `maxRetries = 0` (so we exhaust on first attempt and avoid a
long test wait) and a workflow that asserts the wrapped FatalError
shape: `isFatal`, `instanceof FatalError`, message includes the
original object's serialized form, `cause` is the original non-Error
object verbatim with structure preserved.

Documents the current retry-then-wrap behavior so any future change
to "non-Error throws skip retries" semantics has to update the test.
Pre-upgrade failed runs that wrote into world-postgres's deprecated
`error` text column can't be hydrated through the new pipeline (the
shape is incompatible with the new revivers). The new runtime
intentionally surfaces them as `error: undefined` on read; the
original payload is still readable directly from the `errorJson`
column for manual inspection. Add a one-sentence note to the
changeset's migration text so consumers upgrading don't get blindsided
by suddenly-empty error fields on historical runs.
Conflict resolution notes:

- packages/core/src/runtime.ts: main restructured the workflow handler
  into an inline-replay loop. The 1851 run_failed dehydration changes
  (MAX_DELIVERIES, REPLAY_TIMEOUT, WorkflowRuntimeError catch, user-code
  catch) are reapplied on top of main's new structure, including the
  new memoizeEncryptionKey helper at the per-loop key resolution site.
  The user-code run_failed catch now uses dehydrateRunError + applies
  the source-map-remapped stack via types.isNativeError (cross-realm
  safe), matching the e6e0f64 pattern from this branch.

- packages/core/src/runtime/step-executor.ts: this file is new from
  main (V2 inline step execution). Apply the same step_failed /
  step_retrying serialization changes from this branch's
  ef1309f+e6e0f6433 commits: dehydrateStepError on every event-write
  site, hoist a memoizeEncryptionKey accessor at the top of the trace
  block, preserve the prior step.error as cause on the max-retries-
  reached defensive guard, and apply types.isNativeError for the
  source-map remap.

- packages/world-local/src/storage/events-storage.ts: take main's side
  (the other 1851 changes here are unrelated to this file's main
  restructuring), then re-apply the three error-shape changes —
  run_failed / step_failed / step_retrying now store the SerializedData
  Uint8Array verbatim instead of wrapping in { message, stack, code }.

- packages/world-postgres/src/drizzle/migrations: main introduced
  0010_add_events_entity_creation_unique_index.sql (#1878) at the same
  index this branch wanted for 0010_add_error_code.sql. Renumber the
  branch's migration to 0011 and update the journal so both apply in
  order on existing installations.
Copy link
Copy Markdown
Contributor

@karthikscale3 karthikscale3 left a comment

Choose a reason for hiding this comment

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

AI review: All five flagged items from my prior pass have been addressed — the lazy/memoized encryption key accessor eliminates the hot-path perf regression (#1), the max-retries terminal step_failed now chains the prior attempt's error as .cause (#3), the changeset documents the legacy Postgres error-data loss (#4), and both hydrateStepError/hydrateRunError carry inline comments explaining the intentional throw-on-unexpected-shape design (#5). The push-back on #2 (no plaintext fallback in the error handler) is reasonable — failing loudly is the right call when encryption is configured, and the Vercel serverless runtime's local HKDF derivation means the KMS-unreachable scenario is largely theoretical for the run_failed paths. Approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants