Skip to content

feat: publish each run's X25519 public key on the run entity#3095

Open
TooTallNate wants to merge 3 commits into
nate/encp-payload-keyfrom
nate/encp-run-public-key
Open

feat: publish each run's X25519 public key on the run entity#3095
TooTallNate wants to merge 3 commits into
nate/encp-payload-keyfrom
nate/encp-run-public-key

Conversation

@TooTallNate

@TooTallNate TooTallNate commented Jul 24, 2026

Copy link
Copy Markdown
Member

PR 3 of 7. Stacked on #3094; review the stack in order, starting from #3093.

What

A cross-run writer needs the recipient run's public key to seal to it. Derive that key at start() and stamp it on the run, so a hook resumption or forwarded-stream writer finds it on a run fetch it was already making instead of spending ~350ms on run-key.

Derived from the per-run key material getEncryptionKeyForRun() already returns, so nothing about key acquisition changes.

Why storing it is safe

It is not secret. The matching private scalar is never stored anywhere — only re-derived on demand from the deployment's own env seed. So this does not violate the requirement that no key material live beside run metadata (that requirement is about secrets), and the server remains zero-knowledge: it stores and serves 32 public bytes it cannot use to decrypt anything.

Anyone with tenant-scoped read access to a run gains encryption capability for it, and nothing more.

Presence is the writer-side gate

A run only carries a public key if the runtime that created it could also open a sealed payload. That holds by construction: derivation and encp dispatch both live in @workflow/core, so any core that can stamp can also open. Runs are pinned to their creating deployment, so the capability this attests to is still accurate at resume time.

This matters because @workflow/core and @workflow/world-vercel are versioned independently and do drift in practice. A gate that consulted only one of them would wedge runs under one drift direction or the other:

  • If a deployment could stamp a key but its core could not dispatch encp, a writer would seal a payload the target cannot decode — run wedged. Locating derivation, stamping and dispatch all in @workflow/core makes this combination impossible to construct.
  • If a deployment's core supports encp but its world package never persisted the field, the run simply has no public key, and writers fall back to the symmetric path — degraded, not broken.

Presence therefore attests to both halves at once, which a version comparison cannot.

Resilient start

The field rides on run_created and is mirrored onto the queued runInput, because the resilient-start path recreates the run from the queue message when the run_created write failed. Without that mirror, a run recovered that way would silently lose the ability to receive sealed writes.

Surface

Package Change
@workflow/core derive + stamp at start(); browser/VM-safe base64 helpers
@workflow/world optional encryptionPublicKey on the run schema + run_created/run_started
@workflow/world-local materialize onto the run entity
@workflow/world-postgres encryption_public_key column + migration 0016
@workflow/world-vercel route into the v4 frame meta block
@workflow/web-shared hide the field in the attribute panel (internal plumbing)

Notes for reviewers

  • world-vercel's compile-time wire-contract guard caught the new field before it could be silently dropped on the v4 path, exactly as designed — it failed the build naming encryptionPublicKey. Nice.
  • Base64 helpers are hand-rolled because sealed-box.ts runs in the browser (o11y) and inside the workflow VM, where neither Buffer nor btoa can be assumed. They're cross-validated against Buffer in tests across a range of lengths.
  • base64ToBytes returns undefined on malformed input rather than throwing, so a corrupt stored key degrades to "no usable public key" → symmetric fallback, instead of crashing a resumption.
  • Tests include an end-to-end assertion that the published key actually opens a payload sealed to it, plus per-run isolation and the encryption-disabled case.

Postgres migration is additive and nullable. Full core suite (1585) passes; all world suites pass; workspace typecheck clean.

Copilot AI review requested due to automatic review settings July 24, 2026 20:56
@TooTallNate
TooTallNate requested review from a team and ijjk as code owners July 24, 2026 20:56
@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 34056d5

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

This PR includes changesets to release 20 packages
Name Type
@workflow/core Minor
@workflow/world-postgres Minor
@workflow/web-shared Patch
@workflow/world Minor
@workflow/world-local Minor
@workflow/world-vercel Minor
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web Patch
workflow Minor
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/nuxt Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite 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

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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 Jul 25, 2026 8:50am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 25, 2026 8:50am
example-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-astro-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-express-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-hono-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-nestjs-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workbench-vite-workflow Ready Ready Preview, Comment Jul 25, 2026 8:50am
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 25, 2026 8:50am
workflow-swc-playground Ready Ready Preview, Comment Jul 25, 2026 8:50am
workflow-tarballs Ready Ready Preview, Comment Jul 25, 2026 8:50am
workflow-web Ready Ready Preview, Comment Jul 25, 2026 8:50am

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (390 failed)

astro (54 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 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 abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • 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 abortVoidSleepTimeoutWorkflow: documented void sleep().then(abort) pattern works
  • 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_41KYC83P330GX8HQ49SQYPBW12 | 🔍 observability
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_41KYC83RFE0GKPB1VDWX8HG0N6 | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41KYC83TRH0GK1RXPSNXRE95PX | 🔍 observability
  • getterStepWorkflow - getter functions with "use step" directive | wrun_41KYC83Y970GV71TS6VGP8R7FV | 🔍 observability
  • distributedAbortController - manual abort triggers signal | wrun_41KYC844AG0GKED23ZQQ9YHKRB | 🔍 observability
  • distributedAbortController - TTL expiration triggers signal | wrun_41KYC84CDZ0GM74NT6B3MEPYY2 | 🔍 observability
  • distributedAbortController - reconnect to existing controller | wrun_41KYC84KQE0GY0WEDJCNM90NAS | 🔍 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 validation DX: invalid writes throw catchable FatalErrors naming rule and limit

example (39 failed):

  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41KYC7VV0P0GWSF8SPH4FSY4G4 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 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 abortExternalSignalInFlightWorkflow: external abort fires mid-flight, propagates to nested steps
  • AbortController abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals
  • 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 abortVoidSleepTimeoutWorkflow: documented void sleep().then(abort) pattern works

express (58 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 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 abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • AbortController abortSurvivesReplayWorkflow: controller state consistent across replay
  • AbortController abortReasonTypesWorkflow: various abort reason types propagate correctly
  • AbortController abortFetchUncaughtWorkflow: uncaught fetch AbortError is FatalError, no retries
  • 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()
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41KYC83TRH0GK1RXPSNXRE95PX | 🔍 observability
  • getterStepWorkflow - getter functions with "use step" directive | wrun_41KYC83Y970GV71TS6VGP8R7FV | 🔍 observability
  • distributedAbortController - manual abort triggers signal | wrun_41KYC844AG0GKED23ZQQ9YHKRB | 🔍 observability
  • distributedAbortController - TTL expiration triggers signal | wrun_41KYC84CDZ0GM74NT6B3MEPYY2 | 🔍 observability
  • distributedAbortController - reconnect to existing controller | wrun_41KYC84KQE0GY0WEDJCNM90NAS | 🔍 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 fire-and-forget: void setAttributes lands without awaiting
  • setAttributes Promise.all of disjoint-key writes: every key lands
  • setAttributes workflow throws after awaited setAttributes: attribute still persists on the failed run
  • setAttributes validation DX: invalid writes throw catchable FatalErrors naming rule and limit

fastify (18 failed):

  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability

hono (25 failed):

  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 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
  • AbortController abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • 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

nextjs-turbopack (26 failed):

  • error handling catchability workflow throw round-trips FatalError + cause through run_failed event
  • error handling catchability step throw of a non-Error value preserves it as cause on the wrapping FatalError
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • 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_41KYC7TW250GZXKZXJ5XZBYSH4 | 🔍 observability
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_41KYC7V9BD0GXPQPF037B40Y2F | 🔍 observability
  • hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload | wrun_41KYC7SH860GYA8CT6ZMAXHKTX | 🔍 observability
  • 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution | wrun_41KYC7SM0T0GT4QSR140NNG5JJ | 🔍 observability
  • 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution | wrun_41KYC7SPWD0GY10YTYFXHN5VDM | 🔍 observability
  • hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps | wrun_41KYC7SSHV0GGS0KCMKNPJ6M2X | 🔍 observability
  • hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered | wrun_41KYC7VRYC0GN9D7YJHDYY5QGS | 🔍 observability
  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41KYC7VV0P0GWSF8SPH4FSY4G4 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability

nextjs-webpack (39 failed):

  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41KYC7VV0P0GWSF8SPH4FSY4G4 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • plainModuleDoneHook resumed via plain API route (o2flow shape)
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 observability
  • AbortController abortTimeoutWorkflow: timeout cancels long-running step
  • AbortController abortFromStepWorkflow: step abort cancels an in-flight sibling step
  • AbortController abortExternalSignalWorkflow: signal passed as workflow input
  • AbortController abortExternalSignalInFlightWorkflow: external abort fires mid-flight, propagates to nested steps
  • AbortController abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals
  • AbortController abortSurvivesReplayWorkflow: controller state consistent across replay
  • 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 abortListenerWorkflow: signal.addEventListener fires on the deserialized step signal

nitro (26 failed):

  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 observability
  • AbortController abortTimeoutWorkflow: timeout cancels long-running step
  • AbortController abortReasonWorkflow: abort reason preserved across boundaries

nuxt (31 failed):

  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41KYC7VV0P0GWSF8SPH4FSY4G4 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3 | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 observability
  • AbortController abortTimeoutWorkflow: timeout cancels long-running 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

sveltekit (49 failed):

  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4 | 🔍 observability
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3 | 🔍 observability
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0 | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3 | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7 | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32 | 🔍 observability
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8 | 🔍 observability
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA | 🔍 observability
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351 | 🔍 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 abortReasonWorkflow: abort reason preserved across boundaries
  • AbortController abortViaHookWorkflow: external hook triggers abort on in-flight step
  • AbortController abortExternalSignalInFlightWorkflow: external abort fires mid-flight, propagates to nested steps
  • AbortController abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • 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 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()
  • getterStepWorkflow - getter functions with "use step" directive | wrun_41KYC83Y970GV71TS6VGP8R7FV | 🔍 observability
  • distributedAbortController - manual abort triggers signal | wrun_41KYC844AG0GKED23ZQQ9YHKRB | 🔍 observability
  • distributedAbortController - TTL expiration triggers signal | wrun_41KYC84CDZ0GM74NT6B3MEPYY2 | 🔍 observability
  • 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

vite (25 failed):

  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_41KYC7V9BD0GXPQPF037B40Y2F | 🔍 observability
  • hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload | wrun_41KYC7SH860GYA8CT6ZMAXHKTX | 🔍 observability
  • 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution | wrun_41KYC7SM0T0GT4QSR140NNG5JJ | 🔍 observability
  • 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution | wrun_41KYC7SPWD0GY10YTYFXHN5VDM | 🔍 observability
  • hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps | wrun_41KYC7SSHV0GGS0KCMKNPJ6M2X | 🔍 observability
  • hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered | wrun_41KYC7VRYC0GN9D7YJHDYY5QGS | 🔍 observability
  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_41KYC7VV0P0GWSF8SPH4FSY4G4 | 🔍 observability
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_41KYC7VX010GMJZNPZPSEBPY9V | 🔍 observability
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_41KYC7VBXN0GVGC40BAW2DRRWM | 🔍 observability
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12 | 🔍 observability
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB | 🔍 observability
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4 | 🔍 observability
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM | 🔍 observability
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2 | 🔍 observability
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F | 🔍 observability
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F | 🔍 observability
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D | 🔍 observability
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT | 🔍 observability
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW | 🔍 observability
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC | 🔍 observability
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF | 🔍 observability
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG | 🔍 observability
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T | 🔍 observability
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2 | 🔍 observability
📋 Other (80 failed)

e2e-vercel-prod-nest (52 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4
  • startFromWorkflow - calling start() directly inside a workflow function with hook communication | wrun_41KYC7WHBG0GRK7NR8C41GZPY3
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_41KYC7X1210GGWN5KQYH3K1YQG
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2
  • cancelRun via CLI - cancelling a running workflow | wrun_41KYC7XYD60GYW4NM6JRQKDC32
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351
  • 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 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 abortAnyInWorkflowWorkflow: AbortSignal.any composes signals inside the workflow VM
  • 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 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 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()
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41KYC83TRH0GK1RXPSNXRE95PX
  • getterStepWorkflow - getter functions with "use step" directive | wrun_41KYC83Y970GV71TS6VGP8R7FV
  • distributedAbortController - reconnect to existing controller | wrun_41KYC84KQE0GY0WEDJCNM90NAS
  • 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 fire-and-forget: void setAttributes lands without awaiting
  • setAttributes workflow throws after awaited setAttributes: attribute still persists on the failed run
  • setAttributes validation DX: invalid writes throw catchable FatalErrors naming rule and limit

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

  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_41KYC7VYTQ0GWCACYW0T3NRX12
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_41KYC7W10Q0GGX0S5JG976HTHB
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KYC7V9MF0GNAGZMMS9Y40FA4
  • hookTokenReuseLoopWorkflow - same run recreates a hook with the same token after dispose() | wrun_41KYC7W5TK0GH6QKYVTR087HWM
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_41KYC7W1QF0GHDJ5T2ZN79BEA2
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_41KYC7WAWK0GT3V041M6S2Z16F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_41KYC7VTYC0GH7JVFK69Y3XG0F
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_41KYC7VXW50GPFRQYNJEDHQ13D
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_41KYC7W5MM0GXN6HN5YEEN3TJ4
  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYC7WKZW0GZ2BMJP62QR8ZT0
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_41KYC7WKXY0GSQQNKZSACZDZCT
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_41KYC7WN7D0GRVHEVYMZ70PMZW
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_41KYC7WVJR0GN4HRA0M58J95MC
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_41KYC7WY8V0GJXCGYNV5W6VSTF
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_41KYC7X1ZM0GGJ76NAK0A3PQB3
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_41KYC7X84A0GSXMCENX891K0E3
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_41KYC7XMT10GQYE60CHAPY348T
  • errorSubclassRoundTripWorkflow - first-class Error subclasses survive every serialization boundary | wrun_41KYC7XQH30GNW0FJD0XWH5DRP
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_41KYC7XJHA0GYH2CNMDTWQB0E7
  • cancelRun - cancelling a running workflow | wrun_41KYC7XRNY0GXFD0QPR8YNW3G2
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_41KYC7Y6SC0GWSEQXEDWKGCAP8
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_41KYC7YMBK0GH93HEB9330WSAA
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41KYC7YY1G0GZ89QGX1E1XWFCZ
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_41KYC7Z7QK0GWCAQ8EDVR9R351
  • 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 abortReasonWorkflow: abort reason preserved across boundaries

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 1065 390 239 1694
✅ 💻 Local Development 1621 0 227 1848
✅ 📦 Local Production 1621 0 227 1848
✅ 🐘 Local Postgres 1621 0 227 1848
✅ 🪟 Windows 154 0 0 154
❌ 📋 Other 940 80 212 1232
✅ vercel-multi-region 27 0 0 27
Total 7049 470 1132 8651
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
❌ astro 72 54 28
❌ example 87 39 28
❌ express 68 58 28
❌ fastify 108 18 28
❌ hono 101 25 28
❌ nextjs-turbopack 125 26 3
❌ nextjs-webpack 112 39 3
❌ nitro 100 26 28
❌ nuxt 95 31 28
❌ sveltekit 96 49 9
❌ vite 101 25 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack 154 0 0

❌ 📋 Other

App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 26
✅ e2e-local-dev-tanstack-start- 128 0 26
✅ e2e-local-postgres-nest-stable 128 0 26
✅ e2e-local-postgres-tanstack-start- 128 0 26
✅ e2e-local-prod-nest-stable 128 0 26
✅ e2e-local-prod-tanstack-start- 128 0 26
❌ e2e-vercel-prod-nest 74 52 28
❌ e2e-vercel-prod-tanstack-start 98 28 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

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.

Pull request overview

This PR publishes each workflow run’s X25519 public key (encryptionPublicKey) onto the run entity at creation time, so cross-run writers can seal (encp) payloads to a run without an additional run-key fetch, while keeping private key material derivable-only (not stored).

Changes:

  • Derive the run’s X25519 public key from existing per-run key material during start() and include it on run_created and queued runInput (resilient start).
  • Plumb the optional encryptionPublicKey field through world schemas, Vercel v4 meta splitting, and local/postgres run materialization (plus a postgres migration).
  • Add base64 encode/decode helpers and tests; hide the field in the web attribute panel.

Reviewed changes

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

Show a summary per file
File Description
packages/world/src/runs.ts Adds optional encryptionPublicKey to the run schema with rationale/semantics.
packages/world/src/events.ts Adds encryptionPublicKey to run_created and resilient-start run_started event schemas.
packages/world-vercel/src/events.ts Routes encryptionPublicKey into the v4 meta block so it isn’t dropped on the wire.
packages/world-postgres/src/storage.ts Persists encryptionPublicKey from run_created into the run row.
packages/world-postgres/src/drizzle/schema.ts Adds encryptionPublicKey column mapping to the runs table schema.
packages/world-postgres/src/drizzle/migrations/meta/_journal.json Registers migration 0016_add_encryption_public_key.
packages/world-postgres/src/drizzle/migrations/0016_add_encryption_public_key.sql Adds encryption_public_key column to workflow_runs.
packages/world-local/src/storage/events-storage.ts Persists encryptionPublicKey onto the local run entity for run_created.
packages/web-shared/src/components/sidebar/attribute-panel.tsx Hides encryptionPublicKey in the attribute panel UI.
packages/core/src/sealed-box.ts Adds VM/browser-safe base64 helpers used for publishing the run public key.
packages/core/src/sealed-box.test.ts Tests base64 helpers against Buffer plus malformed input behavior.
packages/core/src/runtime/start.ts Derives/stamps encryptionPublicKey on run_created and queued runInput.
packages/core/src/runtime/start.test.ts Adds tests asserting stamping, usability (seal/open), isolation, and disabled-encryption behavior.
.changeset/encp-run-public-key-worlds.md Changeset for world packages to persist/transport encryptionPublicKey.
.changeset/encp-run-public-key-web-shared.md Changeset for hiding encryptionPublicKey in the web attribute panel.
.changeset/encp-run-public-key-postgres.md Changeset for the postgres column/migration.
.changeset/encp-run-public-key-core.md Changeset for core stamping at start().

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

Comment thread packages/core/src/sealed-box.ts
Comment thread packages/world-postgres/src/storage.ts
Comment thread packages/world-local/src/storage/events-storage.ts

@vercel vercel Bot 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.

Additional Suggestions:

  1. world-local lifecycle handlers (run_started/completed/failed/cancelled) rebuild the run row without encryptionPublicKey, so the key stamped at run_created is dropped as soon as the run starts.
  1. The world-postgres resilient-start path (run_started recreating a run) drops encryptionPublicKey, so runs recovered this way permanently lack their public key and cannot receive sealed writes.
  1. The resilient-start path (run_started recreating a run when run_created was missed) drops encryptionPublicKey, so a run recovered this way permanently loses its ability to receive sealed writes on the world-local (and world-postgres) backends.

Fix on Vercel

@vercel vercel Bot 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.

Additional Suggestion:

Accidentally committed generated file quickjs-assets.generated.ts imports the undeclared module quickjs-wasi, causing TS2307: Cannot find module 'quickjs-wasi' and breaking @workflow/core's build.

Fix on Vercel

@TooTallNate

TooTallNate commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Heads up — this PR's public key never actually reached the server. Found it during manual verification; the fix is now the third commit on this branch (34056d55f).

What was happening

The key was computed, put in the run_created event data, lifted into the v4 frame meta, spread into the request input… and then silently dropped before the request was sent.

packages/world-vercel/src/events-v4.ts builds the wire meta field-by-field, and buildPostFrameMeta never forwarded encryptionPublicKey. The field was also missing from the CreateEventV4Input interface entirely.

The reason nothing caught it: events.ts applies the meta with a spread:

const { payload, meta } = splitEventDataForV4(data);
await createWorkflowRunEventV4({ runId: id, /* … */ payload, ...meta }, config);

TypeScript doesn't apply excess-property checks to spreads, so an unforwarded field is not a type error. The exhaustiveness guard added in this PR covers world schema ↔ events.ts allowlist, but there is no guard for events.ts meta ↔ events-v4.ts wire. That's the same hand-maintained v4 mapping AGENTS.md already flags for having regressed trace-context propagation.

Why this was hard to see

Every symptom pointed at the server side rather than at us:

  • a deliberately oversized key was accepted rather than rejected — because the field never arrived
  • the key was never echoed back
  • encryptionPublicKey was absent from the run row in DynamoDB
  • resumeHook() always chose encr

I spent a while looking server-side. The problem was here: the SDK was dropping the field before it ever went out.

Fix

Add encryptionPublicKey to CreateEventV4Input, forward it in buildPostFrameMeta, cover both run_created and resilient-start run_started, and add a generic guard asserting every field the splitter puts in the meta reaches the wire — so the next omission in this mapping fails a test instead of silently degrading encryption. All three new tests fail without the one-line forward.

Verified in production

run_created.eventData.encryptionPublicKey = 4jWP17aASciRa+hftatSP8/21Qy+byelU0WGkFKjC2w=
run entity  .encryptionPublicKey          = 4jWP17aASciRa+hftatSP8/21Qy+byelU0WGkFKjC2w=

Then a genuinely cross-deployment resumeHook() (owner dpl_7HJcFLVE…, resumer dpl_9RYnfewA…):

hook_received.payload._data prefix = b'encp'   (111 bytes)   <- was `encr`
run status                         = completed
returnValue.received               = {"sealed":"from-deployment-B","n":42}

The round trip is the important part: an encp prefix only proves the writer sealed, but the payload coming back out of the workflow proves the owner re-derived its scalar and opened it. (The run's own outputRef stays encr, which is correct — only cross-run writers seal.)

Where the fix landed

Originally I committed this on the stack tip, which would have meant merging #3095#3098 with encryption silently downgraded to encr. It now lives here instead, so every merge point in the stack is correct. The whole stack was rebased onto latest main at the same time.

A cross-run writer needs the recipient run's public key to seal a payload
to it. Derive that key at `start()` and stamp it on the run, so a hook
resumption or a forwarded-stream writer can find it on a run fetch it was
already making instead of spending ~350ms on `run-key`.

The key is derived from the per-run key material `getEncryptionKeyForRun()`
already returns, so nothing about key acquisition changes. It is not
secret: the matching private scalar is never stored anywhere, only
re-derived on demand from the deployment's own env seed. Storing it beside
run metadata therefore does not weaken the run's confidentiality.

**Presence is the writer-side gate for sealed envelopes.** A run only
carries a public key if the runtime that created it could also open one —
which holds by construction, since derivation and `encp` dispatch both
live in `@workflow/core`, so any core that can stamp can also open. Runs
are pinned to their creating deployment, so the capability this attests to
is still accurate at resume time. Writers seal iff the field is set and
otherwise fall back to the symmetric path, which makes version skew
degrade gracefully instead of wedging a run.

The field rides on `run_created`, and is mirrored onto the queued
`runInput` so the resilient-start path (server recreates the run from the
queue message when the `run_created` write failed) doesn't silently
produce a run that can't receive sealed writes.

world-vercel's compile-time wire-contract guard caught the new field
before it could be silently dropped on the v4 path, exactly as designed —
routed into the frame meta block as plaintext metadata.

Also adds browser- and VM-safe base64 helpers to `sealed-box.ts`, since
neither `Buffer` nor `btoa` can be assumed in every context that module
runs in. `base64ToBytes` returns undefined on malformed input rather than
throwing, so a corrupt stored key degrades to "no usable public key" and
falls back to the symmetric path instead of crashing a resumption. Both
are cross-validated against `Buffer` in tests.
Two real bugs found in review, both in the local worlds. Neither surfaces
as an error — a run just silently stops accepting sealed cross-run writes
and falls back to the slow symmetric path forever.

**Resilient start dropped the key.** When a `run_started` arrives for a
run that was never created, world-local and world-postgres rebuild the run
from the queued message. Neither copied `encryptionPublicKey` onto the run
row or the synthetic `run_created` event they write. That is precisely the
scenario this field exists to survive. (The equivalent server-side path was
already handled.)

**world-local also wiped the key on every lifecycle transition.** Its
run_started / run_completed / run_failed / run_cancelled handlers rewrite
the whole run document field-by-field, so any field not explicitly listed
is dropped — meaning the key was lost on the *first* `run_started`, not
just on the resilient path. All four rebuild sites now carry it.
world-postgres is safe here by construction because it issues
column-scoped SQL UPDATEs rather than rewriting the row.

**base64 decoding is now strict.** The decoder accepted shapes that
cannot describe a whole number of bytes (`length % 4 === 1`) and ignored
anything after a mid-string `=`, returning a short array instead of
`undefined`. That is worse than throwing: a corrupt stored key looked
*present*, so callers sealed to garbage rather than taking the symmetric
fallback. Now rejects out-of-alphabet characters, bad lengths, misplaced
padding, and non-zero trailing bits — with a round-trip test over every
length 0–48 to make sure the strictness does not overshoot.
`splitEventDataForV4` lifted the run's public key into the frame meta and
`events.ts` spread that meta into `CreateEventV4Input`, but
`buildPostFrameMeta` — which copies meta onto the wire field by field — never
forwarded `encryptionPublicKey`, and the field was missing from
`CreateEventV4Input` entirely. Because the meta is applied with a spread,
TypeScript's excess-property check doesn't fire, so the key was computed, put
in the meta, and then silently dropped before the request was sent.

The server therefore never received the key, never stored it on the run
entity, and every cross-run writer fell back to the symmetric envelope. Every
symptom pointed away from the SDK: a deliberately oversized key was accepted
rather than rejected (the field never arrived), the key was absent from the run
row, and `resumeHook()` always chose `encr`.

Add the field to `CreateEventV4Input`, forward it in `buildPostFrameMeta`, and
cover it for both `run_created` and resilient-start `run_started`. Also add a
generic guard asserting that every field the splitter puts in the meta reaches
the wire, so the next omission in this hand-maintained mapping fails a test
instead of silently degrading encryption.

@karthikscale3 karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: Reviewed the run public-key publication path end to end. The key is derived only when encryption material exists and is propagated through run_created, queued runInput, resilient-start reconstruction, lifecycle rewrites, Postgres persistence/migration, and the Vercel v4 split/meta wire path. The three existing inline findings and the lifecycle/resilient-start bot findings are fixed on this head. The stale generated-file warning is not present and module-boundary checks pass. The matrix-wide Vercel production failures remain unrelated to this change.

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