feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious
, '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

feat: classify run failure error codes and improve error logging - #1340

Merged
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error
Mar 18, 2026
Merged

feat: classify run failure error codes and improve error logging#1340
pranaygp merged 1 commit into
mainfrom
pgp/run-failed-schema-vailidation-error

Conversation

@pranaygp

@pranaygppranaygp commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds error code classification (USER_ERROR, RUNTIME_ERROR) to run_failed events, populating the existing but previously unused errorCode field
  • Improves error logging across world-local queue, world-vercel schema validation, and runtime to be more concise and user-friendly
  • Stacked on top of fix: separate infrastructure vs user code error handling #1339 which structurally separates infrastructure vs user code error handling

Details

Error codes

After #1339's structural separation, the run_failed try/catch only catches:

  • User code errorsUSER_ERROR (throws from workflow functions, propagated step failures)
  • WorkflowRuntimeErrorRUNTIME_ERROR (corrupted event log, missing timestamps — internal bugs)

Infrastructure errors (ECONNRESET, 5xx, schema validation) never produce run_failed at all — they propagate to the queue for retry.

The error code flows through the existing (previously unused) plumbing:
eventData.errorCodeStructuredError.codeWorkflowRunFailedError.cause.code

Note on storage:errorCode is stored inline as a plain DynamoDB attribute on the run entity — it does NOT go through refs/encryption. Only the error object (message + stack) goes through refTrackererrorRef. The errorCode is a sibling field in eventData, extracted and stored separately by the server (events.ts:846).

Web UI

  • RUNTIME_ERROR: amber dot + "Internal Error" tooltip header
  • USER_ERROR / absent (backward compat): red dot + "Error Details" tooltip header
  • Error code shown as a label in the tooltip

Logging improvements

See examples below.

Error log examples (captured from e2e test runs)

Runtime error logs (before → after)

Before:

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFXW09EHA9M3QXWNEJNC52Z',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

After (now includes errorCode):

[Workflow] Error while running workflow {
workflowRunId: 'wrun_01KKFY7MARP7D16PN69HCMYNVQ',
errorCode: 'USER_ERROR',
errorName: 'Error',
errorStack: 'Error: Nested workflow error\n at errorNested3 ...'
}

Queue error logs (before → after)

Before (dumped full request body with traceCarrier, runId, stepId, etc.):

[local world] Failed to queue message {
queueName: '__wkf_step_...',
text: '"WorkflowAPIError: Injected 5xx"',
status: 500,
headers: { ... },
body: '{"workflowName":"workflow//./workflows/99_e2e//serverError5xxRetryWorkflow",
"workflowRunId":"wrun_01KKF...",
"workflowStartedAt":1773282422605,
"stepId":"step_01KKF...",
"traceCarrier":{"traceparent":"00-778ab...","baggage":"workflow.run_id=wrun_01KKF..."},
"requestedAt":"2026-03-12T02:27:02.778Z"}'
}

After (concise, actionable):

[world-local] Queue message failed (attempt 1/3, status 500): "WorkflowAPIError: Injected 5xx" {
queueName: '__wkf_step_...',
messageId: 'msg_01KKF...'
}

Schema validation error messages (before → after)

Before (full Zod error dump + CBOR debug context always included):

Schema validation failed for POST /v2/runs/wrun_.../events:
[
{
"expected": "object",
"code": "invalid_type",
"path": ["run", "error"],
"message": "Invalid input: expected object, received undefined"
}
]
Response context: Content-Type: application/cbor, 1589 bytes (CBOR), preview: {
event: { runId: 'wrun_...', eventId: 'evnt_...', correlationId: 'wrun_...',
eventType: 'run_failed', eventData: { error: { _ref: 's3rf:team_...', _type: 'RemoteRef' } },
createdAt: 20... }
}

After (concise issue list, verbose context only when DEBUG env var is set):

Schema validation failed for POST /v2/runs/wrun_.../events:
run.error: Invalid input: expected object, received undefined

Debug curl reproduction (before → after)

Before: only shown when DEBUG=1 (exact string match)
After: shown when DEBUG is set to any truthy value (consistent with debug package)

