Why Channels Exist
Function invocations in iii pass data as JSON-serializable messages. This works well for structured payloads, but breaks down when dealing with large binary blobs (files, media, datasets) or data that is produced over time (progress updates, partial results). Channels solve this by giving each side a Node.js stream backed by a WebSocket connection through the engine. A channel has two ends:- A writer that exposes a
Writablestream for sending data. - A reader that exposes a
Readablestream for receiving data.
StreamChannelRef) that can be passed as a field inside iii.trigger() data. When the receiving function deserializes the ref, the SDK automatically connects it to the engine and materializes the corresponding ChannelWriter or ChannelReader.
How Channels Work
Function A creates a channel, passes thereaderRef to Function B through a regular function call, then writes data to the writer stream. The engine routes the data through a WebSocket-backed pipe to Function B, where it arrives on the reader stream. When Function A ends the writer, Function B’s reader emits end.
Creating a Channel
- Node / TypeScript
- Python
- Rust
| Property | Node / TypeScript | Python | Rust |
|---|---|---|---|
| Writer (local) | channel.writer (ChannelWriter) | channel.writer (ChannelWriter) | channel.writer (ChannelWriter) |
| Reader (local) | channel.reader (ChannelReader) | channel.reader (ChannelReader) | channel.reader (ChannelReader) |
| Writer ref (serializable) | channel.writerRef | channel.writer_ref | channel.writer_ref |
| Reader ref (serializable) | channel.readerRef | channel.reader_ref | channel.reader_ref |
iii.trigger() data to hand the other end of the channel to another function. The SDK on the receiving side automatically materializes the ref into a ChannelWriter or ChannelReader.
The StreamChannelRef is a plain object that survives JSON serialization:
ChannelWriter
ChannelWriter wraps a WebSocket connection and exposes:
| Capability | Node / TypeScript | Python | Rust |
|---|---|---|---|
| Write binary data | writer.stream.write(data) | writer.stream.write(data) (sync, fire-and-forget) or await writer.write(data) (async) | writer.write(&data).await |
| Send text message | writer.sendMessage(msg) | writer.send_message(msg) (sync, fire-and-forget) or await writer.send_message_async(msg) (async) | writer.send_message(&msg).await |
| Close | writer.stream.end() | writer.close() (sync, fire-and-forget) or await writer.close_async() (async) | writer.close().await |
| Access writable stream | writer.stream | writer.stream (WritableStream) | N/A |
ChannelWriter provides both sync and async APIs. The sync methods (send_message(), close()) are fire-and-forget wrappers that queue work on the event loop. The writer.stream property exposes a WritableStream with sync write(data) and end(data?) methods that mirror Node.js Writable semantics.
ChannelReader
ChannelReader wraps a WebSocket connection and exposes:
| Capability | Node / TypeScript | Python | Rust |
|---|---|---|---|
| Read as stream | for await (const chunk of reader.stream) | async for chunk in reader | reader.next_binary().await / reader.read_all().await |
| Read all at once | Collect chunks manually | await reader.read_all() | reader.read_all().await |
| Listen for text messages | reader.onMessage(callback) | reader.on_message(callback) | reader.on_message(callback).await |
| Access readable stream | reader.stream | reader.stream (ReadableStream) | N/A |
ChannelReader implements __aiter__, so use async for chunk in reader to iterate over binary chunks. Text messages received on the same WebSocket are dispatched to callbacks registered via reader.on_message(callback) where callback is Callable[[str], Any] — it receives the raw text message string.
Backpressure is handled automatically by pausing the WebSocket when the stream buffer is full. The reader connects lazily, establishing the WebSocket when reading begins.
Example: One-Way Data Streaming
In this pattern a sender function creates a channel, writes data to it, and passes the reader ref to a processor function. The processor reads the stream, computes a result, and returns it.- Node / TypeScript
- Python
- Rust
Example: Bidirectional Streaming with Progress
When two functions need to exchange data in both directions, create two channels — one for input, one for output. The writer’ssendMessage() method provides a side channel for progress updates or metadata that doesn’t mix with the binary data stream.
- Node / TypeScript
- Python
- Rust
sendMessage(), and writes the final result to the output channel’s stream. Meanwhile, the coordinator listens for text messages on the output channel’s reader and reads the binary result from the output reader stream.