Skip to content

[core] Don't download recorded step inputs on replay (resolveData: 'skip-step-inputs') - #4348

Merged
pranaygp merged 5 commits into
mainfrom
omit-step-inputs-on-replay
Sep 24, 2026
Merged

pranaygp merged 5 commits into
mainfrom
omit-step-inputs-on-replay

Conversation

@pranaygp

@pranaygp pranaygp commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

Replay never reads recorded step inputs. Workflow code recomputes every step's arguments when it re-runs, and a step's input is only read by step execution, which takes it from the step entity (startResult.step in step-executor.ts, the pre-claimed batch's step, or in-memory bytes on the lazy inline path). Yet every replay read transfers each step_created / step_started input:

  • the event list
  • the run_started and hook_received preloads
  • inline sinceCursor deltas

For a workflow that passes growing state into its steps (#4238), those inputs are the part of the replay log that grows quadratically. #3026 reports the resulting cost.

Following review, the omission is a data-resolution mode rather than a separate flag:

  • @workflow/world
    • New EventsResolveData = ResolveData | 'skip-step-inputs' for ListEventsParams.resolveData and CreateEventParams.resolveData.
    • 'skip-step-inputs' is 'all', except that the World may leave input out of step_created / step_started events.
    • On a create it applies only to the event-log page the create returns, never to the created event or step entity.
    • entityResolveData() maps it to 'all' for code that passes create params on to entity reads.
    • A World that doesn't implement it must treat it as 'all'; world-local, world-postgres and world-sim do, and keep returning inputs. A new @workflow/world-testing conformance case checks this.
  • @workflow/world-vercel
    • Lists send remoteRefBehavior=skip-step-inputs.
    • Creates send eventsRemoteRefBehavior: 'skip-step-inputs' in the v4 POST meta. That is the same enum, applied to the event-log page the POST returns: the run_started preload, the hook replay stream, the inline delta, and the skipped-slot page.
    • It isn't folded into the POST's own remoteRefBehavior. That field already chooses how the created event and entities come back, and it is deliberately lazy for every event type except run_created, run_started and step_started. Forcing it to resolve would echo every step_completed result back.
    • The replay-log suffix fetched after a partial preload uses the same value.
  • @workflow/core
    • Every replay read and every write whose returned event-log page feeds the log asks for REPLAY_RESOLVE_DATA ('skip-step-inputs'):
      • loadWorkflowRunEvents
      • the node engine's create wrapper, which covers inline deltas and both preloads
      • the QuickJS list and create paths
      • the step executor, only when it asks for a delta, through withReplayDelta(); its step_started write, whose step.input it reads, is untouched

What replay reads from these events:

  • The node engine only marks hasCreatedEvent on step_created (step.ts) and reads ownerMessageId from step_started.
  • QuickJS only calls markCreated.
  • Ownership and attempt counting read metadata only.
  • The replay payload cache never prepares step inputs.

The web UI and CLI don't ask for the mode, so they keep reading inputs.

Compatibility

  • Old SDK → new server: it never sends the value, so nothing changes.
  • New SDK → old server: an old server rejects remoteRefBehavior=skip-step-inputs on the list with 400 {"error":"validation-error","details":[{"path":["remoteRefBehavior"],…}]}. world-vercel then retries that read once with resolve and remembers the backend for 10 minutes, on globalThis, so a process that met an old instance mid-deploy goes back to the cheaper read afterwards. The replay costs what it did before instead of failing.
    • The backend is only remembered if the retry succeeds, so a 400 about something else still surfaces.
    • An old server ignores the unknown eventsRemoteRefBehavior POST meta key (verified against workflow-server main).
    • So the two PRs can deploy in either order.

Server PR

https://github.com/vercel/workflow-server/pull/1039 (merged)

CI notes

How did you test your changes?

  • @workflow/world-testing (world-local): 17 passed, including the new skip-step-inputs case. I checked that case fails against a World that reads anything but 'all' as 'none'.
  • @workflow/core pnpm test: 2,670 passed.
  • @workflow/world-vercel: 757 passed.
  • @workflow/world-local: 598 passed.
  • @workflow/world-sim: 71 passed.
  • turbo typecheck across all non-workbench packages: passed.
  • Replay against an omitting World (retained-vm-loop.test.ts): runs a growing-state loop against a World that serves every replay event without step inputs when asked (list pages, the run_started preload, inline deltas) and keeps them on the step entity. It covers inline deltas, list-only reads, and a fresh replay after a 412. Each case asserts the same result, identical step inputs, that stripping actually happened, and that every replay read and delta-requesting write asked for skip-step-inputs.
    • For QuickJS, the existing quickjs-runtime.test.ts cases already replay step_created events with no input. quickjs-entrypoint.preload.test.ts now asserts that its fetches ask for the mode.
  • world-vercel wire (events.test.ts):
    • the list enum value, and the POST meta key present and absent;
    • the suffix read carrying it;
    • decoding of an input-less step_created frame;
    • the old-server fallback, using the verbatim body workflow-server main returns, and remembering the backend;
    • a 400 about something else, which still surfaces and isn't remembered;
    • the backend is asked again once the 10-minute window has passed.

🤖 Generated with Claude Code

Replay recomputes every step's arguments by re-running workflow code; a
step's input is only ever read by step execution, from the step entity.
Yet every replay read (the event list, the run_started / hook_received
preloads, and inline sinceCursor deltas) transfers each step_created /
step_started input. For a workflow passing growing state into its steps,
those inputs are the quadratic part of the replay log.

Add an advisory `omitStepInputs` to ListEventsParams and CreateEventParams.
The runtime sets it on every replay read and on every write whose returned
replay events feed the log; world-vercel sends it as a list query param and
POST frame meta flag. A World (or server) that doesn't know it keeps
returning the inputs, so this is compatible in both directions.

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

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 24, 2026 01:20
@pranaygp
pranaygp requested a review from a team as a code owner September 24, 2026 01:20
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6d38f95

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

This PR includes changesets to release 21 packages
Name Type
@workflow/world Patch
@workflow/world-vercel Patch
@workflow/core Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-testing Patch
@workflow/cli Patch
@workflow/errors Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
workflow 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

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
example-nextjs-workflow-turbopack Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
example-nextjs-workflow-webpack Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
example-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-astro-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-express-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-fastify-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-hono-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-nestjs-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-nitro-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-nuxt-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-python-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-sveltekit-workflow Building Building Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-tanstack-start-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workbench-vite-workflow Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workflow-docs Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workflow-swc-playground Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workflow-tarballs Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC
workflow-web Ready Ready Preview, v0 Sep 24, 2026 6:13pm UTC

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

❌ Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (62 failed)

python-node (62 failed):

  • promiseAllWorkflow | wrun_41M3A9TVCR0GJBWTTHTH2B449D | 🔍 observability
  • promiseRaceWorkflow | wrun_41M3A9TVCR0GJBWTTHTH2B449E | 🔍 observability
  • promiseAnyWorkflow | wrun_41M3A9TVCR0GJBWTTHTH2B449F | 🔍 observability
  • retainedInterleavingWorkflow | wrun_41M3A9V1QV0GZRSWN7V6Y1CQ7H | 🔍 observability
  • hookWorkflow | wrun_41M3A9V6YB0GX5YKW9JAKRC2K4 | 🔍 observability
  • hookWorkflow is not resumable via public webhook endpoint | wrun_41M3A9V7470GH5V6FWA102VK39 | 🔍 observability
  • sleepingWorkflow | wrun_41M3A9VFWF0GTJ764CX4YN2YKH | 🔍 observability
  • parallelSleepWorkflow | wrun_41M3A9VHJ30GTH52XRSQQV5MFN | 🔍 observability
  • sleepWinsRaceWorkflow | wrun_41M3A9VJQV0GM7SBP0J9KKSQEM | 🔍 observability
  • stepWinsRaceWorkflow | wrun_41M3A9VNEF0GP15N4CNADZ7ZMR | 🔍 observability
  • nullByteWorkflow | wrun_41M3A9VSY60GXV62T26S7YBK84 | 🔍 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_41M3A9W9770GP2H8XC7ZRQSYGD | 🔍 observability
  • utf8StreamWorkflow | wrun_41M3A9WAX20GQTP7CAWFCVPQV2 | 🔍 observability
  • writableForwardedFromWorkflowWorkflow | wrun_41M3A9WB4D0GYWXQ9WE13ZW67B | 🔍 observability
  • writableForwardedFromStepWorkflow | wrun_41M3A9WDSJ0GT3DSEWG1XF5GT8 | 🔍 observability
  • promiseRaceStressTestWorkflow | wrun_41M3A9WG1M0GYDMARQKEVEECCK | 🔍 observability
  • 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()
  • error handling catchability step throw round-trips FatalError with cause chain to workflow catch
  • error handling catchability workflow throw round-trips FatalError + cause through run_failed event
  • 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_41M3A9XZ3N0GYB8QWQVY2AXSX7 | 🔍 observability
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_41M3A9XTWX0GJBW5Q81VEWBPSC | 🔍 observability
  • hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload | wrun_41M3A9XXEA0GQNCTDZ7R9A6377 | 🔍 observability
  • 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution | wrun_41M3A9Y03A0GV8NQEE3MZFF6W3 | 🔍 observability
  • 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution | wrun_41M3A9Y03M0GQCVMHH9W004W44 | 🔍 observability
  • hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps | wrun_41M3A9Y05C0GV3J910199H583S | 🔍 observability
  • hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered | wrun_41M3A9Y7H10GXWTJYXB0H33C1E | 🔍 observability
  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41M3A9YS9C0GPZRWNWR0WV2HP7 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41M3A9YEJZ0GMTJFNDAGPJFPVY | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41M3A9YH3B0GKB55KF1PMFR6YN | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41M3A9Z5860GMDVC0ATKYWF0ZX | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41M3A9ZA9E0GPRPQPFVVHH6EW2 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41M3A9Z8EB0GKRHZWJMTKTFC9S | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41M3A9ZSV60GKF7XQQYN9AVHNB | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M3AA0MDN0GJW0GT9PKC1PC21 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M3AA0NSB0GKBSCY15GSGDB9G | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41M3AA0QEC0GS5X5F3868YHTJZ | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41M3AA0SJB0GHVN64YPX6JJ23F | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M3AA0TZ50GP7JHSKSJYNTSGE | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41M3AA0WCD0GX7H9BBSWJ6YMPX | 🔍 observability
  • AbortController abortTimeoutWorkflow: timeout cancels long-running step
  • AbortController abortParallelWorkflow: abort cancels all parallel steps
  • AbortController abortReasonWorkflow: abort reason preserved across boundaries
  • AbortController abortViaHookWorkflow: external hook triggers abort on in-flight step
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_41M3AA1AJB0GSYTQYR5RX274XP | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M3AA1APQ0GN41GRB91ZYJADK | 🔍 observability
  • setAttributes start: initial attributes are seeded on run creation
  • setAttributes start: reserved-prefix initial attributes are seeded with allowReservedAttributes
  • setAttributes setAttributesWorkflow: workflow-body calls append native attr_set events and merge correctly
  • setAttributes setAttributesInsideStepWorkflow: step-body calls append attributed native events
  • setAttributes Promise.all of disjoint-key writes: every key lands
  • setAttributes workflow throws after awaited setAttributes: attribute still persists on the failed run

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • resumes in flight during the takeover are never lost: each lands in exactly one log (nitro · local-dev / local / quickjs / stable)
  • takes a retained token from a finished run without touching its log (sveltekit · vercel-prod / vercel / quickjs / production)

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

124 infra events
  • cold-start-warmup · suite warmup (python) · at 18:12:41Z · abandoned wrun_41M3A9SM0J0GRP9RZ2WNY491E8 · (+7 more)
  • run-pickup-stall · retainedInterleavingWorkflow (python) · at 18:12:58Z · abandoned wrun_41M3A9X91S0GSGG0DZTGFR061S
  • run-pickup-stall · promiseAnyWorkflow (python) · at 18:12:58Z · abandoned wrun_41M3A9X91S0GSGG0DZTGFR061R
  • run-pickup-stall · promiseAllWorkflow (python) · at 18:12:58Z · abandoned wrun_41M3A9X91R0GJ8NAAQ3G752VJ7
  • run-pickup-stall · hookWorkflow (python) · at 18:12:58Z · abandoned wrun_41M3A9X91Y0GN3PYHJBX0AF7QY
  • run-pickup-stall · promiseRaceWorkflow (python) · at 18:12:58Z · abandoned wrun_41M3A9X91S0GSGG0DZTGFR061Q
  • run-pickup-stall · retainedInterleavingWorkflow (python) · at 18:13:45Z · abandoned wrun_41M3A9YQKK0GV7QRPG3KMSSF3X
  • run-pickup-stall · hookWorkflow (python) · at 18:13:45Z · abandoned wrun_41M3A9YQQZ0GXSSRXYX9D51VB2
  • run-pickup-stall · promiseAllWorkflow (python) · at 18:13:59Z · abandoned wrun_41M3A9Z4JV0GW6GDP26VV6JHXC
  • run-pickup-stall · promiseAnyWorkflow (python) · at 18:13:59Z · abandoned wrun_41M3A9Z4MA0GWY8ZYG5JA3J2JH
  • run-pickup-stall · promiseRaceWorkflow (python) · at 18:13:59Z · abandoned wrun_41M3A9Z4Z80GQSRPNX1N9D7W7V
  • run-pickup-stall · hookWorkflow is not resumable via public webhook endpoint (python) · at 18:14:33Z · abandoned wrun_41M3AA05ZT0GVP8GTSK1TYDZ62
  • run-pickup-stall · sleepingWorkflow (python) · at 18:14:34Z · abandoned wrun_41M3AA06ZG0GH0XNDX5023E0K1
  • run-pickup-stall · parallelSleepWorkflow (python) · at 18:15:01Z · abandoned wrun_41M3AA10FQ0GM06K1427857E3Q
  • run-pickup-stall · sleepWinsRaceWorkflow (python) · at 18:15:02Z · abandoned wrun_41M3AA10W60GSJHGGMC52C4ESG
  • run-pickup-stall · stepWinsRaceWorkflow (python) · at 18:15:25Z · abandoned wrun_41M3AA115K0GJ521ECVT1XZZGR
  • cold-start-warmup · suite warmup (tanstack-start) · at 18:15:35Z · abandoned wrun_01M3AA199M7M7ERT7Q8WXW4C22 · (+1 more)
  • run-pickup-stall · hookWorkflow is not resumable via public webhook endpoint (python) · at 18:16:36Z · abandoned wrun_41M3AA2QNR0GHJV4684XQG45Q5
  • run-pickup-stall · sleepingWorkflow (python) · at 18:16:36Z · abandoned wrun_41M3AA2QVB0GPN3M3FAW9KZTX8
  • run-pickup-stall · stepWinsRaceWorkflow (python) · at 18:16:36Z · abandoned wrun_41M3AA3S240GGKJPBGSM4P74TQ
  • run-pickup-stall · parallelSleepWorkflow (python) · at 18:16:37Z · abandoned wrun_41M3AA3RC70GQZHHBVYNCM4EHW
  • run-pickup-stall · sleepWinsRaceWorkflow (python) · at 18:16:37Z · abandoned wrun_41M3AA3RRQ0GKPYSRWTYJGS129
  • run-pickup-stall · nullByteWorkflow (python) · at 18:16:57Z · abandoned wrun_41M3AA4JZC0GGRPQMPSK95G9QK
  • run-pickup-stall · no startIndex (reads all chunks) (python) · at 18:16:57Z · abandoned wrun_41M3AA4K690GN1JKEHWNCPDGHG
  • run-pickup-stall · positive startIndex (skips first chunk) (python) · at 18:17:30Z · abandoned wrun_41M3AA5JZ80GZNDXXSCXRGW19G
  • run-pickup-stall · negative startIndex (reads from end) (python) · at 18:17:30Z · abandoned wrun_41M3AA5KBR0GGC7HVXBS4DMWA2
  • run-pickup-stall · getTailIndex returns correct index after stream completes (python) · at 18:17:33Z · abandoned wrun_41M3AA5P120GX9HTT7DYWX0H4T
  • run-pickup-stall · getTailIndex returns -1 before any chunks are written (python) · at 18:17:57Z · abandoned wrun_41M3AA6DJC0GXM5KCTKP89FY22
  • run-pickup-stall · getChunks returns same content as reading the stream (python) · at 18:17:57Z · abandoned wrun_41M3AA6DS90GXVCFD0K7G4NZ7B
  • run-pickup-stall · outputStreamInsideStepWorkflow - getWritable() called inside step functions (python) · at 18:18:14Z · abandoned wrun_41M3AA6XQ90GYN6VYCX3C4A9B2
  • run-pickup-stall · utf8StreamWorkflow (python) · at 18:18:30Z · abandoned wrun_41M3AA7DJ90GP50Z2C4C8Y1EGD
  • run-pickup-stall · nullByteWorkflow (python) · at 18:18:33Z · abandoned wrun_41M3AA7GM20GSMH5CEXPT2WGT7
  • run-pickup-stall · no startIndex (reads all chunks) (python) · at 18:18:33Z · abandoned wrun_41M3AA7GY40GJXDBZYCZKKFA9X
  • run-pickup-stall · positive startIndex (skips first chunk) (python) · at 18:18:57Z · abandoned wrun_41M3AA88C90GNCNPVHX974FK54
  • run-pickup-stall · negative startIndex (reads from end) (python) · at 18:19:14Z · abandoned wrun_41M3AA8S410GMS6PT1W3JMRAFF
  • run-pickup-stall · getTailIndex returns correct index after stream completes (python) · at 18:19:34Z · abandoned wrun_41M3AA9C160GVY6YFEQSR7K893
  • run-pickup-stall · getChunks returns same content as reading the stream (python) · at 18:19:36Z · abandoned wrun_41M3AA9DWX0GWGVQP0MBV7GDN2
  • run-pickup-stall · outputStreamInsideStepWorkflow - getWritable() called inside step functions (python) · at 18:19:57Z · abandoned wrun_41M3AAA2Z90GQQ5PHKRVPQNYWD
  • run-pickup-stall · writableForwardedFromWorkflowWorkflow (python) · at 18:20:15Z · abandoned wrun_41M3AAAKQ20GYYHA1K6WAHNGVJ
  • run-pickup-stall · writableForwardedFromStepWorkflow (python) · at 18:20:32Z · abandoned wrun_41M3AAB4ZN0GR8H22ECSWBGC54
  • run-pickup-stall · utf8StreamWorkflow (python) · at 18:20:34Z · abandoned wrun_41M3AAB6M60GS7P65P6RZJHTV6
  • run-pickup-stall · promiseRaceStressTestWorkflow (python) · at 18:20:38Z · abandoned wrun_41M3AABAFX0GWKH9X2VCDPSTVD
  • run-pickup-stall · crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context (nextjs-webpack) · at 18:20:40Z · abandoned wrun_01M3AABE66Q7P85QAR0KYYRGA7
  • run-pickup-stall · regular Error retries until success (python) · at 18:20:58Z · abandoned wrun_41M3AABYCN0GW0T2N5DV12SAN2
  • run-pickup-stall · FatalError fails immediately without retries (python) · at 18:21:38Z · abandoned wrun_41M3AAD52X0GZ1FVBBFN82PD65
  • run-pickup-stall · RetryableError respects custom retryAfter delay (python) · at 18:21:58Z · abandoned wrun_41M3AADRZN0GYQKHT89KA89Z8B
  • run-pickup-stall · maxRetries=0 disables retries (python) · at 18:22:14Z · abandoned wrun_41M3AAE8X10GMHVM15ER5YZ4N8
  • run-pickup-stall · FatalError can be caught and detected with FatalError.is() (python) · at 18:22:32Z · abandoned wrun_41M3AAET5N0GKFB455BSKAEC8G
  • run-pickup-stall · step throw round-trips FatalError with cause chain to workflow catch (python) · at 18:22:34Z · abandoned wrun_41M3AAEVT60GWMMNKZV3YR66M7
  • run-pickup-stall · workflow throw round-trips FatalError + cause through run_failed event (python) · at 18:22:38Z · abandoned wrun_41M3AAEZNY0GWVFN31V440PEDJ
  • run-pickup-stall · StepNotRegisteredError fails the step but workflow can catch it (python) · at 18:22:58Z · abandoned wrun_41M3AAFKJQ0GG07B99KD4BM5NR
  • run-pickup-stall · StepNotRegisteredError fails the run when not caught in workflow (python) · at 18:23:19Z · abandoned wrun_41M3AAG3G20GKN2PRABGFSP27F
  • run-pickup-stall · promiseRaceStressTestWorkflow (python) · at 18:24:19Z · abandoned wrun_41M3AAHPEH0GGARVHM9B15N1HP
  • run-pickup-stall · regular Error retries until success (python) · at 18:24:46Z · abandoned wrun_41M3AAHY330GP1TGYEG6SJXWFC
  • run-pickup-stall · writableForwardedFromWorkflowWorkflow (python) · at 18:24:48Z · abandoned wrun_41M3AAHZ1W0GNVSKH979KVV2H0
  • run-pickup-stall · writableForwardedFromStepWorkflow (python) · at 18:24:49Z · abandoned wrun_41M3AAHZE80GZKB9FHWSE8BKAE
  • run-pickup-stall · hookCleanupTestWorkflow - hook token reuse after workflow completion (python) · at 18:24:51Z · abandoned wrun_41M3AAJ0HN0GHGY57RTGBEKWCG
  • run-pickup-stall · RetryableError respects custom retryAfter delay (python) · at 18:25:47Z · abandoned wrun_41M3AAKV4M0GWD6C9E53C02CV3
  • run-pickup-stall · FatalError fails immediately without retries (python) · at 18:25:47Z · abandoned wrun_41M3AAKRP20GSCFMT434C462ZQ
  • run-pickup-stall · maxRetries=0 disables retries (python) · at 18:25:54Z · abandoned wrun_41M3AAMEB20GG4JD27AC5PY3R3
  • run-pickup-stall · FatalError can be caught and detected with FatalError.is() (python) · at 18:26:15Z · abandoned wrun_41M3AANM1H0GSBH0EKS53KXPAN
  • run-pickup-stall · step throw round-trips FatalError with cause chain to workflow catch (python) · at 18:26:15Z · abandoned wrun_41M3AANM7W0GZ1QJQCH8749PBG
  • run-pickup-stall · workflow throw round-trips FatalError + cause through run_failed event (python) · at 18:26:16Z · abandoned wrun_41M3AANMM90GX90Q7FJ36GV4JV
  • run-pickup-stall · StepNotRegisteredError fails the step but workflow can catch it (python) · at 18:26:19Z · abandoned wrun_41M3AANR100GV71V48AAKCH0TD
  • run-pickup-stall · StepNotRegisteredError fails the run when not caught in workflow (python) · at 18:26:38Z · abandoned wrun_41M3AAP9R70GTXSEX12YQ4YNYX
  • run-pickup-stall · hookCleanupTestWorkflow - hook token reuse after workflow completion (python) · at 18:27:16Z · abandoned wrun_41M3AAQFN70GZJPR79R7N376B0
  • run-pickup-stall · hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload (python) · at 18:27:19Z · abandoned wrun_41M3AAQJM10GN5R9FS1XQ81P4G
  • run-pickup-stall · 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution (python) · at 18:27:20Z · abandoned wrun_41M3AAQK8A0GWGSF96ABP4HDBC
  • run-pickup-stall · 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution (python) · at 18:27:39Z · abandoned wrun_41M3AAR5VE0GWF8F2KTY58QZCC
  • run-pickup-stall · hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps (python) · at 18:28:03Z · abandoned wrun_41M3AARXRE0GSF80683J2GY8NE
  • run-pickup-stall · hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered (python) · at 18:28:19Z · abandoned wrun_41M3AASCZS0GHQ614954776FEY
  • run-pickup-stall · hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload (python) · at 18:28:20Z · abandoned wrun_41M3AASDYE0GS56PYVAK2ZH96N
  • run-pickup-stall · 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution (python) · at 18:28:20Z · abandoned wrun_41M3AASEHD0GX8EYH4K7BXZGZT
  • run-pickup-stall · 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution (python) · at 18:28:40Z · abandoned wrun_41M3AAT16A0GWTT36KVX30KFDJ
  • run-pickup-stall · hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered (python) · at 18:29:06Z · abandoned wrun_41M3AATV8W0GM6D20DHDK34Y7P
  • run-pickup-stall · hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data (python) · at 18:29:21Z · abandoned wrun_41M3AAV9A10GWXM3D6JE66FNWQ
  • run-pickup-stall · hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue (python) · at 18:29:22Z · abandoned wrun_41M3AAVA3F0GQ2KAQG8HVV2AFK
  • run-pickup-stall · hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps (python) · at 18:29:34Z · abandoned wrun_41M3AAVPF10GYQMGZN474FVDY4
  • run-pickup-stall · hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook (python) · at 18:29:40Z · abandoned wrun_41M3AAVWJH0GTHQNB0GH515SQD
  • run-pickup-stall · resume-or-start route pattern - resumeHook retried after start() reaches the new run (python) · at 18:29:54Z · abandoned wrun_41M3AAW9NX0GJ42ZCZMR5N0B9A
  • run-pickup-stall · hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data (python) · at 18:30:08Z · abandoned wrun_41M3AAWQEF0GYKDCS9HSVXYWB5
  • run-pickup-stall · hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue (python) · at 18:30:09Z · abandoned wrun_41M3AAWRGW0GX374R7AB1RQMXN
  • run-pickup-stall · hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook (python) · at 18:30:27Z · abandoned wrun_41M3AAXAAR0GV8FAG2K14SVJA4
  • run-pickup-stall · resume-or-start route pattern - resumeHook retried after start() reaches the new run (python) · at 18:30:42Z · abandoned wrun_41M3AAXR560GYN3FVN0T5X1WXZ
  • run-pickup-stall · hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running (python) · at 18:30:55Z · abandoned wrun_41M3AAY5BR0GJTS98072T9SCPF
  • run-pickup-stall · hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() (python) · at 18:30:56Z · abandoned wrun_41M3AAY6YW0GV1W3YP40HX101H
  • run-pickup-stall · spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step (python) · at 18:31:05Z · abandoned wrun_41M3AAYF490GYS8XM9JJRKKF3F
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 18:31:21Z · abandoned wrun_41M3AAYYD30GS5J2TG8B1FAPFK
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 18:31:29Z · abandoned wrun_41M3AAZ6P00GR302SFS3B3M987
  • run-pickup-stall · hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running (python) · at 18:31:42Z · abandoned wrun_41M3AAZKPK0GZFH55P7JY7EQFV
  • run-pickup-stall · hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() (python) · at 18:31:44Z · abandoned wrun_41M3AAZN4B0GHT7NR4J0YKC73M
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 18:31:53Z · abandoned wrun_41M3AAZXXQ0GTE7B9G77QGQE4P
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 18:32:01Z · abandoned wrun_41M3AB069D0GX73K9HD4CWYHHV
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (python) · at 18:32:26Z · abandoned wrun_41M3AB0XH70GNQ01K3JW2JRNHY
  • run-pickup-stall · hookWithSleepFinalStepWorkflow - step only on final payload (python) · at 18:32:30Z · abandoned wrun_41M3AB11P70GJ7QZCVX40Z99W8
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 18:32:31Z · abandoned wrun_41M3AB13BY0GYV3VRGR9EKSDZD
  • run-pickup-stall · sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) (python) · at 18:32:34Z · abandoned wrun_41M3AB15QM0GMEGSCDB8HTZ966
  • run-pickup-stall · spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step (python) · at 18:33:06Z · abandoned wrun_41M3AB252N0GGTJTVBPQ0ZX98V
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (python) · at 18:33:13Z · abandoned wrun_41M3AB2C920GZKTCKSC08BY5XS
  • run-pickup-stall · hookWithSleepFinalStepWorkflow - step only on final payload (python) · at 18:33:17Z · abandoned wrun_41M3AB2FVE0GVYVT9KJTPCQVWX
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 18:33:32Z · abandoned wrun_41M3AB2YQY0GP28YFEFKPYX09B
  • run-pickup-stall · sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) (python) · at 18:33:36Z · abandoned wrun_41M3AB31BD0GXM3N792ZF7W1JR
  • run-pickup-stall · abortTimeoutWorkflow: timeout cancels long-running step (python) · at 18:34:13Z · abandoned wrun_41M3AB3ZAS0GSMN4P0H5HBY4X9
  • run-pickup-stall · abortParallelWorkflow: abort cancels all parallel steps (python) · at 18:34:16Z · abandoned wrun_41M3AB437S0GQ3784D1HXS5387
  • run-pickup-stall · metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (Vercel World Regression: getWorkflowMetadata() fully breaking runs #1577) (python) · at 18:34:58Z · abandoned wrun_41M3AB4VYD0GTTVYQFW00EYWRY
  • run-pickup-stall · abortReasonWorkflow: abort reason preserved across boundaries (python) · at 18:35:05Z · abandoned wrun_41M3AB4SAZ0GM7YNDE3N5PG0BW
  • run-pickup-stall · abortViaHookWorkflow: external hook triggers abort on in-flight step (python) · at 18:36:01Z · abandoned wrun_41M3AB5T8N0GJPWZZPGDHXG4W8
  • run-pickup-stall · abortTimeoutWorkflow: timeout cancels long-running step (python) · at 18:36:27Z · abandoned wrun_41M3AB7H7G0GRGEWPQ4NSZGRPK
  • run-pickup-stall · abortParallelWorkflow: abort cancels all parallel steps (python) · at 18:36:27Z · abandoned wrun_41M3AB7MVQ0GK16450N045Z9PK
  • run-pickup-stall · start: initial attributes are seeded on run creation (python) · at 18:36:33Z · abandoned wrun_41M3AB86KB0GKGHZJCQR25WF8W
  • run-pickup-stall · start: reserved-prefix initial attributes are seeded with allowReservedAttributes (python) · at 18:36:39Z · abandoned wrun_41M3AB8H4E0GXDKGS390ZM0VHS
  • run-pickup-stall · setAttributesWorkflow: workflow-body calls append native attr_set events and merge correctly (python) · at 18:36:42Z · abandoned wrun_41M3AB8M5G0GHF5N5475704NPP
  • run-pickup-stall · setAttributesInsideStepWorkflow: step-body calls append attributed native events (python) · at 18:36:54Z · abandoned wrun_41M3AB93WV0GZNSS34PAZVJ1RK
  • run-pickup-stall · abortReasonWorkflow: abort reason preserved across boundaries (python) · at 18:37:02Z · abandoned wrun_41M3AB9BTG0GWM84B6D2V3D512
  • run-pickup-stall · metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (Vercel World Regression: getWorkflowMetadata() fully breaking runs #1577) (python) · at 18:37:05Z · abandoned wrun_41M3AB9EDZ0GWE45Q71WP9QE8J
  • run-pickup-stall · abortViaHookWorkflow: external hook triggers abort on in-flight step (python) · at 18:37:06Z · abandoned wrun_41M3AB9FPM0GQG4Z5P4FXAVZT5
  • run-pickup-stall · Promise.all of disjoint-key writes: every key lands (python) · at 18:37:09Z · abandoned wrun_41M3AB9JXF0GS3N3ZYPTTBZAGE
  • run-pickup-stall · start: initial attributes are seeded on run creation (python) · at 18:37:37Z · abandoned wrun_41M3ABAEF70GZ0YMS1RCXS6GFQ
  • run-pickup-stall · start: reserved-prefix initial attributes are seeded with allowReservedAttributes (python) · at 18:37:40Z · abandoned wrun_41M3ABAGY90GG9QV8QVHMZ47MC
  • run-pickup-stall · setAttributesWorkflow: workflow-body calls append native attr_set events and merge correctly (python) · at 18:37:53Z · abandoned wrun_41M3ABAXBC0GZTWT7YZDH00S2E
  • run-pickup-stall · setAttributesInsideStepWorkflow: step-body calls append attributed native events (python) · at 18:38:03Z · abandoned wrun_41M3ABB7CF0GWKZCMHKEQ5X1D1
  • run-pickup-stall · Promise.all of disjoint-key writes: every key lands (python) · at 18:38:06Z · abandoned wrun_41M3ABBAJ40GZAK5066DXCWQBJ
  • run-pickup-stall · workflow throws after awaited setAttributes: attribute still persists on the failed run (python) · at 18:38:09Z · abandoned wrun_41M3ABBD4T0GQY9Q86F0Z8ZSHB
  • run-pickup-stall · workflow throws after awaited setAttributes: attribute still persists on the failed run (python) · at 18:38:40Z · abandoned wrun_41M3ABCB5X0GNS96HJMQCF52X2

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 3816 62 739 4617
✅ 💻 Local Development 4238 0 550 4788
✅ 📦 Local Production 4238 0 550 4788
✅ 🐘 Local Postgres 4238 0 550 4788
✅ 🪟 Windows 340 0 2 342
✅ vercel-http-transport 873 0 153 1026
✅ vercel-multi-region 27 0 0 27
✅ vercel-ws-transport 591 0 93 684
Total 18361 62 2637 21060
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 141 0 30
✅ astro-quickjs 141 0 30
✅ example-node 141 0 30
✅ example-quickjs 141 0 30
✅ express-node 141 0 30
✅ express-quickjs 141 0 30
✅ fastify-node 141 0 30
✅ fastify-quickjs 141 0 30
✅ hono-node 141 0 30
✅ hono-quickjs 141 0 30
✅ nest-node 141 0 30
✅ nest-quickjs 141 0 30
✅ nextjs-turbopack-node 168 0 3
✅ nextjs-turbopack-quickjs 168 0 3
✅ nextjs-webpack-node 168 0 3
✅ nextjs-webpack-quickjs 168 0 3
✅ nitro-node 141 0 30
✅ nitro-quickjs 141 0 30
✅ nuxt-node 141 0 30
✅ nuxt-quickjs 141 0 30
❌ python-node 4 62 105
✅ sveltekit-node 160 0 11
✅ sveltekit-quickjs 160 0 11
✅ tanstack-start-node 141 0 30
✅ tanstack-start-quickjs 141 0 30
✅ vite-node 141 0 30
✅ vite-quickjs 141 0 30

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 142 0 29
✅ astro-stable-quickjs 142 0 29
✅ express-stable-node 142 0 29
✅ express-stable-quickjs 142 0 29
✅ fastify-stable-node 142 0 29
✅ fastify-stable-quickjs 142 0 29
✅ hono-stable-node 142 0 29
✅ hono-stable-quickjs 142 0 29
✅ nest-stable-node 142 0 29
✅ nest-stable-quickjs 142 0 29
✅ nextjs-turbopack-canary-node 170 0 1
✅ nextjs-turbopack-canary-quickjs 170 0 1
✅ nextjs-turbopack-stable-node 170 0 1
✅ nextjs-turbopack-stable-quickjs 170 0 1
✅ nextjs-webpack-canary-node 170 0 1
✅ nextjs-webpack-canary-quickjs 170 0 1
✅ nextjs-webpack-stable-node 170 0 1
✅ nextjs-webpack-stable-quickjs 170 0 1
✅ nitro-stable-node 142 0 29
✅ nitro-stable-quickjs 142 0 29
✅ nuxt-stable-node 142 0 29
✅ nuxt-stable-quickjs 142 0 29
✅ sveltekit-stable-node 161 0 10
✅ sveltekit-stable-quickjs 161 0 10
✅ tanstack-start-node 142 0 29
✅ tanstack-start-quickjs 142 0 29
✅ vite-stable-node 142 0 29
✅ vite-stable-quickjs 142 0 29

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 142 0 29
✅ astro-stable-quickjs 142 0 29
✅ express-stable-node 142 0 29
✅ express-stable-quickjs 142 0 29
✅ fastify-stable-node 142 0 29
✅ fastify-stable-quickjs 142 0 29
✅ hono-stable-node 142 0 29
✅ hono-stable-quickjs 142 0 29
✅ nest-stable-node 142 0 29
✅ nest-stable-quickjs 142 0 29
✅ nextjs-turbopack-canary-node 170 0 1
✅ nextjs-turbopack-canary-quickjs 170 0 1
✅ nextjs-turbopack-stable-node 170 0 1
✅ nextjs-turbopack-stable-quickjs 170 0 1
✅ nextjs-webpack-canary-node 170 0 1
✅ nextjs-webpack-canary-quickjs 170 0 1
✅ nextjs-webpack-stable-node 170 0 1
✅ nextjs-webpack-stable-quickjs 170 0 1
✅ nitro-stable-node 142 0 29
✅ nitro-stable-quickjs 142 0 29
✅ nuxt-stable-node 142 0 29
✅ nuxt-stable-quickjs 142 0 29
✅ sveltekit-stable-node 161 0 10
✅ sveltekit-stable-quickjs 161 0 10
✅ tanstack-start-node 142 0 29
✅ tanstack-start-quickjs 142 0 29
✅ vite-stable-node 142 0 29
✅ vite-stable-quickjs 142 0 29

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 142 0 29
✅ astro-stable-quickjs 142 0 29
✅ express-stable-node 142 0 29
✅ express-stable-quickjs 142 0 29
✅ fastify-stable-node 142 0 29
✅ fastify-stable-quickjs 142 0 29
✅ hono-stable-node 142 0 29
✅ hono-stable-quickjs 142 0 29
✅ nest-stable-node 142 0 29
✅ nest-stable-quickjs 142 0 29
✅ nextjs-turbopack-canary-node 170 0 1
✅ nextjs-turbopack-canary-quickjs 170 0 1
✅ nextjs-turbopack-stable-node 170 0 1
✅ nextjs-turbopack-stable-quickjs 170 0 1
✅ nextjs-webpack-canary-node 170 0 1
✅ nextjs-webpack-canary-quickjs 170 0 1
✅ nextjs-webpack-stable-node 170 0 1
✅ nextjs-webpack-stable-quickjs 170 0 1
✅ nitro-stable-node 142 0 29
✅ nitro-stable-quickjs 142 0 29
✅ nuxt-stable-node 142 0 29
✅ nuxt-stable-quickjs 142 0 29
✅ sveltekit-stable-node 161 0 10
✅ sveltekit-stable-quickjs 161 0 10
✅ tanstack-start-node 142 0 29
✅ tanstack-start-quickjs 142 0 29
✅ vite-stable-node 142 0 29
✅ vite-stable-quickjs 142 0 29

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 170 0 1
✅ nextjs-turbopack-quickjs 170 0 1

✅ vercel-http-transport

App Passed Failed Skipped
✅ example 141 0 30
✅ express 141 0 30
✅ hono 141 0 30
✅ nextjs-turbopack 168 0 3
✅ nitro 141 0 30
✅ vite 141 0 30

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

✅ vercel-ws-transport

App Passed Failed Skipped
✅ example 141 0 30
✅ express 141 0 30
✅ nextjs-turbopack 168 0 3
✅ vite 141 0 30

📋 View full workflow run

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6d38f95 · Thu, 24 Sep 2026 18:36:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 2177 (+43%) 🔻 2351 🔴 (+37%) 🔻 2393 🔴 (+35%) 🔻 2828 🔴 (+51%) 🔻 30
TTFS stream 240 (-80%) 💚 2303 🔴 (+34%) 🔻 2338 🔴 (+34%) 🔻 2502 🔴 (+23%) 🔻 30
TTFS hook + stream 2430 (+315%) 🔻 2755 🔴 (+34%) 🔻 2799 🔴 (+35%) 🔻 2999 🔴 (+19%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 589 (+17%) 🔻 857 (+36%) 🔻 938 (+22%) 🔻 2926 (+25%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 2411 (+8.5%) 3549 (+24%) 🔻 3690 (-22%) 💚 12787 (+50%) 🔻 10
STSO 1020 steps (inline) 150 (+79%) 🔻 189 (+33%) 🔻 206 (+28%) 🔻 256 (±0%) 1019
WO 1020 steps 186039 (+26%) 🔻 186039 (+26%) 🔻 186039 (+26%) 🔻 186039 (+26%) 🔻 1
CRTT first chunk (pooled) 96 (+48%) 🔻 126 (+20%) 🔻 188 (+6.2%) 4625 (+2190%) 🔻 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 122 (+43%) 181 (-6%) 252 (-13%) 4596 (+575%) 152 (-18%) 10
size sweep (100/s, 160B-12KB) 121 (+44%) 190 (-7%) 278 (-44%) 559 (-21%) 140 (-56%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 124 (+3%) 166 (+11%) 195 (-24%) 327 (-61%) 196 (-69%) 3
replay eve-gpt-5.6-sol-2000t (1x) 120 (+9%) 170 (+11%) 197 (-20%) 262 (-68%) 208 (-60%) 2
replay eve-gpt-5.6-sol-2000t (2x) 121 (+15%) 206 (+11%) 258 (-23%) 358 (-59%) 184 (-61%) 3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 147518ms → this run 185732ms (Δ +38214ms, +26%)

   50-100 ms  ┃                         main   1  this   0    -1
  100-150 ms  ┃██████████████████████   main 844  this   0  -844
  150-200 ms  ████░░░░░░░░░░░░░░░░░░░┃  main 149  this 876  +727
  200-250 ms  █░░┃                      main  13  this 131  +118
  250-300 ms  ┃                         main   4  this  10    +6
  300-350 ms  ┃                         main   1  this   1    +0
  350-400 ms  ┃                         main   1  this   0    -1
  500-550 ms  ┃                         main   0  this   1    +1
  700-750 ms  ┃                         main   1  this   0    -1
  900-950 ms  ┃                         main   1  this   0    -1
1000-1050 ms  ┃                         main   1  this   0    -1
1150-1200 ms  ┃                         main   1  this   0    -1
1200-1250 ms  ┃                         main   2  this   0    -2
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant  RTT 1ms→5s+              avg         p50         p90           p99     n
control  ······▁█▂·▁▁·  452.5 (+207%)  145 (+16%)  252 (-13%)  4596 (+575%)  3000
sweep    ······▁█▂▁···    160.2 (+4%)  150 (+28%)  278 (-44%)    559 (-21%)  3000
gw 1x    ······▁█▁····    144.1 (+5%)  142 (+26%)  195 (-24%)    327 (-61%)  5295
eve 1x   ······▁█▁····    146.8 (±0%)  142 (+21%)  197 (-20%)    262 (-68%)  5186
eve 2x   ······▁█▄····    172.8 (+7%)  163 (+30%)  258 (-23%)    358 (-59%)  7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control  █▇▇▆▅▄▃▃▂▁  310–588ms
sweep    █▃▂▃▃▁▃▄▂▁  146–196ms
gw 1x    ▃▁▅▃▄▇▄█▅▃  130–161ms
eve 1x   ▃▁▃█▂▆▆▅▅▃  136–159ms
eve 2x   ▁▁▂▂▂▄▆█▆▄  142–225ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep  ▅█▆▁▇▁▆  160–161ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control  ▁▄▂▂█▃▅▆▁▃  42–56ms
sweep    ▅▇▁▃█▃█▆▂▃  52–72ms
gw 1x    ▃▃▄▁▅█▆█▅▁  39–50ms
eve 1x   ▄▄▂▆▁▄▅▆▁█  26–33ms
eve 2x   █▅▂▁▆▅▃▂▅▆  24–33ms
📜 Previous results (2)

6c7dd46

Thu, 24 Sep 2026 03:29:47 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 2010 (+44%) 🔻 2175 🔴 (+25%) 🔻 2202 🔴 (+24%) 🔻 2292 🔴 (+8.0%) 30
TTFS stream 189 (-88%) 💚 2154 🔴 (+30%) 🔻 2201 🔴 (+31%) 🔻 2274 🔴 (+33%) 🔻 30
TTFS hook + stream 612 (-66%) 💚 2573 🔴 (+27%) 🔻 2693 🔴 (+30%) 🔻 7177 🔴 (+240%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 439 (-2.0%) 782 (+22%) 🔻 786 (-61%) 💚 2406 (+16%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 1644 (-22%) 💚 3317 (-14%) 4413 (+0.5%) 10335 (+23%) 🔻 10
STSO 1020 steps (inline) 121 (+20%) 🔻 144 (+5.9%) 160 (+10%) 206 (-22%) 💚 1019
WO 1020 steps 145861 (+8.0%) 145861 (+8.0%) 145861 (+8.0%) 145861 (+8.0%) 1
CRTT first chunk (pooled) 64 (±0%) 87 (-14%) 116 (-14%) 151 (-69%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 80 (-9%) 123 (-21%) 153 (-48%) 251 (-74%) 102 (-60%) 10
size sweep (100/s, 160B-12KB) 83 (+11%) 141 (-16%) 245 (-20%) 535 (-11%) 132 (-39%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 81 (-16%) 123 (-11%) 157 (-23%) 463 (-16%) 193 (-62%) 3
replay eve-gpt-5.6-sol-2000t (1x) 86 (+26%) 117 (-23%) 146 (-38%) 433 (+31%) 314 (-9%) 2
replay eve-gpt-5.6-sol-2000t (2x) 125 (+17%) 124 (-32%) 151 (-38%) 319 (-28%) 187 (-31%) 3

ead616e

Thu, 24 Sep 2026 01:39:58 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 2061 (+48%) 🔻 2182 🔴 (+25%) 🔻 2200 🔴 (+24%) 🔻 2577 🔴 (+21%) 🔻 30
TTFS stream 168 (-89%) 💚 2144 🔴 (+29%) 🔻 2178 🔴 (+29%) 🔻 2233 🔴 (+31%) 🔻 30
TTFS hook + stream 646 (-64%) 💚 2534 🔴 (+25%) 🔻 2580 🔴 (+25%) 🔻 2796 🔴 (+33%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 447 (±0%) 659 (+2.6%) 768 (-62%) 💚 2733 (+31%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 2940 (+39%) 🔻 3786 (-1.3%) 4514 (+2.8%) 10635 (+27%) 🔻 10
STSO 1020 steps (inline) 111 (+9.9%) 137 (+0.7%) 151 (+4.1%) 202 (-23%) 💚 1019
WO 1020 steps 138439 (+2.5%) 138439 (+2.5%) 138439 (+2.5%) 138439 (+2.5%) 1
CRTT first chunk (pooled) 68 (+6.3%) 108 (+6.9%) 127 (-5.9%) 131 (-73%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 85 (-3%) 121 (-22%) 158 (-47%) 354 (-63%) 113 (-56%) 10
size sweep (100/s, 160B-12KB) 84.5 (+13%) 143 (-14%) 204 (-33%) 425 (-30%) 139 (-36%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 110 (+13%) 129 (-7%) 178 (-13%) 724 (+31%) 320 (-37%) 3
replay eve-gpt-5.6-sol-2000t (1x) 105 (+54%) 125 (-17%) 153 (-35%) 298 (-10%) 251 (-27%) 2
replay eve-gpt-5.6-sol-2000t (2x) 108 (+1%) 142 (-22%) 199 (-18%) 363 (-18%) 245 (-9%) 3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). █ = main, ┃ = this run, ░ = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000t eaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t 6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 42 total

fence=per-spec

scenario outcome events virt replay violations
✅ smoke-no-steps completed 3 0ms ok 0
✅ smoke-one-step completed 6 0ms ok 0
✅ hook-at-step-started completed 12 0ms ok 0
✅ hook-at-step-completed completed 12 0ms ok 0
✅ hook-at-hook-created completed 12 0ms ok 0
✅ deadline-hook-wins completed 7 1.0h ok 0
✅ deadline-expires completed 7 1.0h ok 0
✅ step-vs-timer-early-settlement completed 8 1.0h ok 0
✅ long-sleep completed 11 30.0d ok 0
✅ hook-never-arrives stalled 3 0ms skipped 0
✅ step-retries-twice completed 10 2.0s ok 0
✅ parallel-steps completed 9 0ms ok 0
✅ hook-on-execution-state completed 12 0ms ok 0
✅ peek-hook-before-branch completed 12 0ms ok 0
✅ peek-hook-after-branch completed 12 0ms ok 0
✅ peek-hook-at-registration completed 12 0ms ok 0
✅ race-hook-before-probe completed 12 0ms ok 0
✅ race-hook-after-probe completed 12 0ms ok 0
✅ race-duplicate-delivery completed 13 0ms ok 0
✅ attr-hook-before-step completed 11 0ms ok 0
✅ attr-hook-after-step completed 11 0ms ok 0
✅ attr-from-step-body completed 13 0ms ok 0
✅ fork-hook-after-timeout completed 14 1.0m ok 0
✅ fork-hook-before-timeout completed 14 1.0m ok 0
✅ count-hook-after-timeout completed 17 1.0m ok 0
✅ count-hook-before-timeout completed 20 1.0m ok 0
✅ stale-read-step-count-fork completed 20 1.0m ok 0
✅ stale-read-equal-step-counts completed 14 1.0m ok 0
✅ step-vs-step-fork completed 12 0ms ok 0
✅ step-vs-step-fork-fenced completed 12 0ms ok 0
✅ fence-catches-benign-direction completed 12 5ms ok 0
✅ in-flight-before-decision completed 17 1.0m ok 0
❌ in-flight-before-decision-counted completed 17 1.0m ok 0
✅ in-flight-after-decision completed 19 2.0m ok 0
✅ stale-read-step-count-fork-fenced completed 20 1.0m ok 0
✅ fork-hook-wins completed 13 1.0m ok 0
✅ fork-timeout-wins completed 13 1.0m ok 0
✅ unclaimed-payload-under-fork completed 17 1.0m ok 0
✅ claimed-payload-under-fork completed 17 1.0m ok 0
✅ writers-independent-step-bodies completed 12 0ms ok 0
✅ writers-scripted-tempo completed 12 0ms ok 0
✅ cancel-mid-step cancelled 7 0ms skipped 0

Full trace: world-sim.txt

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor
Framework Flow route Step reg. Framework output
hono 252.0 KiB (±0) 94.9 KiB (+7 B) 1.91 MiB (+1.3 KiB)
nextjs-turbopack 259.3 KiB (±0) 426 B (±0) 907.9 KiB (+432 B)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

6d38f95 · run

Review feedback: omitting step inputs is a data-resolution choice, so make
it one instead of a separate flag.

- @workflow/world: `EventsResolveData` = `ResolveData | 'skip-step-inputs'`
  for `ListEventsParams.resolveData` and `CreateEventParams.resolveData`
  (on a create it applies to the returned event-log page only), plus
  `entityResolveData()` for code that passes create params on to
  entity reads. `omitStepInputs` is gone.
- @workflow/world-vercel: lists send `remoteRefBehavior=skip-step-inputs`;
  creates send `eventsRemoteRefBehavior: 'skip-step-inputs'` in the POST
  meta, since a POST's `remoteRefBehavior` already selects how its created
  event and entities come back (lazy for most event types). A backend that
  predates the value rejects the list with a 400; the first rejection
  moves the process to `resolve` for that backend (kept on globalThis).
- @workflow/core: the replay reads and delta-requesting writes ask for
  `resolveData: 'skip-step-inputs'`.
- world-local / world-postgres / world-sim: accept the wider type; they
  return inputs as before.

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

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
'@workflow/world-postgres': patch
---

Replay no longer downloads recorded step inputs. The runtime reads the event log with `resolveData: 'skip-step-inputs'`, a new event-read resolution mode in which the World may leave `input` out of `step_created` and `step_started` events. It uses the mode for the event list, the `run_started` and hook preloads, and inline deltas. Replay recomputes step arguments by re-running workflow code, and steps read their input from the step entity. For workflows that pass growing state into their steps, this removes the part of the replay transfer that grows quadratically. Worlds that don't support the mode keep returning the inputs, and world-vercel falls back to full resolution against a server that predates it.

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.

Suggested change
Replay no longer downloads recorded step inputs. The runtime reads the event log with `resolveData: 'skip-step-inputs'`, a new event-read resolution mode in which the World may leave `input` out of `step_created` and `step_started` events. It uses the mode for the event list, the `run_started` and hook preloads, and inline deltas. Replay recomputes step arguments by re-running workflow code, and steps read their input from the step entity. For workflows that pass growing state into their steps, this removes the part of the replay transfer that grows quadratically. Worlds that don't support the mode keep returning the inputs, and world-vercel falls back to full resolution against a server that predates it.
Added `resolveData: 'skip-step-inputs'` option, which directs the World to leave out `input` from `step_created` and `step_started` events. Replay recomputes step arguments by re-running workflow code, so it does not need to fetch step inputs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 6d38f95, merged with the precision nit in the other thread on this line. It now says steps take their input from the step_started response or from memory. I also added that a World that doesn't implement the option must treat it as 'all'.

@VaguelySerious VaguelySerious 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. I checked out 6c7dd46f and ran world-vercel events.test.ts (61 passed) and core retained-vm-loop, runtime/helpers and quickjs-entrypoint.preload (126 passed). I also read workflow-server#1039.

I independently confirmed the key assumption, that nothing in the runtime reads a step input from the replay log:

  • no eventData.input reads on step events in core;
  • ReplayPayloadCache never prepares step inputs;
  • the resilient-dispatch re-ensure takes the input from the queue message;
  • the runtime never calls world.steps.get/list.

The step executor gets its input from the step_started response (still remoteRefBehavior: 'resolve' via eventsNeedingResolve) or from in-memory dehydrated bytes (lazy and pre-claimed inline starts). None of the notes below block merging.

* that passes growing state into its steps those inputs are the part of the
* log that grows quadratically. A World that ignores it returns them.
*/
export type EventsResolveData = ResolveData | 'skip-step-inputs';

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.

Contract risk for Worlds outside this repo. "A World that ignores it returns them" holds only if the World tests resolveData !== 'none'. A third-party World that tests === 'all' would treat 'skip-step-inputs' as 'none' and strip every payload, step results included, which breaks replay. One that validates against a ['none','all'] enum would reject the read. All the in-repo Worlds are fine. Could the doc here state the requirement explicitly (treat unknown values as 'all')? A world-testing conformance case for it would also help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6d38f95.

  • Doc: EventsResolveData now says a World must treat any value other than 'none' as 'all'. It spells out both failure modes (an === 'all' test strips step results; a ['none','all'] enum rejects the read) and points to entityResolveData().
  • Conformance: new world-testing case resolveData 'skip-step-inputs' returns every payload but step inputs. It runs a three-step workflow and lists the log with all and with skip-step-inputs. It asserts the same events, and every payload other than step inputs byte-identical. Step inputs may be either unchanged or absent.
  • Proof it bites: I checked it against world-local patched to read anything but 'all' as 'none'. The case fails, and already the run itself fails to replay.

* The {@link ResolveData} an {@link EventsResolveData} asks for on everything
* other than an event-log page's step inputs: `'skip-step-inputs'` is `'all'`.
*/
export function entityResolveData<T extends EventsResolveData | undefined>(

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.

Nit: widening CreateEventParams.resolveData means any code that forwards create params to an entity read now has to remember entityResolveData(). You covered the legacy paths in world-local, world-postgres and world-sim; worth a one-line note on CreateEventParams.resolveData so future forwarding sites don't miss it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 6d38f95: CreateEventParams.resolveData now says code that forwards create params to an entity read (runs, steps, hooks) must map them with entityResolveData() first.

* On `globalThis` (see `globalSingleton`) so that every bundled copy of this
* module learns from one rejection instead of paying it once per copy.
*/
const skipStepInputsSupport = globalSingleton(

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.

Once a backend is remembered as unsupported, that lasts for the life of the process. A long-lived process that hits an old server instance during a rolling deploy keeps doing full reads after the server upgrades. It only costs performance and is never a correctness problem, but a TTL (or re-probing after N minutes) would be cheap.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6d38f95. A rejection is now remembered for 10 minutes (SKIP_STEP_INPUTS_REPROBE_MS), and the next read after that asks for skip-step-inputs again. A new test pins the sequence: reject → resolve; within the window, resolve without asking; after it, skip-step-inputs again. I checked that the test fails with the TTL removed. With #1039 merged this path should now only matter for self-hosted or older backends.

'@workflow/world-postgres': patch
---

Replay no longer downloads recorded step inputs. The runtime reads the event log with `resolveData: 'skip-step-inputs'`, a new event-read resolution mode in which the World may leave `input` out of `step_created` and `step_started` events. It uses the mode for the event list, the `run_started` and hook preloads, and inline deltas. Replay recomputes step arguments by re-running workflow code, and steps read their input from the step entity. For workflows that pass growing state into their steps, this removes the part of the replay transfer that grows quadratically. Worlds that don't support the mode keep returning the inputs, and world-vercel falls back to full resolution against a server that predates it.

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.

Small precision nit: "steps read their input from the step entity" is true for queued execution, where the input comes back on the step_started POST response. Lazy and pre-claimed inline starts use the in-memory dehydrated bytes instead (lazyStepInput / preclaimedStart.step.input). Maybe "steps take their input from the step_started response or from memory, never from the replay log"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6d38f95: "steps take their input from the step_started response or from memory, never from the replay log". The EventsResolveData doc uses the same wording.

- Document that a World MUST treat any resolveData other than 'none' as
  'all' (a `=== 'all'` test would strip step results on every replay), and
  add a world-testing conformance case: `skip-step-inputs` returns the same
  events as `all`, every payload except step inputs unchanged. A World with
  that bug fails it (the run itself fails to replay).
- Note on CreateEventParams.resolveData that forwarding it to an entity read
  needs entityResolveData().
- Remember a backend's rejection of `skip-step-inputs` for 10 minutes rather
  than for the life of the process, so a process that met an old instance
  mid-deploy goes back to the cheaper read.
- Changeset wording: steps take their input from the step_started response
  or from memory, never from the replay log.

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

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for f2daf40 (AI decision).

This is a replay-transfer performance optimization that introduces new public API surface: a new EventsResolveData type with a 'skip-step-inputs' mode, a new exported entityResolveData() helper, a new World conformance requirement, and a new remoteRefBehavior / eventsRemoteRefBehavior wire value that also depends on a corresponding workflow-server change. Nothing here fixes a user-visible defect on stable — replay works today, it just transfers more bytes than necessary for workflows passing growing state into steps — so it falls under performance work and feature enhancement rather than a stability fix.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

f2daf4023a6a9ddaf91e42aa6409c7e09206f9c6

This branch was successfully deployed

18 active deployments
Preview – workflow-swc-playground — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-docs — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-tanstack-start-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – example-nextjs-workflow-webpack — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – example-nextjs-workflow-turbopack — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-sveltekit-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-vite-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-tarballs — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nuxt-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nitro-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-astro-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-fastify-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-express-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-hono-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nestjs-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – example-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-web — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-python-workflow — 6d38f95d Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants