Skip to content

Change user input/output to be binary data at the World interface#853

Merged
TooTallNate merged 10 commits into
mainfrom
01-23-serialize_data_as_binary
Jan 28, 2026
Merged

Change user input/output to be binary data at the World interface#853
TooTallNate merged 10 commits into
mainfrom
01-23-serialize_data_as_binary

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented Jan 26, 2026

This is a breaking change to the World interface, where we are changing serialization of workflow and step data to use binary format (Uint8Array) instead of JSON arrays ("revived" devalue serialization). This will allow the workflow client to be fully responsible for the data serialization format and introduce further enhancements such as encryption, compression, etc. without the World implementation needing to care what the underlying data represents.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 26, 2026

🦋 Changeset detected

Latest commit: ce52cc0

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

This PR includes changesets to release 19 packages
Name Type
@workflow/world-postgres Patch
@workflow/world-testing Patch
@workflow/world-vercel Patch
@workflow/world-local Patch
@workflow/web-shared Patch
@workflow/world Patch
@workflow/core Patch
@workflow/cli Patch
@workflow/example-nest Patch
@workflow/builders Patch
@workflow/docs-typecheck Patch
@workflow/next Patch
@workflow/nitro Patch
workflow Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/sveltekit Patch
@workflow/nuxt Patch
@workflow/ai 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 26, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 457 0 38 495
✅ 💻 Local Development 418 0 32 450
✅ 📦 Local Production 418 0 32 450
✅ 🐘 Local Postgres 418 0 32 450
✅ 🪟 Windows 45 0 0 45
❌ 🌍 Community Worlds 31 161 0 192
✅ 📋 Other 123 0 12 135
Total 1910 161 146 2217

❌ Failed Tests

🌍 Community Worlds (161 failed)