Test plan

  • Unit tests for classifyRunError (7 tests, all pass)
  • All 478 core unit tests pass
  • E2E: errorWorkflowNested — asserts error.cause.code === 'USER_ERROR' and runData.error.code === 'USER_ERROR'
  • E2E: errorRetryFatal — asserts error.cause.code === 'USER_ERROR'
  • E2E: infraErrorRetryWorkflow — validates infra errors on run_completed retry via queue (not run_failed)
  • Visual: verify amber vs red badge in web UI

🤖 Generated with Claude Code

@vercel

vercelBot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcad456

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

This PR includes changesets to release 21 packages
NameType
@workflow/errorsPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-localPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
workflowPatch
@workflow/world-postgresPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/aiPatch
@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 Mar 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production758067825
✅ 💻 Local Development7820118900
✅ 📦 Local Production7820118900
❌ 🐘 Local Postgres7811118900
✅ 🪟 Windows720375
❌ 🌍 Community Worlds1185615189
❌ 📋 Other197127225
Total3490584664014

❌ Failed Tests

🐘 Local Postgres (1 failed)

sveltekit-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
🌍 Community Worlds (56 failed)

mongodb (3 failed):

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

redis (2 failed):

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

turso (51 failed):

  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • addTenWorkflow | wrun_01KM16H1BXF1Y1GZCG6ZD2NVGA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KM16J2QM1N4TS0GXAJ9PV4D8
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KM16H87FE4JQ029E3JSW7BB3
  • promiseRaceWorkflow | wrun_01KM16HBK6YGKA6D0W7E3WRQEK
  • promiseAnyWorkflow | wrun_01KM16HDSNA0M14FA412ZX8GYH
  • importedStepOnlyWorkflow | wrun_01KM16JCZF90F1DT7GMDC84HV0
  • hookWorkflow | wrun_01KM16HVR3E2W78FDFFK8ZAEH8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KM16J6TZAKBH7QXRBD6V9TN6
  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0
  • sleepingWorkflow | wrun_01KM16JPE4XGEK9NF3TWPV4PPQ
  • parallelSleepWorkflow | wrun_01KM16K21DH1YMFD51YKEWTQGR
  • nullByteWorkflow | wrun_01KM16K6GMBBC145MC3NXR8BMG
  • workflowAndStepMetadataWorkflow | wrun_01KM16K8N2RC3204GQG1JB6ASY
  • fetchWorkflow | wrun_01KM16M860J7720ZNM5SPKMNME
  • promiseRaceStressTestWorkflow | wrun_01KM16MBNNJXEFF8K7RN0TZ0MY
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KM16QA2CMVJPNGQVJS202FZW
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KM16QXS6R4ZFCW43YM379Q8K
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KM16RHZEC82KPP70CVGXQ1SZ
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KM16S628CP26EW2F3763JQ42
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KM16SEYRAZ6QR4KQR7G83S0Z
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KM16SMFZ3F0RCPAH4JBCBJ45
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KM16SPQ4JZ9HYMR89AQ202WF
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KM16T6ETD56SFF46GX0N049C
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KM16TC55TBEN26Z65WB2JNZE
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KM16THVAACYQ0QDCXE0V04DQ
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KM16TRMXHMTPXM03EWFHZEFQ
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KM16TZBTKQJYPSDQR3RT4FJK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KM16V673WJDTHDP9N625T0Y0
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KM16VD6VJ8NFSBR32WCBM88X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KM16VR2K28ADCCWBG3H8JG26
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KM16W02CDN1296Y2SVJETTA0
  • cancelRun - cancelling a running workflow | wrun_01KM16W6TRZ3T8582HN3R6GB59
  • cancelRun via CLI - cancelling a running workflow | wrun_01KM16WGHJZMM5P0CDBA4QGHRD
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KM16WWZD60524EDR69WA9VWK
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KM16XKJ6K117NBQNEF591ZNF
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KM16XY29QJHXDN2V770BHE2J
📋 Other (1 failed)

