Skip to main content
Workflows can stream data in real-time to clients without waiting for the entire workflow to complete. This enables progress updates, AI-generated content, log messages, and other incremental data to be delivered as workflows execute.

Getting Started with getWritable()

Every workflow run has a default writable stream that steps can write to using getWritable(). Data written to this stream becomes immediately available to clients consuming the workflow’s output.

Consuming the Stream

Use the Run object’s readable property to consume the stream from your API route:
When a client makes a request to this endpoint, they’ll receive each message as it’s written, without waiting for the workflow to complete.

Resuming Streams from a Specific Point

Use run.getReadable({ startIndex }) to resume a stream from a specific position. This is useful for reconnecting after timeouts or network interruptions:
This allows clients to reconnect and continue receiving data from where they left off, rather than restarting from the beginning.

Streams as Data Types

ReadableStream and WritableStream are standard Web Streams API types that Workflow DevKit makes serializable. These are not custom types - they follow the web standard - but Workflow DevKit adds the ability to pass them between functions while maintaining their streaming capabilities. Unlike regular values that are fully serialized to the event log, streams maintain their streaming capabilities when passed between functions. Key properties:
  • Stream references can be passed between workflow and step functions
  • Stream data flows directly without being stored in the event log
  • Streams preserve their state across workflow suspension points
How Streams Persist Across Workflow SuspensionsStreams in Workflow DevKit are backed by persistent, resumable storage provided by the “world” implementation. This is what enables streams to maintain their state even when workflows suspend and resume:
  • Vercel deployments: Streams are backed by a performant Redis-based stream
  • Local development: Stream chunks are stored in the filesystem

Passing Streams as Arguments

Since streams are serializable data types, you don’t need to use the special getWritable(). You can even wire your own streams through workflows, passing them as arguments from outside into steps. Here’s an example of passing a request body stream through a workflow to a step that processes it:

Important Limitation

Streams Cannot Be Used Directly in Workflow ContextYou cannot read from or write to streams directly within a workflow function. All stream operations must happen in step functions.
Workflow functions must be deterministic to support replay. Since streams bypass the event log for performance, reading stream data in a workflow would break determinism - each replay could see different data. By requiring all stream operations to happen in steps, the framework ensures consistent behavior. For more on determinism and replay, see Workflows and Steps.

Namespaced Streams

Use getWritable({ namespace: 'name' }) to create multiple independent streams for different types of data. This is useful when you want to separate logs, metrics, data outputs, or other distinct channels.

Consuming Namespaced Streams

Use run.getReadable({ namespace: 'name' }) to access specific streams:

Common Patterns

Progress Updates for Long-Running Tasks

Send incremental progress updates to keep users informed during lengthy workflows:

Streaming AI Responses with DurableAgent

Stream AI-generated content using DurableAgent from @workflow/ai. Tools can also emit progress updates to the same stream using data chunks with the UIMessageChunk type from the AI SDK:
For a complete implementation, see the flight booking example which demonstrates streaming AI responses with tool progress updates.

Streaming Between Steps

One step produces a stream and another step consumes it:

Processing Large Files Without Memory Overhead

Process large files by streaming chunks through transformation steps:

Best Practices

Release locks properly:
Stream locks acquired in a step only apply within that step, not across other steps. This enables multiple writers to write to the same stream concurrently.
If a lock is not released, the step function’s HTTP request cannot terminate. Even though the step returns and the workflow continues, the underlying request will remain active until it times out—wasting compute resources unnecessarily.
Close streams when done:
Streams are automatically closed when the workflow run completes, but explicitly closing them signals completion to consumers earlier. Use typed streams for type safety:

Stream Failures

When a step returns a stream, the step is considered successful once it returns, even if the stream later encounters an error. The workflow won’t automatically retry the step. The consumer of the stream must handle errors gracefully. For more on retry behavior, see Errors and Retries.
Stream errors don’t trigger automatic retries for the producer step. Design your stream consumers to handle errors appropriately. Since the stream is already in an errored state, retrying the consumer won’t help - use FatalError to fail the workflow immediately.