mongodb (40 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (40 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (40 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 41 0 4
✅ example 41 0 4
✅ express 41 0 4
✅ fastify 41 0 4
✅ hono 41 0 4
✅ nextjs-turbopack 44 0 1
✅ nextjs-webpack 44 0 1
✅ nitro 41 0 4
✅ nuxt 41 0 4
✅ sveltekit 41 0 4
✅ vite 41 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 45 0 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 5 40 0
✅ redis-dev 3 0 0
❌ redis 5 40 0
✅ starter-dev 3 0 0
❌ starter 4 41 0
✅ turso-dev 3 0 0
❌ turso 5 40 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 41 0 4
✅ e2e-local-postgres-nest-stable 41 0 4
✅ e2e-local-prod-nest-stable 41 0 4

📋 View full workflow run

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jan 26, 2026

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

Project Deployment Review Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jan 28, 2026 6:25pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jan 28, 2026 6:25pm
example-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-astro-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-express-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-fastify-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-hono-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-nitro-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workbench-vite-workflow Ready Ready Preview, Comment Jan 28, 2026 6:25pm
workflow-nest Ready Ready Preview, Comment Jan 28, 2026 6:25pm
1 Skipped Deployment
Project Deployment Review Updated (UTC)
workflow-docs Skipped Skipped Jan 28, 2026 6:25pm

Copy link
Copy Markdown
Member Author

TooTallNate commented Jan 26, 2026

Comment thread packages/world-vercel/src/steps.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the World interface to treat workflow/step input and output as opaque binary payloads (Uint8Array) instead of JSON-compatible “revived” devalue arrays, enabling transport-level enhancements (e.g., compression/encryption) without coupling World implementations to the underlying serialization.

Changes:

  • Switch core serialization to emit/consume Uint8Array (TextEncoder/TextDecoder over devalue.stringify/parse).
  • Update @workflow/world schemas/types and World implementations (local/postgres/vercel) to store/transport binary data and support resolveData='none' via *WithoutData types.
  • Update workbench triggers and tests to send/receive application/octet-stream request bodies and handle binary values in JSON where needed.

Reviewed changes

Copilot reviewed 54 out of 54 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
workbench/vite/routes/api/trigger.post.ts Reads request body as binary and hydrates args from Uint8Array.
workbench/sveltekit/src/routes/api/trigger/+server.ts Reads request body as binary and hydrates args from Uint8Array.
workbench/nuxt/server/api/trigger.post.ts Uses web Request + arrayBuffer() to support binary args in Nuxt/h3.
workbench/nitro-v3/routes/api/trigger.post.ts Reads request body as binary and hydrates args from Uint8Array.
workbench/nitro-v2/server/api/trigger.post.ts Uses web Request + arrayBuffer() to support binary args in Nitro v2.
workbench/nextjs-webpack/pages/api/trigger-pages.ts Disables body parser and reads raw binary body for args (pages router).
workbench/nextjs-webpack/app/api/trigger/route.ts Reads request body as binary for args (app router).
workbench/nextjs-turbopack/pages/api/trigger-pages.ts Disables body parser and reads raw binary body for args (pages router).
workbench/nextjs-turbopack/app/api/trigger/route.ts Reads request body as binary for args (app router).
workbench/hono/src/index.ts Reads request body as binary for args in Hono handler.
workbench/fastify/src/index.ts Adds octet-stream parser and hydrates args from binary body.
workbench/express/src/index.ts Adds raw octet-stream middleware and hydrates args from binary body.
workbench/example/api/trigger.ts Reads request body as binary for args in example route.
workbench/astro/src/pages/api/trigger.ts Reads request body as binary for args in Astro route.
packages/world/src/steps.ts Updates Step schema to use SerializedData and adds StepWithoutData.
packages/world/src/serialization.ts Defines SerializedData = Uint8Array and a Zod schema for it.
packages/world/src/runs.ts Updates run schemas to use SerializedData and adds WorkflowRunWithoutData.
packages/world/src/interfaces.ts Adds overloads for resolveData='none' returning *WithoutData types.
packages/world/src/index.ts Re-exports SerializedData and SerializedDataSchema.
packages/world/src/hooks.ts Changes hook metadata to use SerializedData.
packages/world/src/events.ts Updates event payload fields (input/output/result/payload) to SerializedData.
packages/world-vercel/src/utils.ts Updates hard-coded workflow-server URL override to a new test deployment.
packages/world-vercel/src/storage.ts Casts storage methods to new overloaded Storage signatures.
packages/world-vercel/src/steps.ts Updates step wire schema to accept Uint8Array and adjusts resolveData filtering.
packages/world-vercel/src/runs.ts Updates run wire schema/types and adds overloads for resolveData-none responses.
packages/world-testing/src/util.mts Adds JSON reviver to decode base64-wrapped Uint8Array from server responses.
packages/world-testing/src/server.mts Adds JSON serialization for Uint8Array as base64-wrapped objects.
packages/world-testing/src/null-byte.mts Updates tests to hydrate workflow output from Uint8Array.
packages/world-testing/src/idempotency.mts Updates tests to hydrate workflow output from Uint8Array.
packages/world-testing/src/hooks.mts Updates tests to hydrate workflow output from Uint8Array.
packages/world-testing/src/errors.mts Updates tests to hydrate workflow output from Uint8Array.
packages/world-testing/src/addition.mts Updates tests to hydrate workflow output from Uint8Array.
packages/world-postgres/test/storage.test.ts Updates postgres tests and legacy inserts for CBOR/binary input columns.
packages/world-postgres/src/storage.ts Updates storage typing and filtering for resolveData='none' and binary IO.
packages/world-local/src/test-helpers.ts Updates helper types to use SerializedData for input/metadata.
packages/world-local/src/storage/steps-storage.ts Updates resolveData-none behavior and typing for step listing/getting.
packages/world-local/src/storage/runs-storage.ts Updates resolveData-none behavior and typing for run listing/getting.
packages/world-local/src/storage/legacy.ts Adjusts legacy event handling with updated run filtering types.
packages/world-local/src/storage/filters.ts Adds overloads returning *WithoutData when resolveData='none'.
packages/world-local/src/storage/events-storage.ts Updates run creation to store binary input values.
packages/world-local/src/storage.test.ts Updates local storage tests and uses writeJSON helper.
packages/world-local/src/fs.ts Adds JSON replacer/reviver to persist Uint8Array as base64-wrapped objects.
packages/web-shared/src/api/workflow-server-actions.ts Adjusts UI server actions to handle resolveData='none' data shapes.
packages/core/src/workflow.ts Changes workflow execution to return binary (Uint8Array) results.
packages/core/src/step.test.ts Updates step tests to use binary dehydrated step results.
packages/core/src/serialization.ts Switches serialization to binary (Uint8Array) via TextEncoder/TextDecoder.
packages/core/src/serialization.test.ts Updates serialization tests for binary output representation.
packages/core/src/runtime/suspension-handler.ts Updates hook/step event payload typing to SerializedData.
packages/core/src/runtime/step-handler.ts Updates step_completed event payload typing to Uint8Array.
packages/core/src/runtime.ts Updates run_completed event payload to store binary output.
packages/core/src/observability.ts Updates observability hydration to detect Uint8Array input/output.
packages/core/e2e/e2e.test.ts Sends trigger args as application/octet-stream with binary body.
packages/cli/src/lib/inspect/output.ts Updates CLI pagination typing for *WithoutData result shapes.

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

Comment on lines 88 to 92
deploymentId: string;
workflowName: string;
input: SerializedData[];
input: SerializedData;
executionContext?: SerializedData;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

CreateWorkflowRunRequest.executionContext is typed as SerializedData (now Uint8Array), but the run schema defines executionContext as z.record(z.string(), z.any()).optional(). This makes the request type inconsistent with what WorkflowRunBaseSchema accepts and with UpdateWorkflowRunRequest.executionContext (record). Consider changing executionContext?: Record<string, any> (or a dedicated type) here to match the schema (and keep only input/output as SerializedData).

Copilot uses AI. Check for mistakes.
Comment thread packages/world-vercel/src/steps.ts Outdated
Comment on lines 108 to 112
return {
...deserialized,
input: [],
input: new Uint8Array(),
output: undefined,
};
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

When resolveData === 'none', this returns a Step with input: new Uint8Array().

But the updated World/Storage typings introduce StepWithoutData where input/output are undefined for resolveData='none'. Returning an empty Uint8Array makes the behavior inconsistent across World implementations and can confuse callers that use undefined as the signal that data was excluded. Consider returning StepWithoutData here (and updating the list/get return types accordingly) with input: undefined and output: undefined.

Copilot uses AI. Check for mistakes.
Comment on lines 640 to 643
return createResponse({
data: (result.data as Step[]).map(hydrate),
// StepWithoutData has undefined input/output, but after hydration the structure is compatible
data: (result.data as unknown as Step[]).map(hydrate),
cursor: result.cursor ?? undefined,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

This code path calls hydrate(...), but hydrate() currently JSON-stringifies/parses the resource before calling hydrateResourceIO. With the new binary I/O, that round-trip converts Uint8Array into a plain object, so hydrateResourceIO will no longer detect input/output as Uint8Array and hydration will silently stop working.

Consider updating hydrate() to preserve Uint8Array (e.g., hydrate first, then make JSON-compatible; or use a custom replacer/reviver that encodes Uint8Array as base64).

Copilot uses AI. Check for mistakes.
Comment on lines +840 to +845
const deploymentId = run.deploymentId;
// hydrateResourceIO deserializes the binary input back to the original array
const newRun = await start(
{ workflowId: run.workflowName },
hydratedRun.input,
hydratedRun.input as unknown as unknown[],
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

hydratedRun comes from hydrate(...), but hydrate() currently JSON-stringifies/parses the run before calling hydrateResourceIO, which strips Uint8Array instances. That means hydratedRun.input may not actually be hydrated back into an arguments array, and the double-cast to unknown[] can mask this at compile time.

After fixing hydrate() to preserve binary data, consider removing the unsafe cast here and ensuring hydratedRun.input is validated/typed as an array before passing it to start(...).

Copilot uses AI. Check for mistakes.
...getProtectionBypassHeaders(),
'Content-Type': 'application/octet-stream',
},
body: dehydratedArgs.buffer as BodyInit,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

body: dehydratedArgs.buffer ignores byteOffset/byteLength and will send the entire underlying ArrayBuffer if dehydratedArgs is ever a view into a larger buffer. Using the Uint8Array directly (or slicing the buffer to the view range) avoids accidentally sending extra bytes.

Suggested change
body: dehydratedArgs.buffer as BodyInit,
body: dehydratedArgs as BodyInit,

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
import {
defineEventHandler,
getRequestURL,
readRawBody,
toWebRequest,
} from 'h3';
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Unused import readRawBody.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
import {
defineEventHandler,
getRequestURL,
readRawBody,
toWebRequest,
} from 'h3';
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Unused import readRawBody.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

🔧 Build Fix:

Type mismatch error: an any[] is being passed where a Uint8Array<ArrayBufferLike> is expected. The array type doesn't have the required properties like BYTES_PER_ELEMENT, buffer, byteLength, etc.

Fix on Vercel

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 27, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.043s (+3.6%) 1.008s (~) 0.964s 10 1.00x
🐘 Postgres Express 0.253s (-35.5% 🟢) 1.016s (~) 0.763s 10 5.82x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.722s (-2.9%) 1.680s (-4.4%) 0.958s 10 1.00x

🔍 Observability: Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.117s (~) 2.008s (~) 0.891s 10 1.00x
🐘 Postgres Express 2.230s (-3.7%) 3.016s (~) 0.785s 10 2.00x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.043s (-6.3% 🟢) 3.967s (-7.1% 🟢) 0.924s 10 1.00x

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 10.845s (~) 11.016s (~) 0.171s 5 1.00x
🐘 Postgres Express 20.415s (+1.0%) 21.039s (~) 0.624s 5 1.88x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 23.356s (-3.9%) 24.149s (-4.4%) 0.793s 5 1.00x

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 5.145s (+268.1% 🔺) 6.128s (+205.5% 🔺) 0.983s 5 1.00x
🐘 Postgres Express 27.840s (+1102.3% 🔺) 28.696s (+852.5% 🔺) 0.856s 2 5.41x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.357s (+37.2% 🔺) 5.245s (+22.0% 🔺) 0.888s 6 1.00x

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 5.200s (+114.5% 🔺) 6.201s (+102.9% 🔺) 1.001s 5 1.00x
🐘 Postgres Express 29.536s (+238.1% 🔺) 30.156s (+223.5% 🔺) 0.620s 2 5.68x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.335s (+5.1% 🔺) 4.151s (+6.7% 🔺) 0.816s 8 1.00x

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 5.398s (+278.7% 🔺) 6.366s (+217.4% 🔺) 0.968s 5 1.00x
🐘 Postgres Express 29.420s (+1307.9% 🔺) 30.108s (+1098.6% 🔺) 0.688s 1 5.45x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.381s (+15.2% 🔺) 4.165s (+4.0%) 0.785s 8 1.00x

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 5.494s (+118.3% 🔺) 6.417s (+112.8% 🔺) 0.923s 5 1.00x
🐘 Postgres Express 32.891s (+179.7% 🔺) 33.153s (+167.8% 🔺) 0.262s 1 5.99x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.627s (+5.9% 🔺) 4.478s (+8.3% 🔺) 0.851s 7 1.00x

🔍 Observability: Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.183s (+1.0%) 0.993s (~) 0.015s (+1.4%) 1.022s (~) 0.839s 10 1.00x
🐘 Postgres Express 2.182s (-9.5% 🟢) 2.861s (+8.8% 🔺) 0.000s (NaN%) 3.016s (~) 0.834s 10 11.93x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.254s (~) 3.483s (-1.6%) 1.232s (+39.6% 🔺) 5.293s (+5.1% 🔺) 2.039s 10 1.00x

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Express 8/8
🐘 Postgres Express 8/8
▲ Vercel Express 8/8
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This is a well-executed architectural change that properly abstracts serialization concerns from the World interface. The implementation is thorough and consistent across all packages.

Key Strengths

  • Consistent implementation across all World backends (local, postgres, vercel)
  • Good use of function overloads for type safety with resolveData options
  • Thorough test coverage updates
  • Clean separation allowing future encryption/compression without World changes

Issues Requiring Attention

1. Critical: Backwards Compatibility for Observability (see inline comments)

The observability code in packages/core/src/observability.ts now only checks for Uint8Array, which will break rendering of specVersion 1 (legacy) runs. Legacy runs store data as JSON arrays, not Uint8Array.

Recommendation: Use specVersion to branch between legacy (unflatten on arrays) and new (TextDecoder + parse on Uint8Array) formats. This can be shipped in a follow-up PR but must be done before releasing a new version to avoid shipping broken observability.

2. Changeset should be marked as breaking change

Per project conventions, breaking changes should include **BREAKING CHANGE** in the changeset description.

3. Minor: Inconsistency in world-vercel/steps.ts

When resolveData='none', this file returns input: new Uint8Array() while other implementations return input: undefined. This should be consistent.

Verdict

Approve with the understanding that the observability backwards compatibility fix will be addressed before release.

Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Code Review: Binary Serialization at World Interface

Overall Assessment

This is a well-executed architectural change that properly abstracts serialization concerns from the World interface. The implementation is thorough and consistent across all packages.

Key Strengths

  • Consistent implementation across all World backends (local, postgres, vercel)
  • Good use of function overloads for type safety with resolveData options
  • Thorough test coverage updates
  • Clean separation allowing future encryption/compression without World changes

🚨 Critical: Backwards Compatibility Issue in Observability

The observability code in packages/core/src/observability.ts now only checks for Uint8Array, which will break rendering of specVersion 1 (legacy) runs.

The Problem

Legacy runs (specVersion 1) store data as JSON arrays, not Uint8Array. The new code:

// packages/core/src/observability.ts - hydrateStepIO
input:
  step.input instanceof Uint8Array && step.input.byteLength > 0
    ? hydrateStepArguments(step.input, ...)
    : step.input,  // ← Legacy data falls through UNHYDRATED!
specVersion Storage Format Observability Check Result
1 (legacy) JSON arrays instanceof Uint8Arrayfalse ❌ Raw arrays shown
2 (new) Binary instanceof Uint8Array → true ✅ Properly hydrated

Recommended Fix (use specVersion to branch)

const hydrateStepIO = <T extends { stepId?: string; input?: any; output?: any; runId?: string; specVersion?: number }>(
  step: T
): T => {
  const isLegacy = (step.specVersion ?? 1) < 2;
  
  if (isLegacy) {
    // Legacy path: use unflatten directly on JSON arrays
    return {
      ...step,
      input: step.input && Array.isArray(step.input) && step.input.length
        ? unflatten(step.input, { ...getStepRevivers(...), ...streamPrintRevivers })
        : step.input,
      output: step.output
        ? unflatten(step.output, { ...getWorkflowRevivers(...), ...streamPrintRevivers })
        : step.output,
    };
  }
  
  // New path: decode Uint8Array (current implementation)
  return { ... };
};

Functions that need this fix in observability.ts:

  1. hydrateStepIO - step input/output
  2. hydrateWorkflowIO - workflow input/output
  3. hydrateEventData - eventData.result
  4. hydrateHookMetadata - hook metadata

This can be shipped in a follow-up PR, but must be done before releasing a new version of @workflow/web-shared to avoid shipping broken observability for historical runs.


Other Issues

Changeset should be marked as breaking

Per project conventions, the changeset should include **BREAKING CHANGE** in the description.

Minor: Inconsistency in world-vercel/steps.ts

When resolveData='none', packages/world-vercel/src/steps.ts:110 returns input: new Uint8Array() while other implementations return input: undefined. Should be consistent.


Verdict

👍 Approve with the understanding that the observability backwards compatibility fix will be addressed before release.

Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Approving with the understanding that the observability backwards compatibility fix (for specVersion 1 legacy runs) will be addressed before releasing a new version of @workflow/web-shared.

Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

LGTM with the latest fixes - let's 🚢 and then do a bug bash

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.

4 participants