Skip to content

feat: add the Streams API, with Stream Stores on Redis and in memory - #2129

Draft
moetemp wants to merge 9 commits into
moe/streams-generated-codefrom
moe/streams-api
Draft

moetemp wants to merge 9 commits into
moe/streams-generated-codefrom
moe/streams-api

Conversation

@moetemp

@moetemp moetemp commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds the Streams API to the Python SDK. A Stream is an ordered log of Records that a Workflow, an Activity or a client writes, and that any of them reads, for example the tokens of a model call. It is experimental.

The public API is the code that nexgen generates from the Stream WIT, which is in #2128. This PR is the hand-written part. It adds the Stream Stores, the config that gives them to a client, and the hooks that send each generated call to a Store.

from temporalio.client import Client, StreamConfig
from temporalio.contrib.redis.streams import RedisStreamStore

store = RedisStreamStore("redis://localhost:6379")
client = await Client.connect("localhost:7233", stream_config=StreamConfig(stores=[store]))

stream = await client.create_stream(id="chat-43", body_type=str)
await stream.append("Hello", " world")
async for record in stream.iter_read_items():
    print(record.body)
await stream.close()

Where to look

Part Files What it has
Stores temporalio/client/_stream_store.py, temporalio/contrib/redis/streams.py, temporalio/testing/_stream_store.py StreamStore, a public interface with the five operations and two Worker hooks. StreamConfig, with the Stores and the selector. RedisStreamStore and MemoryStreamStore.
Client hook temporalio/client/_impl.py, temporalio/client/_client.py Sends a Stream operation of the generated client to the Store of its ref.
Workflow hook temporalio/worker/_stream_store.py, _workflow_instance.py, _worker.py, _replayer.py Answers a Stream call of Workflow code through the Store. The Worker registers the Activities that each Store brings.
Support of the generated code temporalio/nexus/system/_support.py, temporalio/nexus/system/__init__.py The Producer of a call, the request id of a create, the poll helpers of a read, and the conversion between a model and its proto message.
Appender temporalio/contrib/streams/ BufferedAppender, which batches appends. It is a library on top of the generated API, so it is in contrib.
Bridge temporalio/bridge/src/streams.rs, temporalio/bridge/streams.py, temporalio/bridge/Cargo.toml One call into Core's temporalio-streams runtime, behind the streams and streams-redis features.
Errors temporalio/exceptions.py StreamError with a reason, and StreamConfigError.
Generation scripts/gen_nexus_system_api.py, scripts/gen_payload_visitor.py The Stream package and its payload visitor in poe gen-protos.

How it behaves

  • Config. A client takes stream_config=StreamConfig(stores=[...], store_selector=...), in the shape of ExternalStorage. A Worker takes the config from its client and serves every Store in it.
  • Routing. A Stream operation goes to the Store that its ref names, else to the only Store, else to the one that the selector returns. A config can hold several Stores with no selector, and a call that names no Store then raises StreamConfigError. With no config, a client or Activity call raises StreamConfigError, since this release has no Streams on the server. In Workflow code that case fails the Workflow with a non-retryable ApplicationError of type StreamConfigError, so a Worker with a wrong setup is visible.
  • Workflow code can create, get, append to, close and describe a Stream. The Store answers a create, an append and a describe through a Local Activity of its own, so a replay sends nothing to the Store. A close runs the same Activity as a regular Activity, so it reaches the Store only after its Workflow Task completes. The Workflow Stream has no read yet.
  • Fencing. A write from a Local Activity uses its Workflow Task as its epoch, so a Store fences the writes of a failed Workflow Task. append also takes an optional sequence, and a batch that has one is written once. A Workflow append sends one, so a retry of its Local Activity in the same Workflow Task writes once.
  • Workers. A Worker with Workflows and a Stream config polls for Activity Tasks, since a Workflow's close is a regular Activity.
  • Batching. temporalio.contrib.streams.BufferedAppender collects the appends of up to max_delay into one batch, with one write in flight. In Workflow code it uses no timer, and the appends of one activation go as one write.
  • Stores. RedisStreamStore keeps Streams in Redis, and MemoryStreamStore keeps them in memory for tests. Both run Core's temporalio-streams crate through the bridge. A person can also write a Store in Python.
  • Errors. A failed Stream call raises StreamError with a reason. A wrong setup of the Stores raises StreamConfigError.

What it depends on

Please note that the README has no section on Streams yet.

A client takes its Stream Stores in a `StreamConfig`, and each call of the generated Streams
API goes to one of them. The Stores run Core's Stream runtime through the bridge.
The Stream WIT now gives the Workflow target `append` and `close`, and keeps `read` out of it.
The Workflow `Stream` has `append` again, and Workflow code can't do I/O. The Local Activity writes as its own Producer with a fixed attempt, so a retry in one Workflow Task writes once.
A Local Activity reaches the Store before its Workflow Task completes, so a failed task would leave a closed Stream. A Worker with Stream Stores now polls for Activity Tasks, so it can run the close.
A create can name its Store with `store=`, so a config with several Stores needs no selector.
A call that names no Store on such a config raises `StreamConfigError` at the call.
A failed Workflow Task retries for good, and a Worker with no Stream config never recovers.
Workflow code now gets a non-retryable `ApplicationError`, so a user sees the failed Workflow.
A timer of the Workflow event loop goes into History, so in Workflow code the appender starts
its write at the next yield instead. The appends of one activation become one Local Activity.
The public Streams API is the generated code. A helper on top of it is a library,
so it belongs in contrib.
The other contrib packages have one, and the appender's rules for Workflow code need a place.

This branch has not been deployed

No deployments
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.

1 participant