Skip to content

[world-vercel] Switch event endpoints to v4 wire format#2055

Draft
VaguelySerious wants to merge 24 commits into
mainfrom
peter/v4
Draft

[world-vercel] Switch event endpoints to v4 wire format#2055
VaguelySerious wants to merge 24 commits into
mainfrom
peter/v4

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented May 21, 2026

Draft. Switches the world-vercel adapter's event endpoints from v2/v3 to v4. Companion to workflow-server PR #439; both land together.

What changes

The adapter's createWorkflowRunEvent / getEvent / getWorkflowRunEvents keep their public signatures and the EventResult / Event / PaginatedResponse<Event> shapes the workflow runtime consumes. What changes is what's on the wire under those calls:

  • Event metadata rides in x-wf-* HTTP request/response headers instead of inside the body.
  • User payloads stream end-to-end as opaque bytes. The SDK CBOR-encodes at the eventData[field] boundary on write and CBOR-decodes on read; the server treats the bytes as opaque and streams them straight to S3.
  • POST event response carries the materialized EventResult as a CBOR body. The runtime reads e.g. result.run.startedAt immediately after POST without a second round-trip.
  • LIST events response is now the v4 binary-frame stream (application/vnd.workflow.v4-frames). One frame per event with CBOR metadata + raw payload bytes inline. The per-event /refs round-trip used by the v3 client is gone.

What goes away

  • packages/world-vercel/src/refs.ts — deleted. The /refs hydration path is no longer used.
  • hydrateEventRefs / collectPendingRefs / eventDataRefFieldMap and the wire schemas (EventResultResolveWireSchema, EventResultLazyWireSchema, EventWithRefsSchema) — deleted from events.ts.
  • The lazy-refs branching in createWorkflowRunEvent — the server still respects remoteRefBehavior (passed via header for eventsNeedingResolve types) and bakes the resolution decision into its CBOR response, so the SDK has nothing to do.

Net diff: +297 / -601 lines on this PR.

What stays

  • v1Compat path in createWorkflowRunEvent — still uses /v1 endpoints for legacy SDK migrations that haven't moved to event sourcing. v4 doesn't cover these.
  • validateUlidTimestamp on run_created, the HookNotFoundError translation on hook 404s, and the stripEventDataRefs path for resolveData='none'.
  • events-v4.ts is now an internal helper module — not re-exported from the package's public API.

Not yet covered (by design)

  • storage.events.listByCorrelationId throws a clear error explaining the v4 server has no by-correlation-id list endpoint. The runtime mostly used this for hook lookups, which can use storage.hooks.getByToken instead. If real callers need it, a follow-up server PR can add /api/v4/events?correlationId=.

Test plan

  • pnpm --filter @workflow/world-vercel build clean
  • pnpm --filter @workflow/world-vercel test — 79/79 pass
  • pnpm build (full workspace) — 27/27 packages build
  • WORKFLOW_SERVER_URL_OVERRIDE on this branch points at the workflow-server PR Fix command injection vulnerability in CI workflow via untrusted fork PR #439 preview deployment for e2e tests.
  • E2E green against the v4-enabled workflow-server preview.

🤖 Generated with Claude Code

Mirrors the v4 server-side handlers landing in workflow-server. The
v4 wire format moves event metadata into x-wf-* request/response
headers and treats payloads as opaque user-data bytes (streamed
end-to-end). The SDK passes Uint8Array bytes through unchanged at
this layer; higher-level world-vercel adapter glue handles CBOR.

Adds:
  - packages/world-vercel/src/frames.ts: encoder + async-iterable
    decoder for the length-prefixed binary frame format used by the
    v4 list-events response.
  - packages/world-vercel/src/events-v4.ts: three new functions:
    * createWorkflowRunEventV4 — POST with x-wf-* headers + payload
      bytes, returns event/run ids and timestamp from response
      headers.
    * getEventV4 — GET single event, returns metadata + body bytes.
    * getWorkflowRunEventsV4 — GET list, parses frame stream, returns
      events + pagination cursor.
  - V4_HEADERS exported as the canonical name map; mirrors the
    server-side constant.

V4 client characteristics:
  - Required runId in URL for run_created too (no /runs/null/events
    shortcut; the runId is part of the S3 key the server allocates).
    Higher-level callers generate the ULID client-side.
  - Payload bytes flow through without CBOR encode/decode on this
    layer. Callers CBOR-encode for parity with v3 if they want.
  - Pagination cursor surfaces in the LIST response — eliminates the
    per-large-payload /refs round-trip used by v2/v3.

Tests (10 new in src/frames.test.ts, no new e2e):
  - Canonical wire layout round-trip.
  - Multi-frame round-trip with pagination cursor.
  - Decoder survives 1-byte chunk delivery (matching spike B's chunk-
    boundary robustness requirement).
  - 64 KB body split across many small chunks.
  - Bodies containing 0xff padding don't mis-frame.
  - Back-to-back frames in a single chunk.
  - Truncated stream raises.
  - Meta CBOR types (numbers, booleans, arrays) preserved.

The world-vercel adapter still defaults to the v3 path; v4 is exposed
for direct callers and a follow-up PR will switch the adapter over
once the matching server-side PR is on staging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: 3d03e5d

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

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

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

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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 21, 2026

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1067 155 219 1441
✅ 💻 Local Development 1615 0 219 1834
❌ 📦 Local Production 1614 1 219 1834
✅ 🐘 Local Postgres 1615 0 219 1834
✅ 🪟 Windows 131 0 0 131
❌ 📋 Other 736 5 176 917
Total 6778 161 1052 7991

❌ Failed Tests

▲ Vercel Production (155 failed)

astro (6 failed):

  • readableStreamWorkflow | wrun_01KSJVGBMY51ARX0CJ0K70PC8E | 🔍 observability
  • stepWinsRaceWorkflow | wrun_01KSJVHVNWC5C9QF36ZY19S1PY | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_01KSJVJ1W34NXG0Q33CCJ425KN | 🔍 observability
  • AbortController abortAlreadyAbortedWorkflow: pre-aborted signal seen by step
  • AbortController abortAfterCompletionWorkflow: abort after step completes is a no-op
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

example (3 failed):

  • AbortController abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • AbortController abortVoidSleepTimeoutWorkflow: documented void sleep().then(abort) pattern works
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

express (2 failed):

  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KSJW105F0SSRYY1V3AB1AC3N | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

fastify (2 failed):

  • parallelSleepWorkflow | wrun_01KSJVHMA9T6SD9WM7DGNQJEFW | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

hono (2 failed):

  • sleepWinsRaceWorkflow | wrun_01KSJVHQXJM7M6XXD896C7707E | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