e2e-local-postgres-nest-stable (1 failed):

  • webhookWorkflow | wrun_01KM16JFFMSJNY4A120635Z2N0

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6807
✅ example6807
✅ express6807
✅ fastify6807
✅ hono6807
✅ nextjs-turbopack7302
✅ nextjs-webpack7302
✅ nitro6807
✅ nuxt6807
✅ sveltekit6807
✅ vite6807
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
✅ sveltekit-stable6609
✅ vite-stable6609
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6609
✅ express-stable6609
✅ fastify-stable6609
✅ hono-stable6609
✅ nextjs-turbopack-canary55020
✅ nextjs-turbopack-stable7203
✅ nextjs-webpack-canary55020
✅ nextjs-webpack-stable7203
✅ nitro-stable6609
✅ nuxt-stable6609
❌ sveltekit-stable6519
✅ vite-stable6609
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7203
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5233
✅ redis-dev302
❌ redis5323
✅ turso-dev302
❌ turso4513
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6609
❌ e2e-local-postgres-nest-stable6519
✅ e2e-local-prod-nest-stable6609

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@pranaygp
pranaygpforce-pushed the pgp/run-failed-schema-vailidation-error branch from e7068bf to e019085CompareMarch 17, 2026 19:35
return 'bg-emerald-500';
case 'failed':
return 'bg-red-500';
return isInfra ? 'bg-amber-500' : 'bg-red-500';

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.

I think we should just show red no matter whether it's infra or user error

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.

also don't render the entire error stack trace in the tooltip. That won't even be accessible without decryption when a run is encrypted - so it's enough to just show the error code for now

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.

Fixed — all failures are red now, removed the amber/infra distinction.

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.

Fixed — tooltip now only shows the error code (e.g. USER_ERROR), no stack trace or message. The full error data is behind the ref/encryption anyway.

@github-actions

github-actionsBot commented Mar 18, 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.043s (-1.6%)1.005s (~)0.962s101.00x
💻 LocalNitro0.045s (+4.0%)1.006s (~)0.961s101.03x
💻 LocalNext.js (Turbopack)0.051s (+22.7% 🔺)1.005s (~)0.954s101.18x
🌐 RedisNext.js (Turbopack)0.057s (+5.8% 🔺)1.005s (~)0.948s101.32x
🐘 PostgresExpress0.058s (-26.2% 🟢)1.012s (-1.4%)0.954s101.33x
🐘 PostgresNitro0.060s (-3.6%)1.013s (~)0.953s101.38x
🐘 PostgresNext.js (Turbopack)0.066s (+13.0% 🔺)1.012s (~)0.946s101.53x
🌐 MongoDBNext.js (Turbopack)0.100s (~)1.008s (~)0.908s102.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.438s (-9.9% 🟢)2.091s (-12.8% 🟢)1.653s101.00x
▲ VercelExpress0.456s (~)2.295s (+5.4% 🔺)1.839s101.04x
▲ VercelNitro0.546s (+12.9% 🔺)2.577s (+4.0%)2.032s101.25x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.121s (+1.5%)2.006s (~)0.885s101.00x
💻 LocalNitro1.129s (~)2.006s (~)0.876s101.01x
💻 LocalExpress1.130s (+2.2%)2.006s (~)0.876s101.01x
🌐 RedisNext.js (Turbopack)1.133s (~)2.007s (~)0.874s101.01x
🐘 PostgresNext.js (Turbopack)1.143s (~)2.011s (~)0.868s101.02x
🐘 PostgresExpress1.145s (-0.7%)2.012s (~)0.867s101.02x
🐘 PostgresNitro1.156s (~)2.013s (~)0.858s101.03x
🌐 MongoDBNext.js (Turbopack)1.306s (-0.8%)2.009s (~)0.703s101.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.055s (+0.9%)3.320s (-6.0% 🟢)1.265s101.00x
▲ VercelNitro2.119s (~)3.742s (+0.8%)1.622s101.03x
▲ VercelExpress2.285s (+7.2% 🔺)3.689s (+6.1% 🔺)1.405s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.840s (+1.7%)11.025s (~)0.184s31.00x
🐘 PostgresNext.js (Turbopack)10.870s (~)11.039s (~)0.169s31.00x
🌐 RedisNext.js (Turbopack)10.884s (+1.1%)11.023s (~)0.139s31.00x
🐘 PostgresExpress10.891s (-0.6%)11.041s (~)0.151s31.00x
💻 LocalNitro10.912s (~)11.024s (~)0.113s31.01x
💻 LocalExpress10.923s (+1.9%)11.022s (~)0.099s31.01x
🐘 PostgresNitro10.950s (~)11.041s (~)0.091s31.01x
🌐 MongoDBNext.js (Turbopack)12.284s (~)13.028s (~)0.744s31.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.093s (-1.4%)18.560s (-1.5%)1.466s21.00x
▲ VercelNitro17.322s (-4.0%)19.086s (-6.0% 🟢)1.764s21.01x
▲ VercelNext.js (Turbopack)17.552s (+0.8%)18.804s (-0.8%)1.252s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.812s (~)27.049s (~)0.237s31.00x
🐘 PostgresNext.js (Turbopack)27.191s (~)27.722s (~)0.531s31.01x
💻 LocalNext.js (Turbopack)27.241s (+2.0%)28.054s (+3.7%)0.813s31.02x
🐘 PostgresNitro27.249s (~)28.064s (~)0.815s31.02x
🐘 PostgresExpress27.293s (~)28.062s (~)0.769s31.02x
💻 LocalExpress27.507s (+2.0%)28.053s (+3.7%)0.546s31.03x
💻 LocalNitro27.587s (~)28.054s (~)0.467s31.03x
🌐 MongoDBNext.js (Turbopack)30.236s (~)31.055s (~)0.818s21.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro43.975s (-1.2%)45.567s (-1.0%)1.592s21.00x
▲ VercelExpress45.083s (-0.8%)46.775s (-1.0%)1.692s21.03x
▲ VercelNext.js (Turbopack)46.056s (-1.4%)48.004s (-0.9%)1.948s21.05x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.371s (~)54.089s (~)0.717s21.00x
🐘 PostgresExpress54.255s (~)55.093s (~)0.838s21.02x
🐘 PostgresNitro54.392s (~)55.097s (~)0.705s21.02x
🐘 PostgresNext.js (Turbopack)54.429s (+0.6%)54.617s (+0.9%)0.187s21.02x
💻 LocalNext.js (Turbopack)56.042s (+2.2%)56.605s (+2.7%)0.563s21.05x
💻 LocalExpress56.641s (+2.1%)57.104s (+1.8%)0.463s21.06x
💻 LocalNitro56.745s (~)57.106s (~)0.361s21.06x
🌐 MongoDBNext.js (Turbopack)60.736s (~)61.083s (~)0.347s21.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express93.943s (-7.2% 🟢)96.079s (-6.4% 🟢)2.136s11.00x
▲ VercelNitro95.428s (-1.8%)97.707s (-1.9%)2.279s11.02x
▲ VercelNext.js (Turbopack)98.310s (~)99.739s (~)1.429s11.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (-0.6%)2.010s (~)0.740s151.00x
🐘 PostgresNitro1.276s (~)2.012s (~)0.735s151.00x
🐘 PostgresNext.js (Turbopack)1.289s (+1.0%)2.014s (~)0.725s151.01x
🌐 RedisNext.js (Turbopack)1.391s (~)2.006s (~)0.615s151.09x
💻 LocalNitro1.514s (+1.2%)2.005s (~)0.491s151.19x
💻 LocalExpress1.532s (+2.2%)2.006s (~)0.474s151.21x
💻 LocalNext.js (Turbopack)1.562s (+4.4%)2.006s (~)0.445s151.23x
🌐 MongoDBNext.js (Turbopack)2.150s (-0.7%)3.010s (~)0.860s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.410s (+6.6% 🔺)3.796s (+0.8%)1.387s91.00x
▲ VercelNext.js (Turbopack)2.485s (-7.5% 🟢)3.580s (-10.8% 🟢)1.095s91.03x
▲ VercelNitro3.009s (+23.3% 🔺)4.346s (+5.8% 🔺)1.337s71.25x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.440s (-0.7%)3.013s (~)0.572s101.00x
🐘 PostgresExpress2.445s (~)3.012s (~)0.567s101.00x
🐘 PostgresNext.js (Turbopack)2.557s (+2.2%)3.018s (~)0.461s101.05x
🌐 RedisNext.js (Turbopack)2.580s (~)3.007s (~)0.427s101.06x
💻 LocalNitro2.938s (+1.9%)3.342s (+11.1% 🔺)0.403s91.20x
💻 LocalExpress3.025s (+14.0% 🔺)3.453s (+14.8% 🔺)0.428s91.24x
💻 LocalNext.js (Turbopack)3.091s (+12.4% 🔺)3.885s (+25.0% 🔺)0.794s81.27x
🌐 MongoDBNext.js (Turbopack)4.744s (+2.3%)5.179s (~)0.435s61.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.704s (+4.8%)4.152s (+5.0% 🔺)1.448s81.00x
▲ VercelExpress2.730s (+11.7% 🔺)3.821s (+2.4%)1.091s81.01x
▲ VercelNext.js (Turbopack)3.040s (+3.3%)4.267s (-5.9% 🟢)1.227s81.12x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.587s (~)4.014s (~)0.427s81.00x
🐘 PostgresNitro3.615s (+0.5%)4.015s (~)0.401s81.01x
🐘 PostgresNext.js (Turbopack)3.906s (+3.6%)4.147s (+3.3%)0.241s81.09x
🌐 RedisNext.js (Turbopack)4.085s (-2.4%)4.725s (-5.7% 🟢)0.640s71.14x
💻 LocalNext.js (Turbopack)7.824s (+27.2% 🔺)8.022s (+17.7% 🔺)0.198s42.18x
💻 LocalExpress8.129s (+20.2% 🔺)8.523s (+13.4% 🔺)0.394s42.27x
💻 LocalNitro8.769s (+10.0% 🔺)9.275s (+5.7% 🔺)0.505s42.44x
🌐 MongoDBNext.js (Turbopack)9.831s (~)10.351s (~)0.519s32.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.139s (-1.8%)4.486s (-9.2% 🟢)1.347s71.00x
▲ VercelExpress3.337s (+13.7% 🔺)4.981s (+14.8% 🔺)1.645s71.06x
▲ VercelNext.js (Turbopack)3.888s (-3.8%)5.096s (-2.2%)1.208s61.24x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.270s (~)2.011s (~)0.740s151.00x
🐘 PostgresNitro1.274s (+0.6%)2.011s (~)0.737s151.00x
🐘 PostgresNext.js (Turbopack)1.282s (+0.7%)2.012s (~)0.730s151.01x
🌐 RedisNext.js (Turbopack)1.289s (-3.7%)2.006s (~)0.718s151.01x
💻 LocalExpress1.507s (~)2.005s (~)0.498s151.19x
💻 LocalNitro1.523s (+1.2%)2.007s (~)0.483s151.20x
💻 LocalNext.js (Turbopack)1.580s (+3.2%)2.072s (+3.3%)0.492s151.24x
🌐 MongoDBNext.js (Turbopack)2.156s (-1.7%)3.009s (~)0.853s101.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.209s (-10.0% 🟢)3.356s (-11.2% 🟢)1.147s91.00x
▲ VercelNitro2.265s (+0.7%)3.871s (+4.4%)1.606s81.03x
▲ VercelExpress3.931s (+48.8% 🔺)5.630s (+38.8% 🔺)1.699s61.78x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.446s (~)3.011s (~)0.564s101.00x
🐘 PostgresNitro2.469s (~)3.011s (~)0.542s101.01x
🐘 PostgresNext.js (Turbopack)2.532s (~)3.014s (-3.1%)0.481s101.04x
🌐 RedisNext.js (Turbopack)2.579s (-0.8%)3.007s (~)0.428s101.05x
💻 LocalNext.js (Turbopack)2.842s (+3.9%)3.676s (+22.2% 🔺)0.834s91.16x
💻 LocalExpress3.006s (+5.9% 🔺)3.564s (+14.6% 🔺)0.558s91.23x
💻 LocalNitro3.059s (-1.0%)3.760s (~)0.700s81.25x
🌐 MongoDBNext.js (Turbopack)4.698s (+2.4%)5.177s (~)0.479s61.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.715s (-33.2% 🟢)3.833s (-27.1% 🟢)1.117s81.00x
▲ VercelExpress2.796s (+4.1%)4.021s (-4.6%)1.225s81.03x
▲ VercelNitro4.002s (~)5.417s (~)1.415s61.47x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.583s (-0.5%)4.014s (~)0.431s81.00x
🐘 PostgresExpress3.588s (-0.5%)4.016s (~)0.428s81.00x
🐘 PostgresNext.js (Turbopack)3.791s (-1.8%)4.015s (-3.0%)0.224s81.06x
🌐 RedisNext.js (Turbopack)4.182s (+1.3%)5.011s (~)0.829s61.17x
💻 LocalNext.js (Turbopack)8.369s (+22.4% 🔺)8.771s (+16.7% 🔺)0.403s42.34x
💻 LocalExpress8.506s (+8.1% 🔺)9.025s (+9.1% 🔺)0.519s42.37x
💻 LocalNitro9.224s (+4.6%)9.774s (+2.6%)0.550s42.57x
🌐 MongoDBNext.js (Turbopack)10.023s (+0.5%)10.349s (~)0.326s32.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.732s (-5.8% 🟢)4.067s (-8.4% 🟢)1.334s81.00x
▲ VercelExpress2.808s (-8.7% 🟢)3.854s (-18.6% 🟢)1.046s81.03x
▲ VercelNext.js (Turbopack)3.334s (-6.8% 🟢)4.637s (-9.1% 🟢)1.303s71.22x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.171s (+20.7% 🔺)1.002s (~)0.012s (+35.6% 🔺)1.018s (~)0.848s101.00x
🌐 RedisNext.js (Turbopack)0.172s (-3.4%)1.000s (~)0.002s (+15.4% 🔺)1.008s (~)0.835s101.01x
💻 LocalExpress0.199s (+35.9% 🔺)1.003s (~)0.011s (-1.8%)1.017s (~)0.817s101.17x
💻 LocalNitro0.200s (-1.3%)1.003s (~)0.012s (~)1.018s (~)0.818s101.17x
🐘 PostgresNitro0.220s (+4.2%)0.996s (~)0.002s (~)1.012s (~)0.793s101.29x
🐘 PostgresExpress0.221s (-1.3%)0.992s (~)0.002s (+21.4% 🔺)1.013s (~)0.792s101.29x
🐘 PostgresNext.js (Turbopack)0.241s (+11.2% 🔺)1.003s (~)0.002s (+7.1% 🔺)1.019s (+0.5%)0.778s101.41x
🌐 MongoDBNext.js (Turbopack)0.510s (+5.7% 🔺)0.937s (-3.1%)0.001s (~)1.009s (~)0.499s102.99x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.662s (-1.5%)2.422s (-4.6%)0.005s (-34.2% 🟢)15.088s (+377.3% 🔺)13.426s101.00x
▲ VercelExpress1.814s (+2.3%)2.919s (+2.6%)0.006s (+42.9% 🔺)15.475s (+340.0% 🔺)13.661s101.09x
▲ VercelNext.js (Turbopack)1.837s (+14.7% 🔺)2.956s (+5.4% 🔺)0.005s (+20.0% 🔺)3.522s (+5.1% 🔺)1.685s101.11x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresExpress6/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds run failure classification via standardized error codes and threads those codes through runtime events into the UI, while also tightening up debug/error logging in world implementations.

Changes:

  • Introduces RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) and classifyRunError() to populate run_failed.eventData.errorCode.
  • Updates Web UI status badge to surface/copy error codes (when present) instead of raw error details.
  • Adjusts world-local queue error logging and world-vercel schema-validation error messaging / debug gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsTweaks DEBUG behavior and improves schema validation error formatting (with optional debug context).
packages/world-local/src/queue.tsSimplifies queue failure logs and standardizes log prefixes/metadata.
packages/web/app/components/display-utils/status-badge.tsxShows a tooltip for failed statuses that exposes/copies StructuredError.code.
packages/errors/src/index.tsRe-exports new error code constants/types from error-codes.
packages/errors/src/error-codes.tsDefines canonical run error codes and exported union type.
packages/core/src/runtime.tsClassifies caught workflow errors and includes errorCode in run_failed events.
packages/core/src/classify-error.tsAdds helper to classify errors into run error codes.
packages/core/src/classify-error.test.tsUnit tests for classifyRunError.
packages/core/e2e/e2e.test.tsExtends e2e assertions to verify error codes are present in surfaced errors and CLI output.
.changeset/classify-run-error-codes.mdPublishes patch bumps and documents the feature/logging changes.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment threadpackages/world-vercel/src/utils.ts
Comment on lines 360 to 386
@@ -374,7 +381,7 @@ export function workflowEntrypoint(
message: errorMessage,
stack: errorStack,
},
// TODO: include error codes when we define them
errorCode,
},
},

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.

Fixed — the setup failure path now includes errorCode: RUN_ERROR_CODES.RUNTIME_ERROR in the run_failed event data. Both run_failed emission sites are now consistent.

@VaguelySeriousVaguelySerious 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.

First pass by Claude:

1. DEBUG truthiness change leaks Authorization Bearer tokens in logs

The DEBUG check changed from process.env.DEBUG === '1' to process.env.DEBUG (truthy). The curl-reproduction debug block at line 302 stringifies all request headers, including Authorization: Bearer <token>. The debug npm package (a dependency of @workflow/core) commonly sets DEBUG=workflow:* or DEBUG=*, which is now truthy and triggers token emission to stderr. This flows into Vercel deployment logs, Datadog log drains, and CI output.

Suggestion: Either (a) revert to process.env.DEBUG === '1' for the curl block specifically, or (b) redact the Authorization header before logging. The schema-validation debug context at line 369 is less sensitive and can use truthy.

// Option (a): revert strict check for curl blockif(process.env.DEBUG==='1'){// Option (b): redact sensitive headersconstsafeHeaders=Array.from(headers.entries()).filter(([key])=>key.toLowerCase()!=='authorization').map(([key,value])=>`-H "${key}: ${value}"`).join(' ');

2. UI regression: StatusBadge tooltip lost for runs without error code

The old ErrorStatusBadge showed a tooltip with the full error message whenever status === 'failed' && context?.error. The new ErrorCodeBadge only renders when getErrorCode(context?.error) returns a string. This means:

  • Historical failed runs pre-dating this PR have no .code on their StructuredError -- tooltip silently disappears
  • Step failures never populate errorCode (only run_failed events do) -- no tooltip for failed steps
  • The error message itself is no longer shown anywhere in the tooltip -- only the code string like USER_ERROR

Suggestion: Keep ErrorCodeBadge for runs with a code, but restore a fallback that shows the error message when no code is present. Consider a two-tier tooltip: error code (if present) + error message.


3. errorCode accepts arbitrary strings in wire schemas

Both WorkflowRunWireBaseSchema and RunFailedEventSchema define errorCode: z.string().optional(). The domain defines only two valid codes (USER_ERROR, RUNTIME_ERROR), but validation doesn't enforce this at the boundary.

Suggestion: Use z.enum(['USER_ERROR', 'RUNTIME_ERROR']).optional() on the write path (event creation). On the read path (wire schema), z.string().optional() is acceptable for forward-compatibility with newer server versions.


4. Misleading retry denominator in queue log message

attempt ${attempt + 1}/${attempt + 1 + defaultRetriesLeft} works by arithmetic coincidence because defaultRetriesLeft is decremented before this line. If retry logic changes (e.g., defaultRetriesLeft is incremented on timeout at line 165), the denominator inflates.

Suggestion: Capture total before the loop:

constmaxAttempts=defaultRetriesLeft+1;// then in the loop:`attempt ${attempt+1}/${maxAttempts}`

5. Stale TODO in StructuredError.code definition

Comment says // TODO: currently unused. make this an enum maybe but this PR actively populates it.

Suggestion: Update to reflect current state: // Populated with RunErrorCode values (USER_ERROR, RUNTIME_ERROR) for run_failed events


6. Missing OTEL span attribute for errorCode

The errorCode is logged to runtimeLogger but NOT set as an OTEL span attribute. Span attributes include WorkflowRunStatus, WorkflowErrorName, WorkflowErrorMessage, and ErrorType but omit error classification. Datadog traces cannot filter by USER_ERROR vs RUNTIME_ERROR.

Suggestion: Add Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks (lines 406-409 and 417-421).

@pranaygppranaygp left a comment

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.

Addressed all actionable items:

  1. Auth header leak — Fixed. Authorization header is now filtered out of the curl debug output.

  2. UI tooltip regression — Intentional. The old error message tooltip won't work with encryption (error is behind refs), so we intentionally show only the error code. No tooltip for runs without a code is the expected behavior for backward compat.

  3. z.string() vs z.enum — Keeping z.string(). The domain types enforce valid codes at the TypeScript level. Using z.enum on the wire would break forward-compat when new error codes are added.

  4. Misleading retry denominator — Fixed. Captured maxAttempts before the loop.

  5. Stale TODO — Fixed. Updated comment to reflect current state.

  6. Missing OTEL span attribute — Fixed. Added Attribute.WorkflowErrorCode(errorCode) to both span attribute blocks.

@VaguelySeriousVaguelySerious 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.

LGTM, left a few pieces of human feedback.

Also Claude says Steps can fail but have no classification. A step failure that bubbles up to "run_failed" gets a code only at the run level. Document this as an intentional Phase 1 scope limitation if step-level classification is planned.

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(errorMessage);
await navigator.clipboard.writeText(errorCode);

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.

The Clipboard API throws on unfocused pages, non-HTTPS contexts, or permission denial. The existing copyable-text.tsx:28-34 correctly wraps this in try/catch - this component does not. Could fix.

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.

Fixed — wrapped in try/catch.

@@ -90,12 +90,16 @@ export function serializeError<T extends { error?: StructuredError }>(
* status), but the transformation preserves all other fields correctly.
*/
export function deserializeError<T extends Record<string, any>>(obj: any): T {

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.

Changes here are missing unit tests but fine IMO

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.

Ack — the errorCode merging in deserializeError is covered implicitly by the e2e tests that assert error.cause.code === 'USER_ERROR' end-to-end.

expect(WorkflowRunFailedError.is(error)).toBe(true);
assert(WorkflowRunFailedError.is(error));
expect(error.cause.message).toContain('Nested workflow error');
expect(error.cause.code).toBe('USER_ERROR');

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.

We should add one e2e test that actually causes + asserts a RUNTIME_ERROR

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.

Triggering RUNTIME_ERROR in e2e requires corrupting the event log (e.g., injecting an unexpected event type for a step). This is fragile and environment-dependent. We have unit test coverage for WorkflowRuntimeError classification in classify-error.test.ts and for the error itself in step.test.ts. Could add an e2e in a follow-up with a dedicated fault injection mechanism.

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.

human: I tried this but not possible without chaos testing or a bigger change out of scope of this PR. tl;dr we can't easily inject failures into runs (we do on steps by injecting 500s into the world) but for workflow/run code - we would either need to expose things into the VM to allow it to inject that (i.e. changing runtime code just for a fault injection test) - or another ideas is we need to use a proxy in front of workflow-server and queue to inject these failures

at that point I'm thinking we just do this in the chaos testing @TooTallNate is setting up and we can have validation that it's working once that's up

* These are populated in the `errorCode` field of `run_failed` events
* and flow through to `StructuredError.code` on the run entity.
*/
export const RUN_ERROR_CODES = {

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.

If these are also user-facing, should add to docs

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 call — will add docs in a follow-up. These are user-facing via WorkflowRunFailedError.cause.code.

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.

Added docs in #1445 — documents error codes in the errors & retries guide.

- Add RUN_ERROR_CODES (USER_ERROR, RUNTIME_ERROR) to @workflow/errors
- Populate errorCode in run_failed events via classifyRunError()
- Update web UI StatusBadge to show amber dot for infrastructure errors
- Improve world-local queue error logging (concise, no body dump)
- Improve schema validation error messages (concise, verbose behind DEBUG)
- Add e2e tests for error code flow and infrastructure error retry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pranaygp

pranaygp commented Mar 18, 2026

Copy link
Copy Markdown
ContributorAuthor

human: Merging this so I can unblock the next PR. good call on the docs though - I've started another PR for that rather than this one so I don't have to wait for CI again for a docs change

@pranaygppranaygp left a comment

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.

Docs PR for error codes: #1445

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.

3 participants

@pranaygp@VaguelySerious