Skip to main content
The Workflow DevKit uses a sophisticated serialization system to transfer data across execution boundaries—from client to workflow, workflow to step, and back again. This page explores the serialization format, special type handling, and the reducer/reviver pattern.

Why Serialization Matters

Workflows execute in isolated environments:
  • Client code runs in your application (Node.js, Edge, etc.)
  • Workflow code runs in a deterministic VM sandbox
  • Step code runs in the full Node.js runtime
Data must be serialized when crossing these boundaries: Each boundary has different requirements and constraints.

Serialization Format

The Workflow DevKit uses a prefix-based format system that allows self-describing payloads:
Why prefixes?
  • Self-describing: The World layer doesn’t need to know the format in advance
  • Gradual migration: Old runs keep working when new formats are introduced
  • Composability: Encryption can wrap any format (e.g., “encr” wrapping “devl”)
  • Debugging: Raw data inspection immediately reveals the format
Encoding with format prefix:
Decoding:

The Devalue Library

The current format (devl) uses the devalue library, which provides:
  • Circular reference support: Objects that reference themselves
  • Rich type support: Date, RegExp, Map, Set, typed arrays, etc.
  • Extensibility: Custom reducers/revivers for framework-specific types
  • Compact output: More efficient than JSON.stringify
Basic usage:

Special Type Handling

The Workflow DevKit extends devalue to handle workflow-specific types:

Reducers and Revivers

The Workflow DevKit uses a reducer/reviver pattern to customize serialization:
  • Reducers: Convert complex objects to serializable forms during stringify()
  • Revivers: Reconstruct complex objects from serialized forms during parse()

Common Reducers

From serialization.ts, the getCommonReducers() function handles types used across all boundaries:
Why check instanceof global.X? Different execution contexts (VM vs host) have different constructor functions. We check against the global parameter to handle both contexts.

Common Revivers

Revivers reverse the transformation:

Boundary-Specific Serialization

Different execution boundaries need different reducer/reviver sets:

External Boundary (Client ↔ Workflow)

From getExternalReducers() and getExternalRevivers(): Client → Workflow (arguments):
Workflow → Client (return value):

Workflow Boundary (Workflow ↔ Step)

From getWorkflowReducers() and getWorkflowRevivers(): Workflow → Step (arguments):
Step → Workflow (return value): From getStepReducers(), steps can create new streams:

Step Function Serialization

Step functions can be passed as arguments and serialized: Reducer:
Reviver (in workflow context):
Reviver (in step context):

Stream Serialization

Streams are serialized by piping data to server storage and passing stream names across boundaries:

Server-Side Stream Storage

WorkflowServerWritableStream:
WorkflowServerReadableStream:

Stream Framing

For non-byte streams (object streams), chunks are framed with length prefixes:
Why framing? Allows the deserializer to find chunk boundaries even when multiple chunks are concatenated or split across transport reads.

Request/Response in VM

The workflow VM provides stub implementations of Request and Response that defer body parsing to steps:
Why fake streams? Parsing response bodies is async work that would break determinism during replay. By storing the raw BodyInit and deferring parsing to a step, we ensure consistent replay behavior.

Error Handling

Serialization errors are wrapped with helpful context:
Example error:

Custom Class Serialization

Classes can implement custom serialization using special symbols:
Reducer:
Reviver:

Performance Considerations

Binary format: Using Uint8Array with format prefixes reduces string encoding overhead compared to JSON. Stream buffering: The WorkflowServerWritableStream batches writes every 10ms to reduce database round-trips:
Batch operations: When multiple chunks are buffered, writeToStreamMulti sends them in a single operation.

Conclusion

The Workflow DevKit’s serialization system provides:
  • Rich type support: Handles complex types like streams, requests, errors, and custom classes
  • Cross-boundary data transfer: Seamless serialization across client, workflow, and step contexts
  • Format evolution: Prefix-based format system allows gradual migration
  • Performance: Binary encoding and batched stream writes reduce overhead
  • Debugging: Helpful error messages with path information
This serialization layer is fundamental to the Workflow DevKit’s ability to provide durable, resumable workflows with a natural JavaScript developer experience.