nextjs-turbopack (12 failed):

  • DurableAgent e2e prepareCall (GAP) completes but prepareCall is not applied (GAP)
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KSJW105F0SSRYY1V3AB1AC3N | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KSJW6MWQYG3B87GWSDC1MWYQ | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KSJW70ZNN5XZ3B4FT61K8S62 | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KSJW97VQNBJKM66TMDCBW4JS | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KSJW9DR1R4P4TCB0P1C3YY9T | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KSJW9KFH35XDMVS6WWHNT19J | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KSJWB47A37ZTXPAARGW3N68Y | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_01KSJWBTRG216TXAT09J6QCN3T | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KSJWCNM9GY3VC73R4GADCW7B | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

nextjs-webpack (116 failed):

  • DurableAgent e2e core basic text response
  • DurableAgent e2e core single tool call
  • DurableAgent e2e core multiple sequential tool calls
  • DurableAgent e2e core tool error recovery
  • DurableAgent e2e onStepFinish fires constructor + stream callbacks in order with step data
  • DurableAgent e2e onFinish fires constructor + stream callbacks in order with event data
  • DurableAgent e2e provider tools provider tool identity preserved across step boundaries
  • DurableAgent e2e provider tools mixed provider and function tools
  • DurableAgent e2e instructions string instructions are passed to the model
  • DurableAgent e2e timeout completes within timeout
  • DurableAgent e2e experimental_onStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onStepStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onToolCallStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onToolCallFinish (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e prepareCall (GAP) completes but prepareCall is not applied (GAP)
  • DurableAgent e2e prepareStep on constructor agent-level prepareStep is called for each LLM step
  • DurableAgent e2e prepareStep on constructor stream-level prepareStep overrides constructor-level
  • DurableAgent e2e multimodal tool results passes through LanguageModelV3ToolResultOutput from tools
  • DurableAgent e2e tool approval (GAP) completes but needsApproval is not checked (GAP)
  • addTenWorkflow | wrun_01KSJVFAVKEWRXFVKDY3VG7JB0 | 🔍 observability
  • addTenWorkflow | wrun_01KSJVFAVKEWRXFVKDY3VG7JB0 | 🔍 observability
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KSJVG92CXD0E8P7ASK35R87F | 🔍 observability
  • promiseAllWorkflow | wrun_01KSJVFGYA8DG945WMG8JS5SVQ | 🔍 observability
  • promiseRaceWorkflow | wrun_01KSJVFP792HKKYMHFYG600JBH | 🔍 observability
  • promiseAnyWorkflow | wrun_01KSJVG256VPVKHE6BBX4G1KBT | 🔍 observability
  • importedStepOnlyWorkflow | wrun_01KSJVGZGEVF63YD6S2M7FDSM6 | 🔍 observability
  • readableStreamWorkflow | wrun_01KSJVGBMY51ARX0CJ0K70PC8E | 🔍 observability
  • hookWorkflow | wrun_01KSJVGNF01YAWW6WHPKNTWAAP | 🔍 observability
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KSJVGX6VJXEMB1CD92RMKAQQ | 🔍 observability
  • webhookWorkflow | wrun_01KSJVH27YQ4270XMAKWJTSGG7 | 🔍 observability
  • sleepingWorkflow | wrun_01KSJVH7XR2J0CPMMEWJASGH1S | 🔍 observability
  • parallelSleepWorkflow | wrun_01KSJVHMA9T6SD9WM7DGNQJEFW | 🔍 observability
  • sleepWinsRaceWorkflow | wrun_01KSJVHQXJM7M6XXD896C7707E | 🔍 observability
  • stepWinsRaceWorkflow | wrun_01KSJVHVNWC5C9QF36ZY19S1PY | 🔍 observability
  • nullByteWorkflow | wrun_01KSJVHZMQZN4GHV55ZN44FKYX | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_01KSJVJ1W34NXG0Q33CCJ425KN | 🔍 observability
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions | wrun_01KSJVNM6ZE69HC7XHA6B32MRG | 🔍 observability
  • utf8StreamWorkflow | wrun_01KSJVP1E90PC40YV4GEVA915X | 🔍 observability
  • writableForwardedFromWorkflowWorkflow | wrun_01KSJVP7DFARERDANPKHNCDQ8R | 🔍 observability
  • writableForwardedFromStepWorkflow | wrun_01KSJVPA07AKE53MBACZ1D2H00 | 🔍 observability
  • fetchWorkflow | wrun_01KSJVPC7R1XYFH9T85WTXYH1X | 🔍 observability
  • promiseRaceStressTestWorkflow | wrun_01KSJVPEC6HTDM5QZ1YN6WFF7F | 🔍 observability
  • 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 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()
  • error handling catchability step throw round-trips FatalError with cause chain to workflow catch
  • error handling catchability step throw of a non-Error value preserves it as cause on the wrapping FatalError
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KSJVY59HJ09WNQ4ZJY9TMX76 | 🔍 observability
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KSJVYJB3JHEYFDPFR9S1VN9N | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KSJVZ3CA0R31X63E0BG34K44 | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KSJVZMQR8QJPY1TNP0J4TZG1 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KSJVZY761K79TEB9CP5EFHA9 | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KSJW03XX1VRYEBB2NYHPA9TT | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KSJW06687E9KZF64MW3W22VD | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KSJW0GXGZCWVVTR6AHP0VQRS | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_01KSJW0WE8XZFQCCYD8DPFTFYR | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KSJW105F0SSRYY1V3AB1AC3N | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KSJW6MWQYG3B87GWSDC1MWYQ | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KSJW6V5PDDQ4DY3X12VMCS69 | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KSJW70ZNN5XZ3B4FT61K8S62 | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KSJW8VZ88KN2N8ZAAQJ8M93J | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KSJW97VQNBJKM66TMDCBW4JS | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KSJW9DR1R4P4TCB0P1C3YY9T | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KSJW9KFH35XDMVS6WWHNT19J | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KSJW9XZZH84TNE5E11QYR1VB | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_01KSJWA4W60ZVB234DPSSSXMNH | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KSJWA8DZ0X8M4PVVF65S1ADR | 🔍 observability
  • 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_01KSJWB47A37ZTXPAARGW3N68Y | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_01KSJWBTRG216TXAT09J6QCN3T | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KSJWCB32G36J32KSV0ANR843 | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KSJWCNM9GY3VC73R4GADCW7B | 🔍 observability
  • AbortController abortTimeoutWorkflow: timeout cancels long-running step
  • AbortController abortParallelWorkflow: abort cancels all parallel steps
  • AbortController abortFromStepWorkflow: step abort cancels an in-flight sibling step
  • AbortController abortAlreadyAbortedWorkflow: pre-aborted signal seen by step
  • AbortController abortReasonWorkflow: abort reason preserved across boundaries
  • AbortController abortAfterCompletionWorkflow: abort after step completes is a no-op
  • AbortController abortViaHookWorkflow: external hook triggers abort on in-flight step
  • AbortController abortExternalSignalWorkflow: signal passed as workflow input
  • AbortController abortExternalSignalInFlightWorkflow: external abort fires mid-flight, propagates to nested steps
  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals
  • AbortController abortSurvivesReplayWorkflow: controller state consistent across replay
  • AbortController abortThrowIfAbortedWorkflow: throwIfAborted causes FatalError, no retries
  • AbortController abortReasonTypesWorkflow: various abort reason types propagate correctly
  • AbortController abortFetchUncaughtWorkflow: uncaught fetch AbortError is FatalError, no retries
  • AbortController abortFetchInFlightWorkflow: aborting cancels an in-flight fetch
  • AbortController abortVoidSleepTimeoutWorkflow: documented void sleep().then(abort) pattern works
  • AbortController abortDeterministicBranchWorkflow: if-check takes same path on first-run and replay
  • AbortController abortListenerWorkflow: signal.addEventListener fires on the deserialized step signal
  • AbortController abortThrowIfAbortedMidFlightWorkflow: throwIfAborted in a polling loop bails when abort fires
  • AbortController abortDeterministicBranchFromStepWorkflow: branches stay consistent when abort comes from a step
  • AbortController abortHookOrderingWorkflow [listener-first-abort-first]: addEventListener → hook.then → abort() → resumeHook
  • AbortController abortHookOrderingWorkflow [listener-first-hook-first]: addEventListener → hook.then → resumeHook → abort()
  • AbortController abortHookOrderingWorkflow [hook-first-abort-first]: hook.then → addEventListener → abort() → resumeHook
  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KSJWHHQGAQR61R1SE18VS0ET | 🔍 observability
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KSJWHKX0S8DPTPA1K6MQVHF4 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KSJWJ1E3B37E5C9Z8Y5QM953 | 🔍 observability
  • distributedAbortController - manual abort triggers signal | wrun_01KSJWJ7CA6CZ17NN6R8K8KNBW | 🔍 observability
  • distributedAbortController - TTL expiration triggers signal | wrun_01KSJWJARWN8DQKVCBG41BDW5R | 🔍 observability
  • distributedAbortController - reconnect to existing controller | wrun_01KSJWJFCTJDM32K8T2CFCJP61 | 🔍 observability

nitro (1 failed):

  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

nuxt (2 failed):

  • AbortController abortHookOrderingWorkflow [hook-first-abort-first]: hook.then → addEventListener → abort() → resumeHook
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

sveltekit (2 failed):

  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KSJW105F0SSRYY1V3AB1AC3N | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability

vite (7 failed):

  • outputStreamWorkflow positive startIndex (skips first chunk)
  • error handling retry behavior maxRetries=0 disables retries
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KSJW105F0SSRYY1V3AB1AC3N | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KSJW70ZNN5XZ3B4FT61K8S62 | 🔍 observability
  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR | 🔍 observability
📦 Local Production (1 failed)

hono-stable (1 failed):

  • AbortController abortDeterministicBranchFromStepWorkflow: branches stay consistent when abort comes from a step
📋 Other (5 failed)

e2e-vercel-prod-tanstack-start (5 failed):

  • outputStreamWorkflow positive startIndex (skips first chunk)
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_01KSJWBTRG216TXAT09J6QCN3T
  • AbortController abortSurvivesReplayWorkflow: controller state consistent across replay
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KSJWHP50X7SEG73BWVCNP3SR

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
❌ astro 99 6 26
❌ example 102 3 26
❌ express 103 2 26
❌ fastify 103 2 26
❌ hono 103 2 26
❌ nextjs-turbopack 117 12 2
❌ nextjs-webpack 13 116 2
❌ nitro 104 1 26
❌ nuxt 103 2 26
❌ sveltekit 122 2 7
❌ vite 98 7 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
❌ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
❌ hono-stable 105 1 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 131 0 0
❌ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 106 0 25
✅ e2e-local-dev-tanstack-start- 106 0 25
✅ e2e-local-postgres-nest-stable 106 0 25
✅ e2e-local-postgres-tanstack-start- 106 0 25
✅ e2e-local-prod-nest-stable 106 0 25
✅ e2e-local-prod-tanstack-start- 106 0 25
❌ e2e-vercel-prod-tanstack-start 100 5 26

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.030s (-29.9% 🟢) 1.005s (~) 0.975s 10 1.00x
💻 Local Express 0.032s (-28.2% 🟢) 1.006s (~) 0.974s 10 1.05x
🐘 Postgres Express 0.047s (-18.4% 🟢) 1.011s (~) 0.964s 10 1.57x
💻 Local Next.js (Turbopack) 0.048s 1.007s 0.960s 10 1.57x
🐘 Postgres Nitro 0.050s (-47.2% 🟢) 1.011s (-3.0%) 0.961s 10 1.67x
🐘 Postgres Next.js (Turbopack) 0.054s 1.011s 0.957s 10 1.78x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.298s (+18.5% 🔺) 2.005s (-14.1% 🟢) 1.707s 10 1.00x
▲ Vercel Nitro 0.347s (-15.3% 🟢) 2.122s (-15.5% 🟢) 1.775s 10 1.16x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.072s (-5.2% 🟢) 2.006s (~) 0.933s 10 1.00x
💻 Local Express 1.073s (-4.7%) 2.007s (~) 0.934s 10 1.00x
🐘 Postgres Express 1.083s (-5.5% 🟢) 2.010s (~) 0.927s 10 1.01x
🐘 Postgres Nitro 1.084s (-4.9%) 2.010s (~) 0.927s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.114s 2.008s 0.894s 10 1.04x
💻 Local Next.js (Turbopack) 1.116s 2.005s 0.890s 10 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.689s (-56.6% 🟢) 3.255s (-44.9% 🟢) 1.566s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.313s (+13.7% 🔺) 3.885s (+1.4%) 1.572s 10 1.37x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.416s (-4.2%) 11.015s (~) 0.599s 3 1.00x
💻 Local Nitro 10.423s (-4.8%) 11.022s (~) 0.599s 3 1.00x
🐘 Postgres Express 10.425s (-4.9%) 11.014s (~) 0.589s 3 1.00x
💻 Local Express 10.431s (-4.5%) 11.023s (~) 0.591s 3 1.00x
💻 Local Next.js (Turbopack) 10.687s 11.022s 0.335s 3 1.03x
🐘 Postgres Next.js (Turbopack) 10.721s 11.018s 0.297s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.588s (-42.7% 🟢) 14.847s (-40.9% 🟢) 1.259s 3 1.00x
▲ Vercel Next.js (Turbopack) 14.023s (-19.0% 🟢) 15.543s (-19.9% 🟢) 1.520s 2 1.03x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 13.451s (-7.8% 🟢) 14.018s (-6.7% 🟢) 0.566s 5 1.00x
💻 Local Nitro 13.473s (-10.6% 🟢) 14.027s (-12.5% 🟢) 0.554s 5 1.00x
🐘 Postgres Express 13.476s (-7.6% 🟢) 14.024s (-6.6% 🟢) 0.548s 5 1.00x
💻 Local Express 13.511s (-9.7% 🟢) 14.028s (-6.7% 🟢) 0.517s 5 1.00x
🐘 Postgres Next.js (Turbopack) 14.039s 15.017s 0.979s 4 1.04x
💻 Local Next.js (Turbopack) 14.051s 15.029s 0.978s 4 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 20.998s (-67.4% 🟢) 22.557s (-66.1% 🟢) 1.559s 3 1.00x
▲ Vercel Next.js (Turbopack) 21.520s (-59.1% 🟢) 23.337s (-57.3% 🟢) 1.818s 3 1.02x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.893s (-29.1% 🟢) 12.021s (-29.4% 🟢) 0.129s 8 1.00x
💻 Local Express 12.010s (-27.7% 🟢) 12.523s (-26.5% 🟢) 0.513s 8 1.01x
🐘 Postgres Nitro 12.043s (-13.8% 🟢) 12.518s (-12.5% 🟢) 0.475s 8 1.01x
🐘 Postgres Express 12.049s (-14.0% 🟢) 12.647s (-13.3% 🟢) 0.597s 8 1.01x
🐘 Postgres Next.js (Turbopack) 12.975s 13.302s 0.327s 7 1.09x
💻 Local Next.js (Turbopack) 13.327s 14.169s 0.842s 7 1.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 30.210s (-92.9% 🟢) 31.474s (-92.6% 🟢) 1.263s 3 1.00x
▲ Vercel Next.js (Turbopack) 31.650s (-92.0% 🟢) 32.849s (-91.7% 🟢) 1.199s 3 1.05x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.142s (-9.4% 🟢) 2.007s (~) 0.865s 15 1.00x
🐘 Postgres Nitro 1.145s (-10.2% 🟢) 2.008s (~) 0.863s 15 1.00x
💻 Local Nitro 1.159s (-29.0% 🟢) 2.006s (-3.3%) 0.847s 15 1.02x
💻 Local Express 1.177s (-21.0% 🟢) 2.006s (~) 0.830s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.214s 2.007s 0.793s 15 1.06x
💻 Local Next.js (Turbopack) 1.301s 2.006s 0.705s 15 1.14x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.553s (-9.4% 🟢) 4.003s (-7.4% 🟢) 1.450s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.692s (-20.8% 🟢) 4.085s (-17.2% 🟢) 1.393s 8 1.05x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.210s (-48.5% 🟢) 2.008s (-33.3% 🟢) 0.798s 15 1.00x
🐘 Postgres Express 1.221s (-48.3% 🟢) 2.008s (-33.3% 🟢) 0.787s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.329s 2.007s 0.678s 15 1.10x
💻 Local Nitro 1.625s (-48.3% 🟢) 2.006s (-48.4% 🟢) 0.381s 15 1.34x
💻 Local Express 1.834s (-37.9% 🟢) 2.151s (-37.7% 🟢) 0.316s 14 1.52x
💻 Local Next.js (Turbopack) 1.860s 2.074s 0.213s 15 1.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.176s (-41.2% 🟢) 5.483s (-38.4% 🟢) 1.307s 6 1.00x
▲ Vercel Nitro 6.726s (+66.0% 🔺) 8.136s (+37.4% 🔺) 1.410s 4 1.61x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.320s (-62.1% 🟢) 2.009s (-49.9% 🟢) 0.689s 15 1.00x
🐘 Postgres Express 1.349s (-61.3% 🟢) 2.008s (-49.9% 🟢) 0.659s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.603s 2.073s 0.470s 15 1.21x
💻 Local Nitro 4.547s (-45.5% 🟢) 5.010s (-44.5% 🟢) 0.463s 6 3.45x
💻 Local Next.js (Turbopack) 5.293s 6.012s 0.719s 5 4.01x
💻 Local Express 5.368s (-35.6% 🟢) 6.013s (-33.4% 🟢) 0.645s 5 4.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.389s (+81.2% 🔺) 7.737s (+39.8% 🔺) 1.348s 4 1.00x
▲ Vercel Next.js (Turbopack) 66.234s (+642.9% 🔺) 68.327s (+523.4% 🔺) 2.092s 5 10.37x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.156s (-8.1% 🟢) 2.009s (~) 0.853s 15 1.00x
🐘 Postgres Nitro 1.156s (-8.0% 🟢) 2.008s (~) 0.852s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.202s 2.007s 0.805s 15 1.04x
💻 Local Nitro 1.324s (-29.0% 🟢) 2.006s (-14.3% 🟢) 0.682s 15 1.15x
💻 Local Next.js (Turbopack) 1.357s 2.006s 0.649s 15 1.17x
💻 Local Express 1.412s (-25.4% 🟢) 2.006s (-15.1% 🟢) 0.594s 15 1.22x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.584s (+5.1% 🔺) 3.945s (-5.4% 🟢) 1.361s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.697s (-8.0% 🟢) 4.389s (-5.5% 🟢) 1.692s 7 1.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.208s (-48.4% 🟢) 2.008s (-33.3% 🟢) 0.799s 15 1.00x
🐘 Postgres Express 1.215s (-48.1% 🟢) 2.008s (-33.3% 🟢) 0.794s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.340s 2.008s 0.668s 15 1.11x
💻 Local Nitro 2.011s (-34.4% 🟢) 2.391s (-38.5% 🟢) 0.380s 13 1.66x
💻 Local Express 2.079s (-33.6% 🟢) 2.509s (-33.3% 🟢) 0.430s 12 1.72x
💻 Local Next.js (Turbopack) 2.150s 3.008s 0.857s 10 1.78x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.301s (+5.1% 🔺) 4.621s (+2.2%) 1.319s 7 1.00x
▲ Vercel Nitro 3.446s (+6.6% 🔺) 4.847s (-4.5%) 1.400s 7 1.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.322s (-62.0% 🟢) 2.007s (-49.9% 🟢) 0.685s 15 1.00x
🐘 Postgres Express 1.332s (-61.9% 🟢) 2.008s (-49.9% 🟢) 0.676s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.573s 2.007s 0.434s 15 1.19x
💻 Local Nitro 4.928s (-46.1% 🟢) 5.343s (-46.7% 🟢) 0.415s 6 3.73x
💻 Local Next.js (Turbopack) 5.832s 6.415s 0.583s 5 4.41x
💻 Local Express 6.407s (-27.2% 🟢) 7.015s (-24.3% 🟢) 0.608s 5 4.85x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.047s (-10.5% 🟢) 7.587s (-11.2% 🟢) 1.540s 4 1.00x
▲ Vercel Nitro 6.236s (+22.4% 🔺) 7.710s (+13.1% 🔺) 1.474s 4 1.03x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.444s (-47.0% 🟢) 1.007s (-1.6%) 0.562s 60 1.00x
💻 Local Nitro 0.482s (-50.9% 🟢) 1.021s (-6.6% 🟢) 0.539s 59 1.08x
💻 Local Express 0.498s (-49.4% 🟢) 1.004s (-6.7% 🟢) 0.506s 60 1.12x
🐘 Postgres Nitro 0.515s (-37.3% 🟢) 1.060s (+5.3% 🔺) 0.545s 57 1.16x
🐘 Postgres Next.js (Turbopack) 0.641s 1.006s 0.365s 60 1.44x
💻 Local Next.js (Turbopack) 0.723s 1.005s 0.282s 60 1.63x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.659s (-74.3% 🟢) 7.086s (-70.5% 🟢) 1.427s 9 1.00x
▲ Vercel Next.js (Turbopack) 5.664s (-60.9% 🟢) 6.876s (-57.3% 🟢) 1.212s 9 1.00x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.114s (-42.2% 🟢) 1.922s (-8.5% 🟢) 0.808s 47 1.00x
🐘 Postgres Express 1.151s (-41.7% 🟢) 1.964s (-13.0% 🟢) 0.812s 46 1.03x
💻 Local Nitro 1.185s (-60.9% 🟢) 2.005s (-46.6% 🟢) 0.820s 45 1.06x
💻 Local Express 1.219s (-59.6% 🟢) 2.006s (-44.1% 🟢) 0.787s 45 1.09x
🐘 Postgres Next.js (Turbopack) 1.515s 2.007s 0.492s 45 1.36x
💻 Local Next.js (Turbopack) 1.776s 2.006s 0.231s 45 1.59x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.966s (-64.6% 🟢) 15.401s (-62.7% 🟢) 1.435s 6 1.00x
▲ Vercel Next.js (Turbopack) 15.978s (-67.9% 🟢) 17.415s (-66.3% 🟢) 1.436s 6 1.14x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.085s (-47.7% 🟢) 2.758s (-36.9% 🟢) 0.673s 44 1.00x
🐘 Postgres Nitro 2.134s (-48.0% 🟢) 2.822s (-38.7% 🟢) 0.688s 43 1.02x
💻 Local Nitro 2.624s (-71.8% 🟢) 3.032s (-69.7% 🟢) 0.408s 40 1.26x
💻 Local Express 2.716s (-70.5% 🟢) 3.008s (-70.0% 🟢) 0.291s 40 1.30x
🐘 Postgres Next.js (Turbopack) 3.005s 3.399s 0.393s 36 1.44x
💻 Local Next.js (Turbopack) 3.785s 4.075s 0.290s 30 1.82x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 32.775s (-69.4% 🟢) 35.238s (-67.6% 🟢) 2.463s 4 1.00x
▲ Vercel Nitro 132.522s (+36.8% 🔺) 134.083s (+36.2% 🔺) 1.561s 3 4.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.180s (-36.2% 🟢) 1.006s (~) 0.826s 60 1.00x
🐘 Postgres Nitro 0.189s (-33.3% 🟢) 1.006s (~) 0.817s 60 1.05x
🐘 Postgres Next.js (Turbopack) 0.224s 1.006s 0.782s 60 1.24x
💻 Local Nitro 0.376s (-37.8% 🟢) 1.004s (-1.7%) 0.628s 60 2.09x
💻 Local Express 0.384s (-31.5% 🟢) 1.004s (~) 0.620s 60 2.13x
💻 Local Next.js (Turbopack) 0.523s 1.005s 0.482s 60 2.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.238s (+34.8% 🔺) 3.589s (+7.1% 🔺) 1.351s 17 1.00x
▲ Vercel Next.js (Turbopack) 103.954s (+5039.9% 🔺) 105.033s (+2668.7% 🔺) 1.079s 3 46.44x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.304s (-38.7% 🟢) 1.006s (~) 0.702s 90 1.00x
🐘 Postgres Express 0.309s (-39.3% 🟢) 1.007s (~) 0.697s 90 1.02x
🐘 Postgres Next.js (Turbopack) 0.408s 1.005s 0.597s 90 1.34x
💻 Local Nitro 2.037s (-19.7% 🟢) 2.494s (-17.1% 🟢) 0.457s 37 6.69x
💻 Local Express 2.121s (-15.6% 🟢) 2.796s (-7.1% 🟢) 0.675s 33 6.97x
💻 Local Next.js (Turbopack) 2.296s 2.944s 0.649s 31 7.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.371s (+35.5% 🔺) 6.014s (+24.7% 🔺) 1.643s 15 1.00x
▲ Vercel Next.js (Turbopack) 4.853s (+37.3% 🔺) 6.382s (+22.9% 🔺) 1.529s 15 1.11x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.638s (-19.2% 🟢) 1.007s (~) 0.368s 120 1.00x
🐘 Postgres Express 0.665s (-18.7% 🟢) 1.006s (-1.1%) 0.341s 120 1.04x
🐘 Postgres Next.js (Turbopack) 0.847s 1.006s 0.159s 120 1.33x
💻 Local Nitro 9.385s (-16.1% 🟢) 10.025s (-14.1% 🟢) 0.640s 13 14.70x
💻 Local Express 10.175s (-9.1% 🟢) 10.612s (-11.1% 🟢) 0.437s 12 15.94x
💻 Local Next.js (Turbopack) 10.734s 11.300s 0.566s 11 16.82x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 11.245s (+45.6% 🔺) 13.397s (+42.5% 🔺) 2.152s 9 1.00x
▲ Vercel Next.js (Turbopack) 54.787s (+430.5% 🔺) 57.128s (+365.0% 🔺) 2.341s 7 4.87x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.123s (+425.6% 🔺) 2.005s (+99.6% 🔺) 0.010s (-16.8% 🟢) 2.017s (+98.0% 🔺) 0.894s 10 1.00x
🐘 Postgres Express 1.133s (+452.5% 🔺) 2.003s (+100.6% 🔺) 0.001s (-18.8% 🟢) 2.011s (+98.8% 🔺) 0.878s 10 1.01x
💻 Local Express 1.137s (+471.0% 🔺) 2.006s (+99.7% 🔺) 0.012s (+0.8%) 2.020s (+98.4% 🔺) 0.883s 10 1.01x
🐘 Postgres Nitro 1.144s (+458.2% 🔺) 2.001s (+100.2% 🔺) 0.001s (-6.7% 🟢) 2.012s (+99.0% 🔺) 0.868s 10 1.02x
💻 Local Next.js (Turbopack) 1.196s 2.004s 0.013s 2.020s 0.824s 10 1.06x
🐘 Postgres Next.js (Turbopack) 1.201s 2.002s 0.001s 2.010s 0.809s 10 1.07x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.431s (-36.6% 🟢) 3.375s (-36.0% 🟢) 1.850s (+149.3% 🔺) 5.599s (-13.6% 🟢) 3.168s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.926s (-57.3% 🟢) 3.276s (-62.1% 🟢) 2.079s (+229.1% 🔺) 6.367s (-34.9% 🟢) 3.441s 10 1.20x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.527s (+82.0% 🔺) 2.013s (+98.9% 🔺) 0.010s (+5.3% 🔺) 2.025s (+81.4% 🔺) 0.498s 30 1.00x
💻 Local Express 1.531s (+102.2% 🔺) 2.012s (+95.6% 🔺) 0.010s (+9.1% 🔺) 2.024s (+94.7% 🔺) 0.493s 30 1.00x
🐘 Postgres Nitro 1.533s (+145.6% 🔺) 2.005s (+99.2% 🔺) 0.004s (-5.7% 🟢) 2.026s (+98.1% 🔺) 0.493s 30 1.00x
🐘 Postgres Express 1.585s (+151.5% 🔺) 2.008s (+99.5% 🔺) 0.004s (+3.6%) 2.027s (+98.2% 🔺) 0.443s 30 1.04x
🐘 Postgres Next.js (Turbopack) 1.633s 2.010s 0.004s 2.024s 0.391s 30 1.07x
💻 Local Next.js (Turbopack) 1.683s 2.012s 0.010s 2.025s 0.342s 30 1.10x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.196s (-63.4% 🟢) 8.007s (-56.1% 🟢) 0.213s (+1.0%) 8.594s (-54.6% 🟢) 2.398s 7 1.00x
▲ Vercel Nitro 67.796s (+130.3% 🔺) 69.046s (+124.1% 🔺) 0.235s (+110.2% 🔺) 69.674s (+119.3% 🔺) 1.878s 5 10.94x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.675s (-29.8% 🟢) 1.033s (-19.1% 🟢) 0.000s (-60.3% 🟢) 1.051s (-19.6% 🟢) 0.376s 58 1.00x
🐘 Postgres Nitro 0.677s (-30.1% 🟢) 1.049s (-15.9% 🟢) 0.000s (+26.3% 🔺) 1.060s (-15.7% 🟢) 0.383s 57 1.00x
🐘 Postgres Next.js (Turbopack) 0.719s 1.051s 0.000s 1.061s 0.342s 57 1.07x
💻 Local Express 1.325s (+8.2% 🔺) 2.014s (~) 0.000s (~) 2.016s (~) 0.691s 30 1.96x
💻 Local Nitro 1.353s (+10.6% 🔺) 2.014s (~) 0.000s (+133.3% 🔺) 2.016s (~) 0.663s 30 2.01x
💻 Local Next.js (Turbopack) 1.450s 2.014s 0.000s 2.017s 0.567s 30 2.15x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.424s (+12.2% 🔺) 4.443s (+1.1%) 0.000s (+200.0% 🔺) 4.882s (+1.5%) 1.459s 13 1.00x
▲ Vercel Next.js (Turbopack) 4.142s (-59.3% 🟢) 5.278s (-54.2% 🟢) 0.010s (+Infinity% 🔺) 5.675s (-52.9% 🟢) 1.534s 11 1.21x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.323s (-25.4% 🟢) 2.141s (-1.7%) 0.000s (+Infinity% 🔺) 2.158s (-1.8%) 0.836s 28 1.00x
🐘 Postgres Nitro 1.373s (-23.3% 🟢) 2.067s (-3.5%) 0.000s (+189.7% 🔺) 2.080s (-4.4%) 0.706s 29 1.04x
🐘 Postgres Next.js (Turbopack) 1.448s 2.146s 0.000s 2.155s 0.707s 28 1.09x
💻 Local Next.js (Turbopack) 2.828s 3.292s 0.000s 3.296s 0.468s 19 2.14x
💻 Local Express 3.017s (-13.0% 🟢) 3.735s (-7.4% 🟢) 0.001s (-19.1% 🟢) 3.738s (-7.4% 🟢) 0.721s 17 2.28x
💻 Local Nitro 3.110s (-8.2% 🟢) 3.730s (-7.5% 🟢) 0.000s (-22.8% 🟢) 3.737s (-7.4% 🟢) 0.627s 17 2.35x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.380s (+31.4% 🔺) 6.664s (+24.0% 🔺) 0.000s (-100.0% 🟢) 7.122s (+22.9% 🔺) 1.742s 9 1.00x
▲ Vercel Next.js (Turbopack) 5.636s (~) 7.027s (+0.7%) 0.000s (-100.0% 🟢) 7.524s (~) 1.888s 9 1.05x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Sets WORKFLOW_SERVER_URL_OVERRIDE in
packages/world-vercel/src/utils.ts to
https://workflow-server-git-peter-v4.vercel.sh so that e2e tests
running off this SDK branch exercise the v4-enabled workflow-server
preview instead of production.

The override is the inline mechanism documented at the constant —
when set, it wins over both the default
(https://vercel-workflow.com) and the VERCEL_WORKFLOW_SERVER_URL
env var. The same pattern is used in v4 testing on the workflow-
server side: CI rewrites this string on PR branches. Reset to ''
before merging to main.

Companion to vercel/workflow-server#439.

Updates four tests in utils.test.ts that previously assumed the
override is empty. Each affected assertion gets a comment noting
what the expectation looks like on main; flipping back to the main
behavior is a one-line edit per test when the override is reset.

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

The matching server-side change
(workflow-server PR #439, commit 5d79cf1) now returns:

  - GET single event: full event entity CBOR-encoded in the body
    (resolves refs server-side and bakes payload into eventData).
  - LIST events: each frame's meta is the full event entity (CBOR),
    payload stays as a RefDescriptor in eventData[field], resolved
    bytes ride in the frame body.

This commit threads that through the world-vercel adapter:

  - events-v4.ts:
    * getEventV4 returns DecodedV4Event (CBOR-decode of response
      body). Drop the parseEventMetaFromHeaders / readHeader-driven
      reconstruction.
    * ListedEventV4 carries `{ event: DecodedV4Event, body:
      Uint8Array }` — the full entity plus the resolved payload bytes
      to splice in.

  - events.ts:
    * buildEventFromV4 takes (decoded entity, payload bytes) and
      splices the bytes into eventData[payloadField] for the LIST
      path. For the GET path the server already baked the bytes in,
      so buildEventFromV4 is called with an empty body.
    * CBOR-decode the payload bytes back into the original JS value
      on read. Matches the unconditional CBOR-encode on write
      (Uint8Array round-trips via cbor-x's binary type).

Why this matters: the workflow runtime's replay path reads arbitrary
fields off eventData (executionContext, hookToken, isWebhook,
resumeAt, error shape, …). The previous cherry-picked-metadata shape
dropped those fields, which is why every E2E Vercel Prod test on
this branch was getting stuck after run_started with no further
events — the runtime's invocation of the workflow function on the
workbench deployment couldn't reconstruct state correctly.

Companion to workflow-server PR #439 commit 5d79cf1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,5 @@
---
"@workflow/world-vercel": major
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
"@workflow/world-vercel": major
"@workflow/world-vercel": minor

Comment thread .changeset/v4-events-client.md Outdated
"@workflow/world-vercel": major
---

Switch the world-vercel adapter's event endpoints from the v2/v3 wire format to v4. Event metadata now rides in `x-wf-*` HTTP headers and payloads stream end-to-end as opaque bytes — no server-side CBOR parse on writes, and no per-event `/refs` round-trip on list responses. POST event response carries the materialized EventResult as a CBOR body. Public `createWorkflowRunEvent` / `getEvent` / `getWorkflowRunEvents` signatures are unchanged; the underlying wire calls swap to v4. `listEventsByCorrelationId` is not yet implemented on v4 and now throws — callers should fetch hooks directly via `storage.hooks.getByToken`. Requires workflow-server with v4 routes mounted.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
Switch the world-vercel adapter's event endpoints from the v2/v3 wire format to v4. Event metadata now rides in `x-wf-*` HTTP headers and payloads stream end-to-end as opaque bytes — no server-side CBOR parse on writes, and no per-event `/refs` round-trip on list responses. POST event response carries the materialized EventResult as a CBOR body. Public `createWorkflowRunEvent` / `getEvent` / `getWorkflowRunEvents` signatures are unchanged; the underlying wire calls swap to v4. `listEventsByCorrelationId` is not yet implemented on v4 and now throws — callers should fetch hooks directly via `storage.hooks.getByToken`. Requires workflow-server with v4 routes mounted.
New internal API format: separately encode event metadata from user payloads. Eliminates the need for calling separate endpoints for ref resolution, which improves performance on longer runs.

VaguelySerious and others added 3 commits May 25, 2026 10:49
Companion to workflow-server PR #439 commit 2a55acd, which switched
GET /api/v4/runs/:runId/events/:eventId from a CBOR-entity body to a
single v4 frame (same wire shape as one LIST frame).

  - getEventV4 now returns `{ event: DecodedV4Event, body: Uint8Array }`
    by reading exactly one frame off the response body via
    `decodeFrames` — same reader the LIST path uses. No content-type
    branching, no separate CBOR decode path.

  - getEvent (the storage adapter wrapper) passes both pieces to
    `buildEventFromV4`, which splices the CBOR-decoded body into
    `eventData[payloadField]`. Same path LIST already uses, so no
    GET-specific shape exists anymore.

  - Drop the special-case fallback in `buildEventFromV4` that used to
    re-decode an in-eventData Uint8Array — only one input shape now.

Net effect: server-side memory for GET single-event is now bounded by
S3 chunk size (~64 KB) instead of full payload size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to workflow-server PR #439 commit 5036b56, which added
`GET /api/v4/events?correlationId=...`. The SDK adapter's
`storage.events.listByCorrelationId` no longer throws.

Implementation:

  - New `getEventsByCorrelationIdV4` wire helper alongside
    `getWorkflowRunEventsV4`. Both share a small
    `consumeListFrameStream(url, config, opName)` that drives the
    same frame-stream-to-page conversion — only the URL differs.

  - `events.ts`'s `getWorkflowRunEvents` dispatches between the two
    based on whether params carry `runId` or `correlationId`. Same
    return shape on either path.

Changeset updated to drop the "not yet implemented" caveat.

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

Mirrors the server-side change in workflow-server: the POST request body
is now one length-prefixed frame containing CBOR-encoded metadata plus
the opaque payload. Eliminates the V4_HEADERS constant, the
percent-encoding for non-ASCII values, the base64-CBOR encoding for
executionContext, and the implicit 32 KB cap on Vercel header size.
The response side still uses x-wf-event-id/run-id/created-at headers
for callers that want eventId without decoding the CBOR body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"@workflow/world-vercel": major
---

Switch the world-vercel adapter's event endpoints from the v2/v3 wire format to v4. Event metadata now rides in `x-wf-*` HTTP headers and payloads stream end-to-end as opaque bytes — no server-side CBOR parse on writes, and no per-event `/refs` round-trip on list responses. POST event response carries the materialized EventResult as a CBOR body. GET single event and LIST events use the same `application/vnd.workflow.v4-frames` binary frame stream; `listEventsByCorrelationId` is wired through too. Public `createWorkflowRunEvent` / `getEvent` / `getWorkflowRunEvents` signatures are unchanged. Requires workflow-server with v4 routes mounted.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
Switch the world-vercel adapter's event endpoints from the v2/v3 wire format to v4. Event metadata now rides in `x-wf-*` HTTP headers and payloads stream end-to-end as opaque bytes — no server-side CBOR parse on writes, and no per-event `/refs` round-trip on list responses. POST event response carries the materialized EventResult as a CBOR body. GET single event and LIST events use the same `application/vnd.workflow.v4-frames` binary frame stream; `listEventsByCorrelationId` is wired through too. Public `createWorkflowRunEvent` / `getEvent` / `getWorkflowRunEvents` signatures are unchanged. Requires workflow-server with v4 routes mounted.
New internal API format: separately encode event metadata from user payloads. Eliminates the need for calling separate endpoints for ref resolution, which improves performance especially on longer runs.

VaguelySerious and others added 16 commits May 25, 2026 12:23
Payload fields (input / output / result / error / payload / metadata)
reach world-vercel after the runtime has already serialized them via
dehydrateRunError / dehydrateStepReturnValue / dehydrateStepArguments —
they're Uint8Arrays carrying a devalue blob with a format prefix.
splitEventDataForV4 was running them through cbor-x.encode again, so
the wire bytes ended up as cbor(Uint8Array). On reads through
runs.get (which goes through v2 and just returns the raw stored bytes),
the consumer saw the CBOR wrapping and hydrateRunError couldn't parse
the format prefix — every failed workflow run surfaced as
"Failed to hydrate workflow run error".

Pass the bytes through unchanged on write and read; symmetric with
world-local and the v2/v3 wire format. Throw on non-Uint8Array to flag
non-runtime callers loudly instead of silently double-wrapping.

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

Hook-emitting runtimes set eventData.token (matches the world contract
in packages/world/src/events.ts). splitEventDataForV4 was looking for
eventData.hookToken instead, so the frame meta arrived without a token
and the server's hook materialization failed validation. The v4 wire
name (meta.hookToken) is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cbor-x encodes Date natively (CBOR tag 1) and the server (post-23c79b9)
decodes it back to a Date for the materialization service. Stop
pre-flattening to an ISO string — that was a workaround for the original
header-based v4 contract and now leaves the runtime with a string in
eventData.resumeAt after replay, blowing up on .getTime().

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

DynamoDB's eventData column stringifies Date values on write — even when
the v4 frame meta originally carried them as native Dates — so the v4
GET/LIST response delivers eventData.resumeAt back as an ISO string.
The runtime expects a Date (wait_created.resumeAt → .getTime() in the
replay loop) and crashed every sleeping workflow with
"resumeAt.getTime is not a function".

Run the assembled event through EventSchema.safeParse, which applies
the per-event-type z.coerce.date() that mirror the v2/v3 read path
(see packages/world-vercel/src/events.ts on main). safeParse so a
mid-rollout event with an unknown shape still passes through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Tests run (26447355995) on commit e278374 got stuck in a
"queued" state for 20+ minutes — likely related to the same GitHub
account/token suspension that blocked the vercel/wait-for-deployment
step earlier today. `gh run rerun --failed` and even
`POST /actions/runs/.../cancel` were rejected. Force a fresh push so a
new workflow run is created.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous CI retrigger commits e699861 and the subsequent main
merge 512ce2a didn't spawn workflow runs because GitHub Actions was
degraded throughout that window (incident gnftqj9htp0g, mitigated
2026-05-26T13:01Z). Push an empty commit now that the platform is
healthy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The E2E suite is failing with "Failed to hydrate step error: Invalid
format prefix" on every step_failed / run_failed replay even after the
CBOR-double-wrap fix. The bytes arriving at hydrateStepError have a
prefix that doesn't match /^[a-z0-9]{4}$/, which means they were never
the devalue blob dehydrateStepError emitted — something on the read
path is reshaping them.

Add a temporary console.warn in buildEventFromV4 that dumps the first
16 bytes (hex) and the failed prefix string when the body bytes for a
step_failed / run_failed event don't look like a valid format prefix.
The output lands in the workbench Vercel logs alongside the workflow
replay. Revert once we identify the source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous diagnostic that only logged bad-prefix cases never fired in
workbench Vercel logs. Either the bytes are already valid here (and
the failure is later in the pipeline) or buildEventFromV4 isn't being
hit at all for the failing replays. Log every step_failed / run_failed
event so we can tell which scenario we're in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause for the step-error hydration failure was found server-side
(workflow-server@899193a — handleStepFailed / handleStepRetrying now
recognize pre-built RemoteRef instances and pass them through). Drop
the temporary console.warn that was logging every step_failed /
run_failed event's payload prefix.

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

Matches the server-side change in workflow-server@961988e. When
run_created POST fails (e.g. simulated 500 in the resilient-start
e2e), the runtime re-issues run_started with the run-input bag
attached to eventData (input + deploymentId + workflowName +
executionContext). Without listing run_started here we'd silently
drop the input bytes during splitEventDataForV4, leaving the server's
"run_started arrived before run_created" fallback with no input to
backfill from.

For a normal run_started call (no eventData) the new entry is a no-op —
the eventData lookup misses and no payload is built.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
createWorkflowRunEventV4 / getEventV4 / getWorkflowRunEventsV4 were
throwing plain \`Error\` on non-2xx — meaning runtime callers that
specifically branch on EntityConflictError / RunExpiredError /
ThrottleError saw an opaque error and treated it as fatal. The
runClassSerializationWorkflow e2e regressed once run_started carried
input on the resilient-start path: queue + run_started races against
run_created sometimes win, and the parallel run_created POST in
start() then 409s. start() catches EntityConflictError on that path
and continues — but only if the error has the right type.

Mirror the v3 makeRequest mapping (utils.ts): 409 → EntityConflictError,
410 → RunExpiredError, 425 → TooEarlyError, 429 → ThrottleError,
everything else → WorkflowWorldError with the original status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-add the SDK-side run_started: 'input' entry to PAYLOAD_FIELD_BY_EVENT_TYPE
(server already has it from 961988e). Previous attempt regressed
~6 unrelated workflows with "Invalid input" / devalue parse errors,
but I never identified the root cause — too generic of a message.

Add a temporary console.warn in workflow.ts that dumps workflowRun.input
shape + first 16 bytes right before hydrateWorkflowArguments, so the
workbench Vercel logs surface exactly which bytes are arriving when
the hydration fails. Wraps the hydrate in try/catch to also log the
exception path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…9/429/5xx

start.ts catches EntityConflictError on the run_created POST so a
resilient-start race against start.ts's parallel POST can ride through
cleanly. With plain Error, the 409 was treated as fatal and any test
that hit the race (e.g. outputStreamWorkflow, thisSerializationWorkflow,
wellKnownAgentWorkflow) failed with "Workflow run already exists".

Only convert the codes start.ts / runtime actually branch on
(409, 429, 5xx). Keep other statuses as plain Error to avoid widening
behavior changes at other catch sites — the previous broader mapping
attempt (7ba5a10) coincided with unrelated workflow regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep the typed-errors mapping from createWorkflowRunEventV4 — start.ts
catches EntityConflictError correctly so the 409 race against parallel
run_created POSTs rides through. Drop the run_started:'input' SDK entry
and the workflow.ts hydration diagnostic.

The "resilient start: addTenWorkflow completes when run_created returns
500" test remains broken without the input piggyback. Fixing it cleanly
without regressing other workflows needs a deeper investigation than
the diagnostic logging surfaced — the runs that fail with "Invalid
input" never reached hydrateWorkflowArguments, so the bad deserialize
is somewhere later in the workflow VM (likely hydrateStepReturnValue
on a step that wraps a child workflow's return value).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant