Serialization

All function arguments and return values passed between workflow and step functions must be serializable. Workflow DevKit uses a custom serialization system built on top of devalueExternal link. This system supports standard JSON types, as well as a few additional popular Web API types.

The serialization system ensures that all data persists correctly across workflow suspensions and resumptions, enabling durable execution.

Supported Serializable Types

The following types can be serialized and passed through workflow functions:

Standard JSON Types:

  • string
  • number
  • boolean
  • null
  • Arrays of serializable values
  • Objects with string keys and serializable values

Extended Types (with special handling):

  • undefined
  • ArrayBuffer
  • BigInt64Array, BigUint64Array
  • Date
  • Float32Array, Float64Array
  • Headers
  • Int8Array, Int16Array, Int32Array
  • Map<Serializable, Serializable>
  • RegExp
  • Response
  • Set<Serializable>
  • URL
  • URLSearchParams
  • Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array
  • ReadableStream<Serializable>
  • WritableStream<Serializable>

Streaming

As noted in the list above, ReadableStream and WritableStream are supported in Workflow DevKit's serialization system. These streams are automatically managed by the Workflow runtime and can be passed between workflow and step functions, while maintaining their streaming capabilities.

However, there is an important consideration to keep in mind which is that streams cannot be interacted with within the workflow function context. They should be considered opaque handles that may be passed around between steps.

Why this limitation?

Workflow functions must be deterministic and replay-safe. Streams represent asynchronous, non-deterministic data flow. When you pass a stream through a workflow, only the stream reference is serialized—not the actual streaming data. The stream data flows directly between steps without being persisted, which maintains efficiency while preserving the workflow's ability to resume.

This pattern is particularly useful for handling large datasets, streaming LLM responses, or processing file uploads/downloads where you want to pass data efficiently between steps without loading everything into memory.

For example, one step function may produce a ReadableStream while a different step consumes the stream. The workflow function does not interact directly with the stream but is able to pass it on to the next step:

async function generateStream() {
  "use step";

  return new ReadableStream({
    async start(controller) {
      controller.enqueue(1);
      controller.enqueue(2);
      controller.enqueue(3);
      controller.close();
    }
  });
}

async function consumeStream(readable: ReadableStream<number>) {
  "use step";

  const values: number[] = [];
  for await (const value of readable) {
    values.push(value);
  }

  console.log(values);
  // Logs: [1, 2, 3]
}

export async function passingStreamWorkflow() {
  "use workflow";

  // ✅ Stream is received as a return value and passed
  // into a step, but NOT directly used in the workflow
  const readable = await generateStream();  
  await consumeStream(readable);  
}

What NOT to do: Do not attempt to read from or write to a stream directly within the workflow function context, as this will not work as expected and an error will be thrown at runtime:

export async function incorrectStreamUsage() {
  "use workflow";

  const readable = await generateStream();

  // ❌ This will fail - cannot read stream in workflow context
  const reader = readable.getReader(); 
}

Always delegate stream operations to step functions.

Request & Response

The Web API Request and Response APIs are supported by the serialization system, and can be passed around between workflow and step functions similarly to other data types.

As a convenience, these two APIs are treated slightly differently when used within a workflow function: calling the text() / json() / arrayBuffer() instance methods is automatically treated as a step function invocation. This allows you to consume the body directly in the workflow context while maintaining proper serialization and caching.

For example, consider how receiving a webhook request provides the entire Request instance into the workflow context. You may consume the body of that request directly in the workflow, which will be cached as a step result for future resumptions of the workflow:

import { createWebhook } from 'workflow';

export async function handleWebhookWorkflow() {
  "use workflow";

  const webhook = createWebhook();
  const request = await webhook;

  // The body of the request will only be consumed once
  const body = await request.json(); 

  // …
}