Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
533306d
docs(creating-workers): add overview page
anthonyiscoding May 20, 2026
51953f1
docs(landing): consolidate "The Solution" with quadratic-to-zero
anthonyiscoding May 20, 2026
9984a10
chore: auto-render skill artifacts
github-actions[bot] May 20, 2026
f4a562e
docs(creating-workers): restore review TODOs and refresh worker docs
anthonyiscoding May 20, 2026
879a16b
docs(creating-workers/workers): refactor lifecycle and shutdown sections
anthonyiscoding May 20, 2026
26e785b
docs: rename SDK binding `iii` -> `worker`, polish, link out worker p…
anthonyiscoding May 20, 2026
dfa1ed4
docs(creating-workers): clarify "Declare a trigger type"
anthonyiscoding May 20, 2026
3dcbac4
docs: cross-link triggers, add built-in functions index, stub how-tos
anthonyiscoding May 20, 2026
90a47d8
docs(creating-workers/triggers): rework "Declaring a Trigger Type"
anthonyiscoding May 20, 2026
2a57e15
docs(creating-workers/triggers): expand Components and add mini-http …
anthonyiscoding May 20, 2026
3fe3dd1
docs(creating-workers/triggers): strip em dashes flagged by Vale
anthonyiscoding May 20, 2026
038dfa1
docs(creating-workers/triggers): tighten intro framing
anthonyiscoding May 20, 2026
aaf4e44
docs(creating-workers): add "Scaffold a new worker" and strip em dashes
anthonyiscoding May 20, 2026
747a12e
docs(creating-workers/workers): split iii worker init re-run notes
anthonyiscoding May 20, 2026
c9b79d9
docs(functions): consolidate runtime-validation note in creating-workers
anthonyiscoding May 20, 2026
c697d54
docs(using-iii/functions): show basic trigger examples inline
anthonyiscoding May 20, 2026
9c662a2
docs: HTTP-invokable functions, trigger metadata, and polish
anthonyiscoding May 20, 2026
111c80f
Merge branch 'main' into docs/phase-2
anthonyiscoding May 20, 2026
7a795a5
chore: auto-render skill artifacts
github-actions[bot] May 20, 2026
8491071
rerun
anthonyiscoding May 20, 2026
2b72b92
docs(using-iii/workers): group operations under "Managing workers"
anthonyiscoding May 21, 2026
922b42e
docs: cross-link lockfile + restructure Versioning + register-fn polish
anthonyiscoding May 21, 2026
02d6ee7
docs(creating-workers): fix review findings
anthonyiscoding May 21, 2026
03b6ad0
Merge branch 'main' into docs/phase-2
anthonyiscoding May 21, 2026
34342d0
chore: auto-render skill artifacts
github-actions[bot] May 21, 2026
9d5069a
docs(using-iii/channels): restyle + add payload-size section
anthonyiscoding May 21, 2026
da2d00f
chore: auto-render skill artifacts
github-actions[bot] May 21, 2026
6683ba4
docs(using-iii/channels): regroup local-end ops under "Using channels"
anthonyiscoding May 21, 2026
f2c6a35
docs: move channels to creating-workers
anthonyiscoding May 21, 2026
caa8ee5
docs(understanding-iii/channels): polish lazy-connect and lifecycle copy
anthonyiscoding May 21, 2026
501232d
docs(custom.css): tighten code block line-height and padding
anthonyiscoding May 21, 2026
8b39451
docs(custom.css): enable tabular-nums in code blocks
anthonyiscoding May 21, 2026
00fc911
docs(using-iii/triggers): add code samples to last three sections
anthonyiscoding May 21, 2026
145ffcd
docs(understanding-iii): merge workers/triggers/functions into the ov…
anthonyiscoding May 21, 2026
a4996f5
docs(using-iii/console): copy polish
anthonyiscoding May 21, 2026
f396fd8
docs: fix code samples surfaced by doc walkthrough
anthonyiscoding May 21, 2026
9b6cd9b
docs(using-iii/triggers): replace em dashes with parentheses
anthonyiscoding May 21, 2026
24e3954
docs: re-render skill artifacts
anthonyiscoding May 21, 2026
f558702
Merge branch 'main' into docs/phase-2
anthonyiscoding May 21, 2026
0a7b26f
chore: auto-render skill artifacts
github-actions[bot] May 21, 2026
4e62745
Merge branch 'main' into docs/phase-2
anthonyiscoding May 22, 2026
02ff113
docs: fix review findings on worker voice and install promo list
anthonyiscoding May 22, 2026
7c52d6e
Merge branch 'main' into docs/phase-2
anthonyiscoding May 22, 2026
c2b9065
rerun
anthonyiscoding May 22, 2026
b4926c7
Merge branch 'main' into docs/phase-2
anthonyiscoding May 22, 2026
bde5d11
Merge branch 'main' into docs/phase-2
anthonyiscoding May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .skill-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ docs:
- "docs/0-10-0/**"
- "docs/0-11-0/**"
- "docs/changelog/**"
- "docs/**/changelog/**"
ai_check:
provider: anthropic
model: claude-sonnet-4-6
Expand Down
332 changes: 332 additions & 0 deletions docs/creating-workers/channels.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
---
title: "Channels"
description: "Stream large or binary payloads between iii workers."
owner: "devrel"
type: "how-to"
---

Channels move large or binary payloads between iii workers without putting the data in a JSON
function payload. Use one when the payload is expected to be large (files, images, datasets), above
roughly **16 MB**, intended to be streamed (audio, video), or you want incremental progress updates
during long-running work. For small JSON, stick with a regular `worker.trigger(...)` call.

<Note>
For the underlying model (how channels are addressed, multiplexed, and torn down), see [Channels
architecture](/understanding-iii/channels).
</Note>

## Payload size

iii itself doesn't enforce a maximum trigger-payload size. The effective ceiling comes from
whichever WebSocket library the engine and the calling SDK use, and each has its own defaults for
the per-frame and per-message size. The smallest common per-frame default sits around 16 MB, which
is the practical line at which you should switch to a channel.

The libraries iii currently relies on:

- **Engine** ([axum](https://docs.rs/axum) on top of
[tokio-tungstenite](https://docs.rs/tokio-tungstenite) ). See
[`WebSocketConfig`](https://docs.rs/tungstenite/latest/tungstenite/protocol/struct.WebSocketConfig.html)
for `max_frame_size` and `max_message_size`.
- **Node SDK** ([ws](https://github.com/websockets/ws)). See the
[`maxPayload`](https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback)
option.
- **Python SDK** ([websockets](https://websockets.readthedocs.io/)). See the
[`max_size`](https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html) option.
- **Rust SDK** ([tokio-tungstenite](https://docs.rs/tokio-tungstenite)) See
[`WebSocketConfig`](https://docs.rs/tungstenite/latest/tungstenite/protocol/struct.WebSocketConfig.html),
same as the engine.

These are library defaults and can shift between dependency versions; iii doesn't publish a hard
guaranteed cap. 16 MB is a safe default to follow.

## Using channels

A channel is created by one worker and has two local stream ends: `writer` and `reader`; plus two
serializable refs (`writerRef` / `readerRef`) that can be handed to another function. The
subsections below cover the local-end API: creating a channel, writing bytes into its `writer`, and
reading bytes from its `reader`.

### Create a channel

`worker.createChannel()` returns a channel with two local stream objects and two serializable refs:
`writer` and `reader` are the local stream ends, and `writerRef` / `readerRef` are the tokens you
pass to another function so it can read or write the other end.

<Tabs>
<Tab title="Node / TypeScript">
```typescript
const channel = await worker.createChannel();

// channel.writer
// channel.reader
// channel.writerRef
// channel.readerRef
```

</Tab>
<Tab title="Python">
```python
channel = iii_client.create_channel()

# channel.writer
# channel.reader
# channel.writer_ref
# channel.reader_ref
```

</Tab>
<Tab title="Rust">
```rust
let channel = worker.create_channel(None).await?;

// channel.writer
// channel.reader
// channel.writer_ref
// channel.reader_ref
```

</Tab>
</Tabs>

### Write to a channel

Write the payload to the local `writer` and close it when finished. The bytes flow through the
engine to whichever worker holds the matching `reader`.

<Tabs>
<Tab title="Node / TypeScript">
```typescript
const channel = await worker.createChannel();

channel.writer.stream.end(Buffer.from("file contents"));
```

</Tab>
<Tab title="Python">
```python
channel = await iii_client.create_channel_async()

await channel.writer.write(b"file contents")
await channel.writer.close_async()
```

</Tab>
<Tab title="Rust">
```rust
let channel = worker.create_channel(None).await?;

channel.writer.write(b"file contents").await?;
channel.writer.close().await?;
```

</Tab>
</Tabs>

### Read from a channel

Read the local `reader` until the other end closes. The bytes arrive in the order they were written
by whichever worker holds the matching `writer`.

<Tabs>
<Tab title="Node / TypeScript">
```typescript
const channel = await worker.createChannel();

let bytes = 0;
for await (const chunk of channel.reader.stream) {
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
}
```

</Tab>
<Tab title="Python">
```python
channel = await iii_client.create_channel_async()

bytes_total = 0
async for chunk in channel.reader:
bytes_total += len(chunk)
```

</Tab>
<Tab title="Rust">
```rust
let channel = worker.create_channel(None).await?;

let mut bytes = 0;
while let Some(chunk) = channel.reader.next_binary().await? {
bytes += chunk.len();
}
```

</Tab>
</Tabs>

## Using channels across functions

A channel only becomes useful once both ends are owned by different code paths. Typically one
function that holds the local `writer` / `reader` and another that receives the matching ref in its
payload. The two subsections below cover that handoff: first how to send a ref alongside a normal
trigger call, then how the receiving function turns it back into a live stream to read or write.

### Pass a channel ref to another function

Pass the `readerRef` (or `writerRef`) as part of a normal function invocation. The receiving
function uses the ref to read from (or write to) the channel.

<Tabs>
<Tab title="Node / TypeScript">
```typescript
const result = await worker.trigger({
function_id: "files::process",
payload: {
filename: "report.csv",
reader: channel.readerRef,
},
});
```
</Tab>
<Tab title="Python">
```python
result = await iii_client.trigger_async({
"function_id": "files::process",
"payload": {
"filename": "report.csv",
"reader": channel.reader_ref.model_dump(),
},
})
```
</Tab>
<Tab title="Rust">
```rust
use iii_sdk::TriggerRequest;
use serde_json::json;

let result = worker
.trigger(TriggerRequest {
function_id: "files::process".to_string(),
payload: json!({
"filename": "report.csv",
"reader": channel.reader_ref,
}),
action: None,
timeout_ms: None,
})
.await?;
```

</Tab>
</Tabs>

Node and Python deserialize incoming channel refs into live `ChannelReader` / `ChannelWriter`
objects before the handler runs, so the ref arrives ready to iterate or write to. Rust receives the
ref in JSON and reconstructs the reader or writer explicitly with `ChannelReader::new(...)` or
`ChannelWriter::new(...)`.

### Read from a channel ref

<Tabs>
<Tab title="Node / TypeScript">
```typescript
import type { ChannelReader } from "iii-sdk";

worker.registerFunction("files::process", async (input: { reader: ChannelReader }) => {
let bytes = 0;
for await (const chunk of input.reader.stream) {
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
}
return { bytes };
});
```

</Tab>
<Tab title="Python">
```python
async def process_file(input: dict) -> dict:
reader = input["reader"]
total = 0
async for chunk in reader:
total += len(chunk)
return {"bytes": total}

worker.register_function("files::process", process_file)
```

</Tab>
<Tab title="Rust">
```rust
use iii_sdk::{ChannelDirection, ChannelReader, IIIError};
use serde_json::json;

let refs = iii_sdk::extract_channel_refs(&input);
let (_, reader_ref) = refs
.iter()
.find(|(k, r)| k == "reader" && matches!(r.direction, ChannelDirection::Read))
.ok_or_else(|| IIIError::Handler("missing reader channel ref".into()))?;

let reader = ChannelReader::new(worker.address(), reader_ref);
let mut bytes = 0;
while let Some(chunk) = reader.next_binary().await? {
bytes += chunk.len();
}
Ok(json!({ "bytes": bytes }))
```

</Tab>
</Tabs>

### Write to a channel ref

<Tabs>
<Tab title="Node / TypeScript">
```typescript
import type { ChannelWriter } from "iii-sdk";

worker.registerFunction("files::generate", async (input: { writer: ChannelWriter }) => {
input.writer.stream.write(Buffer.from("hello "));
input.writer.stream.end(Buffer.from("world"));
return { ok: true };
});
```

</Tab>
<Tab title="Python">
```python
async def generate_file(input: dict) -> dict:
writer = input["writer"]
await writer.write(b"hello ")
await writer.write(b"world")
await writer.close_async()
return {"ok": True}

worker.register_function("files::generate", generate_file)
```

</Tab>
<Tab title="Rust">
```rust
use iii_sdk::{ChannelDirection, ChannelWriter, IIIError};
use serde_json::json;

let refs = iii_sdk::extract_channel_refs(&input);
let (_, writer_ref) = refs
.iter()
.find(|(k, r)| k == "writer" && matches!(r.direction, ChannelDirection::Write))
.ok_or_else(|| IIIError::Handler("missing writer channel ref".into()))?;

let writer = ChannelWriter::new(worker.address(), writer_ref);
writer.write(b"hello ").await?;
writer.write(b"world").await?;
writer.close().await?;
Ok(json!({ "ok": true }))
```

</Tab>
</Tabs>

<Note>
For the per-language channel API surface, see the SDK reference:
[Node](/sdk-reference/node-sdk#channels), [Python](/sdk-reference/python-sdk#channels), and
[Rust](/sdk-reference/rust-sdk#channels).
</Note>
Loading
Loading