diff --git a/.skill-check.yaml b/.skill-check.yaml
index 64b2e8244..88134fa6a 100644
--- a/.skill-check.yaml
+++ b/.skill-check.yaml
@@ -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
diff --git a/docs/creating-workers/channels.mdx b/docs/creating-workers/channels.mdx
new file mode 100644
index 000000000..510e7e5b5
--- /dev/null
+++ b/docs/creating-workers/channels.mdx
@@ -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.
+
+
+ For the underlying model (how channels are addressed, multiplexed, and torn down), see [Channels
+ architecture](/understanding-iii/channels).
+
+
+## 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.
+
+
+
+ ```typescript
+ const channel = await worker.createChannel();
+
+ // channel.writer
+ // channel.reader
+ // channel.writerRef
+ // channel.readerRef
+ ```
+
+
+
+ ```python
+ channel = iii_client.create_channel()
+
+ # channel.writer
+ # channel.reader
+ # channel.writer_ref
+ # channel.reader_ref
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ // channel.writer
+ // channel.reader
+ // channel.writer_ref
+ // channel.reader_ref
+ ```
+
+
+
+
+### 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`.
+
+
+
+ ```typescript
+ const channel = await worker.createChannel();
+
+ channel.writer.stream.end(Buffer.from("file contents"));
+ ```
+
+
+
+ ```python
+ channel = await iii_client.create_channel_async()
+
+ await channel.writer.write(b"file contents")
+ await channel.writer.close_async()
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ channel.writer.write(b"file contents").await?;
+ channel.writer.close().await?;
+ ```
+
+
+
+
+### 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`.
+
+
+
+ ```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);
+ }
+ ```
+
+
+
+ ```python
+ channel = await iii_client.create_channel_async()
+
+ bytes_total = 0
+ async for chunk in channel.reader:
+ bytes_total += len(chunk)
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ let mut bytes = 0;
+ while let Some(chunk) = channel.reader.next_binary().await? {
+ bytes += chunk.len();
+ }
+ ```
+
+
+
+
+## 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.
+
+
+
+ ```typescript
+ const result = await worker.trigger({
+ function_id: "files::process",
+ payload: {
+ filename: "report.csv",
+ reader: channel.readerRef,
+ },
+ });
+ ```
+
+
+ ```python
+ result = await iii_client.trigger_async({
+ "function_id": "files::process",
+ "payload": {
+ "filename": "report.csv",
+ "reader": channel.reader_ref.model_dump(),
+ },
+ })
+ ```
+
+
+ ```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?;
+ ```
+
+
+
+
+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
+
+
+
+ ```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 };
+ });
+ ```
+
+
+
+ ```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)
+ ```
+
+
+
+ ```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 }))
+ ```
+
+
+
+
+### Write to a channel ref
+
+
+
+ ```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 };
+ });
+ ```
+
+
+
+ ```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)
+ ```
+
+
+
+ ```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 }))
+ ```
+
+
+
+
+
+ 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).
+
diff --git a/docs/creating-workers/channels.mdx.skill.md b/docs/creating-workers/channels.mdx.skill.md
new file mode 100644
index 000000000..60654dc23
--- /dev/null
+++ b/docs/creating-workers/channels.mdx.skill.md
@@ -0,0 +1,328 @@
+
+
+
+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.
+
+
+ For the underlying model (how channels are addressed, multiplexed, and torn down), see [Channels
+ architecture](/understanding-iii/channels).
+
+
+## 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.
+
+
+
+ ```typescript
+ const channel = await worker.createChannel();
+
+ // channel.writer
+ // channel.reader
+ // channel.writerRef
+ // channel.readerRef
+ ```
+
+
+
+ ```python
+ channel = iii_client.create_channel()
+
+ # channel.writer
+ # channel.reader
+ # channel.writer_ref
+ # channel.reader_ref
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ // channel.writer
+ // channel.reader
+ // channel.writer_ref
+ // channel.reader_ref
+ ```
+
+
+
+
+### 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`.
+
+
+
+ ```typescript
+ const channel = await worker.createChannel();
+
+ channel.writer.stream.end(Buffer.from("file contents"));
+ ```
+
+
+
+ ```python
+ channel = await iii_client.create_channel_async()
+
+ await channel.writer.write(b"file contents")
+ await channel.writer.close_async()
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ channel.writer.write(b"file contents").await?;
+ channel.writer.close().await?;
+ ```
+
+
+
+
+### 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`.
+
+
+
+ ```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);
+ }
+ ```
+
+
+
+ ```python
+ channel = await iii_client.create_channel_async()
+
+ bytes_total = 0
+ async for chunk in channel.reader:
+ bytes_total += len(chunk)
+ ```
+
+
+
+ ```rust
+ let channel = worker.create_channel(None).await?;
+
+ let mut bytes = 0;
+ while let Some(chunk) = channel.reader.next_binary().await? {
+ bytes += chunk.len();
+ }
+ ```
+
+
+
+
+## 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.
+
+
+
+ ```typescript
+ const result = await worker.trigger({
+ function_id: "files::process",
+ payload: {
+ filename: "report.csv",
+ reader: channel.readerRef,
+ },
+ });
+ ```
+
+
+ ```python
+ result = await iii_client.trigger_async({
+ "function_id": "files::process",
+ "payload": {
+ "filename": "report.csv",
+ "reader": channel.reader_ref.model_dump(),
+ },
+ })
+ ```
+
+
+ ```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?;
+ ```
+
+
+
+
+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
+
+
+
+ ```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 };
+ });
+ ```
+
+
+
+ ```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)
+ ```
+
+
+
+ ```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 }))
+ ```
+
+
+
+
+### Write to a channel ref
+
+
+
+ ```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 };
+ });
+ ```
+
+
+
+ ```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)
+ ```
+
+
+
+ ```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 }))
+ ```
+
+
+
+
+
+ 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).
+
diff --git a/docs/creating-workers/functions.mdx b/docs/creating-workers/functions.mdx
index 9605fb99c..fd02f317b 100644
--- a/docs/creating-workers/functions.mdx
+++ b/docs/creating-workers/functions.mdx
@@ -7,31 +7,41 @@ type: "how-to"
## What "writing a function" means
-A worker contributes capability by registering functions. Each function has an `id` of the form
-`service::name`, a handler that receives the payload and returns a result, and optional JSON
-Schemas that describe the request and response shape.
+A worker contributes capabilities to a iii system by registering functions. Each function has an
+`id` of the form `service::name`, a handler that receives the payload and returns a result, and
+optional JSON Schemas that describe the request and response shape.
For how callers invoke functions (`worker.trigger` / `iii trigger` / event-bound triggers), see
-[Using iii / Functions](/using-iii/functions) and
-[Using iii / Triggers](/using-iii/triggers). This page is about the authoring surface.
+[Using iii / Triggers](/using-iii/triggers). This page is about the authoring new functions.
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
## Register a function
-Inside the worker, register the function with the SDK. The `id` is what callers pass as
-`function_id`; the handler signature is the same regardless of how the invocation arrived (direct
-call, HTTP trigger, cron, queue message).
+Inside the worker, register the function with the SDK. The `id` is what triggers will use as a
+`function_id`.
+
+
+ Each type of trigger has its own expected argument structure for a given function. For example
+ `cron` will call functions without arguments, while `http` will provide a standard http-style
+ payload that includes `body`, `headers`, and other properties. For each worker visit their
+ respective page at [workers.iii.dev](https://workers.iii.dev/) for their expected payload.
+
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
return { c: payload.a + payload.b };
});
```
+
```python
@@ -48,6 +58,7 @@ call, HTTP trigger, cron, queue message).
worker.register_function("math::add", add_handler)
```
+
```rust
@@ -58,8 +69,9 @@ call, HTTP trigger, cron, queue message).
worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
Ok(serde_json::json!({ "c": input.a + input.b }))
- }))?;
+ }));
```
+
@@ -71,26 +83,32 @@ agent-readable skills.
Runtime validation is not yet supported. Attached schemas are metadata only; the engine does not
- reject payloads or handler return values that don't match them.
+ enforce a specific schema, reject payloads, nor handler return values that don't match the
+ schemas. Treat the schemas as contract documentation for function invocations, agents, and the
+ console.
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction(
"math::add",
async (payload) => ({ c: payload.a + payload.b }),
{
- request_schema: {
+ request_format: {
type: "object",
properties: { a: { type: "number" }, b: { type: "number" } },
required: ["a", "b"],
},
- response_schema: {
+ response_format: {
type: "object",
properties: { c: { type: "number" } },
required: ["c"],
@@ -98,6 +116,7 @@ agent-readable skills.
},
);
```
+
```python
@@ -112,61 +131,185 @@ agent-readable skills.
worker.register_function(
"math::add",
add_handler,
- request_schema={
+ request_format={
"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
"required": ["a", "b"],
},
- response_schema={
+ response_format={
"type": "object",
"properties": {"c": {"type": "number"}},
"required": ["c"],
},
)
```
+
```rust
use iii_sdk::{InitOptions, RegisterFunction, register_worker};
- use serde_json::json;
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+
+ #[derive(Deserialize, JsonSchema)]
+ struct AddInput { a: f64, b: f64 }
+
+ #[derive(serde::Serialize, JsonSchema)]
+ struct AddOutput { c: f64 }
let url = std::env::var("III_URL").expect("III_URL must be set");
let worker = register_worker(&url, InitOptions::default());
- worker.register_function(
- RegisterFunction::new("math::add", |input: AddInput| {
- Ok(serde_json::json!({ "c": input.a + input.b }))
- })
- .request_schema(json!({
- "type": "object",
- "properties": { "a": { "type": "number" }, "b": { "type": "number" } },
- "required": ["a", "b"],
- }))
- .response_schema(json!({
- "type": "object",
- "properties": { "c": { "type": "number" } },
- "required": ["c"],
- })),
- )?;
+ // Rust derives `request_format` and `response_format` from the closure's
+ // input and output types via `schemars::JsonSchema`. No builder methods
+ // are needed — annotate your request/response structs and the SDK
+ // generates the schemas automatically.
+ worker.register_function(RegisterFunction::new(
+ "math::add",
+ |input: AddInput| -> Result {
+ Ok(AddOutput { c: input.a + input.b })
+ },
+ ));
```
+
The schemas also feed the iii console and the agent-readable skills.
+## HTTP-invokable functions
+
+You can also register an external HTTP endpoint as a function. The engine makes the HTTP call
+whenever the function is invoked, your worker only declares the endpoint.
+
+This is useful for delegating work to your existing API Gateways, webhooks, serverless platforms
+(Lambda, Azure Functions, Google Cloud Functions), or any third-party API you want to surface as a
+regular iii function.
+
+The function is then triggerable like any other: `worker.trigger`, `iii trigger`, and any bound
+trigger type (queue, cron, state, http) all work without any other changes.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
+
+ worker.registerFunction(
+ "notifications::send",
+ {
+ url: "https://hooks.provider.example.com/notify",
+ method: "POST",
+ timeout_ms: 5000,
+ headers: { "X-Service": "iii-worker" },
+ auth: { type: "bearer", token_key: "PROVIDER_API_TOKEN" },
+ },
+ { description: "POST a notification to the provider webhook" },
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import HttpInvocationConfig, InitOptions, register_worker
+ from iii.iii_types import HttpAuthBearer
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="notifications-worker"),
+ )
+
+ worker.register_function(
+ "notifications::send",
+ HttpInvocationConfig(
+ url="https://hooks.provider.example.com/notify",
+ method="POST",
+ timeout_ms=5000,
+ headers={"X-Service": "iii-worker"},
+ auth=HttpAuthBearer(token_key="PROVIDER_API_TOKEN"),
+ ),
+ description="POST a notification to the provider webhook",
+ )
+ ```
+
+
+ ```rust
+ use std::collections::HashMap;
+ use iii_sdk::{
+ HttpAuthConfig, HttpInvocationConfig, HttpMethod, InitOptions,
+ RegisterFunctionMessage, register_worker,
+ };
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ let mut headers = HashMap::new();
+ headers.insert("X-Service".into(), "iii-worker".into());
+
+ worker.register_function((
+ RegisterFunctionMessage::with_id("notifications::send".into())
+ .with_description("POST a notification to the provider webhook".into()),
+ HttpInvocationConfig {
+ url: "https://hooks.provider.example.com/notify".into(),
+ method: HttpMethod::Post,
+ timeout_ms: Some(5000),
+ headers,
+ auth: Some(HttpAuthConfig::Bearer {
+ token_key: "PROVIDER_API_TOKEN".into(),
+ }),
+ },
+ ));
+ ```
+
+
+
+
+
+### `HttpInvocationConfig` fields
+
+While a normal function takes an `id` and a `handler`, http invokeable functions take an `id` and a
+`HttpInvocationConfig`.
+
+| Field | Type | Default | Description |
+| ------------ | ------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- |
+| `url` | `string` | (required) | Endpoint the engine calls when the function is invoked. |
+| `method` | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"` | `"POST"` | HTTP method. |
+| `timeout_ms` | `number` | `30000` | Per-request timeout in milliseconds. |
+| `headers` | `Record` | `{}` | Headers added to every invocation. |
+| `auth` | `HttpAuthConfig` | (none) | An object with two fields: `bearer`, `hmac`, or `api_key` AND `token_key`, `secret_key`, or `value_key` |
+
+
+ Auth fields (`token_key`, `secret_key`, `value_key`) are specified as the **names of environment
+ variables**, not the secrets themselves. The engine resolves them from its own process environment
+ at registration time, so secrets stay on the engine host and never travel over the SDK WebSocket.
+
+
+### HTTP error handling
+
+The engine sends the invocation payload as the JSON request body and treats any non-2xx response or
+network error as an invocation failure that propagates back to the caller. HTTP-invoked functions
+appear in [`engine::functions::list`](/using-iii/functions#engine-functions-engine) and are
+discoverable through the console exactly like in-process handlers.
+
## Return values and errors
A function returns either a value (which the handler is responsible for shaping to match its
-documented response schema) or an error. Errors raised inside the handler are propagated to the
-caller as invocation errors with the worker's stack trace; the engine doesn't swallow them. Use
-this distinction to express expected failures (return a structured error value) versus unexpected
-ones (throw / raise / return `Err`).
+documented [response schema](#attach-request-and-response-schemas)) or an error. Errors raised
+inside the handler are propagated to the caller as invocation errors with the worker's stack trace
+attached: Node forwards `error.stack`, Python forwards `traceback.format_exc()`, and Rust forwards
+the underlying error's stack trace. The engine doesn't swallow them. Use this distinction to express
+expected failures (return a structured error value) versus unexpected ones (throw / raise / return
+`Err`).
## Unregister a function
-`registerFunction` returns a handle with an `unregister()` method that removes the function from
-the engine at runtime. When the worker disconnects, all of its functions are removed automatically
-and pending invocations error out.
+`registerFunction` returns a handle with an `unregister()` method that removes the function from the
+engine at runtime. When the worker disconnects, all of its functions are removed automatically and
+pending invocations error out.
@@ -177,6 +320,7 @@ and pending invocations error out.
add.unregister();
```
+
```python
@@ -184,6 +328,7 @@ and pending invocations error out.
add.unregister()
```
+
```rust
@@ -193,12 +338,6 @@ and pending invocations error out.
add.unregister();
```
+
-
-## What goes in Worker Docs
-
-The function ids your worker exposes, what each one does, and any worker-specific semantics
-(idempotency, rate limits, side effects) belong in this worker's Worker Docs. Keep iii-level
-concepts (the registration surface, schema metadata, error propagation) here; document the
-per-function specifics there.
diff --git a/docs/creating-workers/functions.mdx.skill.md b/docs/creating-workers/functions.mdx.skill.md
index f9ff86224..2af482657 100644
--- a/docs/creating-workers/functions.mdx.skill.md
+++ b/docs/creating-workers/functions.mdx.skill.md
@@ -5,31 +5,41 @@
## What "writing a function" means
-A worker contributes capability by registering functions. Each function has an `id` of the form
-`service::name`, a handler that receives the payload and returns a result, and optional JSON
-Schemas that describe the request and response shape.
+A worker contributes capabilities to a iii system by registering functions. Each function has an
+`id` of the form `service::name`, a handler that receives the payload and returns a result, and
+optional JSON Schemas that describe the request and response shape.
For how callers invoke functions (`worker.trigger` / `iii trigger` / event-bound triggers), see
-[Using iii / Functions](/using-iii/functions) and
-[Using iii / Triggers](/using-iii/triggers). This page is about the authoring surface.
+[Using iii / Triggers](/using-iii/triggers). This page is about the authoring new functions.
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
## Register a function
-Inside the worker, register the function with the SDK. The `id` is what callers pass as
-`function_id`; the handler signature is the same regardless of how the invocation arrived (direct
-call, HTTP trigger, cron, queue message).
+Inside the worker, register the function with the SDK. The `id` is what triggers will use as a
+`function_id`.
+
+
+ Each type of trigger has its own expected argument structure for a given function. For example
+ `cron` will call functions without arguments, while `http` will provide a standard http-style
+ payload that includes `body`, `headers`, and other properties. For each worker visit their
+ respective page at [workers.iii.dev](https://workers.iii.dev/) for their expected payload.
+
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
return { c: payload.a + payload.b };
});
```
+
```python
@@ -46,6 +56,7 @@ call, HTTP trigger, cron, queue message).
worker.register_function("math::add", add_handler)
```
+
```rust
@@ -56,8 +67,9 @@ call, HTTP trigger, cron, queue message).
worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
Ok(serde_json::json!({ "c": input.a + input.b }))
- }))?;
+ }));
```
+
@@ -69,26 +81,32 @@ agent-readable skills.
Runtime validation is not yet supported. Attached schemas are metadata only; the engine does not
- reject payloads or handler return values that don't match them.
+ enforce a specific schema, reject payloads, nor handler return values that don't match the
+ schemas. Treat the schemas as contract documentation for function invocations, agents, and the
+ console.
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction(
"math::add",
async (payload) => ({ c: payload.a + payload.b }),
{
- request_schema: {
+ request_format: {
type: "object",
properties: { a: { type: "number" }, b: { type: "number" } },
required: ["a", "b"],
},
- response_schema: {
+ response_format: {
type: "object",
properties: { c: { type: "number" } },
required: ["c"],
@@ -96,6 +114,7 @@ agent-readable skills.
},
);
```
+
```python
@@ -110,61 +129,185 @@ agent-readable skills.
worker.register_function(
"math::add",
add_handler,
- request_schema={
+ request_format={
"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
"required": ["a", "b"],
},
- response_schema={
+ response_format={
"type": "object",
"properties": {"c": {"type": "number"}},
"required": ["c"],
},
)
```
+
```rust
use iii_sdk::{InitOptions, RegisterFunction, register_worker};
- use serde_json::json;
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+
+ #[derive(Deserialize, JsonSchema)]
+ struct AddInput { a: f64, b: f64 }
+
+ #[derive(serde::Serialize, JsonSchema)]
+ struct AddOutput { c: f64 }
let url = std::env::var("III_URL").expect("III_URL must be set");
let worker = register_worker(&url, InitOptions::default());
- worker.register_function(
- RegisterFunction::new("math::add", |input: AddInput| {
- Ok(serde_json::json!({ "c": input.a + input.b }))
- })
- .request_schema(json!({
- "type": "object",
- "properties": { "a": { "type": "number" }, "b": { "type": "number" } },
- "required": ["a", "b"],
- }))
- .response_schema(json!({
- "type": "object",
- "properties": { "c": { "type": "number" } },
- "required": ["c"],
- })),
- )?;
+ // Rust derives `request_format` and `response_format` from the closure's
+ // input and output types via `schemars::JsonSchema`. No builder methods
+ // are needed — annotate your request/response structs and the SDK
+ // generates the schemas automatically.
+ worker.register_function(RegisterFunction::new(
+ "math::add",
+ |input: AddInput| -> Result {
+ Ok(AddOutput { c: input.a + input.b })
+ },
+ ));
```
+
The schemas also feed the iii console and the agent-readable skills.
+## HTTP-invokable functions
+
+You can also register an external HTTP endpoint as a function. The engine makes the HTTP call
+whenever the function is invoked, your worker only declares the endpoint.
+
+This is useful for delegating work to your existing API Gateways, webhooks, serverless platforms
+(Lambda, Azure Functions, Google Cloud Functions), or any third-party API you want to surface as a
+regular iii function.
+
+The function is then triggerable like any other: `worker.trigger`, `iii trigger`, and any bound
+trigger type (queue, cron, state, http) all work without any other changes.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
+
+ worker.registerFunction(
+ "notifications::send",
+ {
+ url: "https://hooks.provider.example.com/notify",
+ method: "POST",
+ timeout_ms: 5000,
+ headers: { "X-Service": "iii-worker" },
+ auth: { type: "bearer", token_key: "PROVIDER_API_TOKEN" },
+ },
+ { description: "POST a notification to the provider webhook" },
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import HttpInvocationConfig, InitOptions, register_worker
+ from iii.iii_types import HttpAuthBearer
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="notifications-worker"),
+ )
+
+ worker.register_function(
+ "notifications::send",
+ HttpInvocationConfig(
+ url="https://hooks.provider.example.com/notify",
+ method="POST",
+ timeout_ms=5000,
+ headers={"X-Service": "iii-worker"},
+ auth=HttpAuthBearer(token_key="PROVIDER_API_TOKEN"),
+ ),
+ description="POST a notification to the provider webhook",
+ )
+ ```
+
+
+ ```rust
+ use std::collections::HashMap;
+ use iii_sdk::{
+ HttpAuthConfig, HttpInvocationConfig, HttpMethod, InitOptions,
+ RegisterFunctionMessage, register_worker,
+ };
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ let mut headers = HashMap::new();
+ headers.insert("X-Service".into(), "iii-worker".into());
+
+ worker.register_function((
+ RegisterFunctionMessage::with_id("notifications::send".into())
+ .with_description("POST a notification to the provider webhook".into()),
+ HttpInvocationConfig {
+ url: "https://hooks.provider.example.com/notify".into(),
+ method: HttpMethod::Post,
+ timeout_ms: Some(5000),
+ headers,
+ auth: Some(HttpAuthConfig::Bearer {
+ token_key: "PROVIDER_API_TOKEN".into(),
+ }),
+ },
+ ));
+ ```
+
+
+
+
+
+### `HttpInvocationConfig` fields
+
+While a normal function takes an `id` and a `handler`, http invokeable functions take an `id` and a
+`HttpInvocationConfig`.
+
+| Field | Type | Default | Description |
+| ------------ | ------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- |
+| `url` | `string` | (required) | Endpoint the engine calls when the function is invoked. |
+| `method` | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"` | `"POST"` | HTTP method. |
+| `timeout_ms` | `number` | `30000` | Per-request timeout in milliseconds. |
+| `headers` | `Record` | `{}` | Headers added to every invocation. |
+| `auth` | `HttpAuthConfig` | (none) | An object with two fields: `bearer`, `hmac`, or `api_key` AND `token_key`, `secret_key`, or `value_key` |
+
+
+ Auth fields (`token_key`, `secret_key`, `value_key`) are specified as the **names of environment
+ variables**, not the secrets themselves. The engine resolves them from its own process environment
+ at registration time, so secrets stay on the engine host and never travel over the SDK WebSocket.
+
+
+### HTTP error handling
+
+The engine sends the invocation payload as the JSON request body and treats any non-2xx response or
+network error as an invocation failure that propagates back to the caller. HTTP-invoked functions
+appear in [`engine::functions::list`](/using-iii/functions#engine-functions-engine) and are
+discoverable through the console exactly like in-process handlers.
+
## Return values and errors
A function returns either a value (which the handler is responsible for shaping to match its
-documented response schema) or an error. Errors raised inside the handler are propagated to the
-caller as invocation errors with the worker's stack trace; the engine doesn't swallow them. Use
-this distinction to express expected failures (return a structured error value) versus unexpected
-ones (throw / raise / return `Err`).
+documented [response schema](#attach-request-and-response-schemas)) or an error. Errors raised
+inside the handler are propagated to the caller as invocation errors with the worker's stack trace
+attached: Node forwards `error.stack`, Python forwards `traceback.format_exc()`, and Rust forwards
+the underlying error's stack trace. The engine doesn't swallow them. Use this distinction to express
+expected failures (return a structured error value) versus unexpected ones (throw / raise / return
+`Err`).
## Unregister a function
-`registerFunction` returns a handle with an `unregister()` method that removes the function from
-the engine at runtime. When the worker disconnects, all of its functions are removed automatically
-and pending invocations error out.
+`registerFunction` returns a handle with an `unregister()` method that removes the function from the
+engine at runtime. When the worker disconnects, all of its functions are removed automatically and
+pending invocations error out.
@@ -175,6 +318,7 @@ and pending invocations error out.
add.unregister();
```
+
```python
@@ -182,6 +326,7 @@ and pending invocations error out.
add.unregister()
```
+
```rust
@@ -191,12 +336,6 @@ and pending invocations error out.
add.unregister();
```
+
-
-## What goes in Worker Docs
-
-The function ids your worker exposes, what each one does, and any worker-specific semantics
-(idempotency, rate limits, side effects) belong in this worker's Worker Docs. Keep iii-level
-concepts (the registration surface, schema metadata, error propagation) here; document the
-per-function specifics there.
diff --git a/docs/creating-workers/index.mdx b/docs/creating-workers/index.mdx
new file mode 100644
index 000000000..5e76ae01f
--- /dev/null
+++ b/docs/creating-workers/index.mdx
@@ -0,0 +1,41 @@
+---
+title: "Overview"
+description:
+ "Every new capability in iii is added by writing a Worker, never by extending the Engine."
+owner: "devrel"
+type: "explanation"
+---
+
+Every new capability in a iii system is added by creating a Worker. A queue, a scheduler, an HTTP
+edge, a browser tab, an agent, a CRM integration, or a sandbox. Each is a Worker that connects to
+the Engine and registers Triggers and Functions. The Engine itself never needs to change.
+
+## Workers are the services
+
+A Worker is the unit of capability in iii. It is the service. When you need new behaviour, you write
+a new Worker (or install an existing one from the registry); you do not patch the Engine, add a
+plugin to it, or fork it. The Engine is a fixed coordinator, it routes invocations between Workers
+and maintains the live registry of what each Worker provides. Everything that makes a iii system do
+something useful runs in a Worker.
+
+This is why "add a Worker" is the answer to almost every "how do I add X to iii?" question. If a
+capability is missing, the gap is filled by a Worker, not by a change to iii itself.
+
+
+ For the mental model behind Workers, Triggers, Functions, and the Engine, see [Understanding iii /
+ Overview](/understanding-iii). To build Workers continue with this section.
+
+
+## What's in this section
+
+
+
+ Connect a Worker to the Engine and deploy it.
+
+
+ Declare what causes those Functions to run.
+
+
+ Register the Functions a Worker contributes.
+
+
diff --git a/docs/creating-workers/index.mdx.skill.md b/docs/creating-workers/index.mdx.skill.md
new file mode 100644
index 000000000..fe88fe708
--- /dev/null
+++ b/docs/creating-workers/index.mdx.skill.md
@@ -0,0 +1,38 @@
+
+
+# Overview
+
+
+Every new capability in a iii system is added by creating a Worker. A queue, a scheduler, an HTTP
+edge, a browser tab, an agent, a CRM integration, or a sandbox. Each is a Worker that connects to
+the Engine and registers Triggers and Functions. The Engine itself never needs to change.
+
+## Workers are the services
+
+A Worker is the unit of capability in iii. It is the service. When you need new behaviour, you write
+a new Worker (or install an existing one from the registry); you do not patch the Engine, add a
+plugin to it, or fork it. The Engine is a fixed coordinator, it routes invocations between Workers
+and maintains the live registry of what each Worker provides. Everything that makes a iii system do
+something useful runs in a Worker.
+
+This is why "add a Worker" is the answer to almost every "how do I add X to iii?" question. If a
+capability is missing, the gap is filled by a Worker, not by a change to iii itself.
+
+
+ For the mental model behind Workers, Triggers, Functions, and the Engine, see [Understanding iii /
+ Overview](/understanding-iii). To build Workers continue with this section.
+
+
+## What's in this section
+
+
+
+ Connect a Worker to the Engine and deploy it.
+
+
+ Declare what causes those Functions to run.
+
+
+ Register the Functions a Worker contributes.
+
+
diff --git a/docs/creating-workers/triggers.mdx b/docs/creating-workers/triggers.mdx
index 5f35f384c..d072574c1 100644
--- a/docs/creating-workers/triggers.mdx
+++ b/docs/creating-workers/triggers.mdx
@@ -1,201 +1,453 @@
---
title: "Triggers"
-description: "Author new trigger types so other workers' functions can run on the events your worker emits."
+description:
+ "Author new trigger types so other workers' functions can run on the events your worker emits."
owner: "devrel"
type: "how-to"
---
## What "writing a trigger" means
-Workers don't only call functions; they can also _source_ events. When a worker advertises a
-trigger type (e.g. `webhook`, a custom schedule, or any external event source you implement), any
-other worker can bind its functions to that type. The engine routes binding requests to your
-trigger type's handler, and your worker invokes the bound functions when the underlying event
-fires.
+A worker uses triggers two ways. Most of the time, you bind the worker's functions by registering a
+trigger on an existing trigger type such as: [`http`](https://workers.iii.dev/workers/iii-http),
+[`cron`](https://workers.iii.dev/workers/iii-cron),
+[queue messages](https://workers.iii.dev/workers/iii-queue),
+[`state` changes](https://workers.iii.dev/workers/iii-state), and any other event source in the
+system. Less often, you register a new trigger type from your worker so other workers can bind their
+functions to events your worker emits.
-This page is about authoring new trigger types from inside your worker. For invoking functions and
-binding triggers to functions from the consumer side, see [Using iii / Triggers](/using-iii/triggers).
+This page primarily covers the latter: making your own triggers, if you want to use existing
+triggers in new workers then refer to [Using iii / Triggers](/using-iii/triggers).
-## Declare a trigger type
+
+ For the caller-side mechanics (direct invocation with `worker.trigger` or `iii trigger`, the
+ `TriggerAction` variants, gating with conditions, multiple bindings per function), see [Using iii
+ / Triggers](/using-iii/triggers).
+
-Inside the worker, register the trigger type once during startup. You pass an `id` and
-`description` plus a handler with `registerTrigger` / `unregisterTrigger` callbacks. The engine
-calls these whenever a consumer binds or unbinds a function against your trigger type, so your
-worker can keep its own table of `{ trigger id → function id, config }` bindings.
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
-`registerTriggerType` returns a typed handle. Use it later to bind functions you own to this
-trigger type, or to tear the type down.
+## Bind a function to an existing trigger type
+
+Most workers consume trigger types that other workers already publish: `http` from
+[iii-http](https://workers.iii.dev/workers/iii-http) to expose a function as an endpoint, `cron`
+from [iii-cron](https://workers.iii.dev/workers/iii-cron) to run a function on a schedule, queue
+triggers from [iii-queue](https://workers.iii.dev/workers/iii-queue) to fire a function on each
+message, `state` from [iii-state](https://workers.iii.dev/workers/iii-state) to react to data
+changes. Bind one of your worker's functions to a trigger type with
+`worker.registerTrigger({ type, function_id, config })`. The worker that publishes the trigger type
+must be connected when you register; otherwise the registration fails.
```typescript
- import { registerWorker } from "iii-sdk";
- import type { TriggerConfig, TriggerHandler } from "iii-sdk";
-
- const worker = registerWorker(process.env.III_URL);
-
- type WebhookTriggerConfig = {
- path: string;
- secret?: string;
- };
-
- const webhookHandler: TriggerHandler = {
- async registerTrigger(config: TriggerConfig) {
- // Stash { config.id, config.function_id, config.config } so the
- // worker's HTTP listener knows which function to invoke when a
- // request matches `config.config.path`.
- },
- async unregisterTrigger(config: TriggerConfig) {
- // Remove the binding from the worker's table.
- },
- };
-
- const webhook = worker.registerTriggerType(
- { id: "webhook", description: "Incoming webhook trigger" },
- webhookHandler,
- );
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
```
```python
- import os
- from iii import (
- InitOptions,
- RegisterTriggerTypeInput,
- TriggerConfig,
- TriggerHandler,
- register_worker,
- )
-
- worker = register_worker(
- os.environ.get("III_URL"),
- InitOptions(worker_name="webhook-worker"),
- )
-
- class WebhookHandler(TriggerHandler):
- async def register_trigger(self, config: TriggerConfig) -> None:
- # Stash config.id, config.function_id, config.config
- ...
-
- async def unregister_trigger(self, config: TriggerConfig) -> None:
- ...
-
- webhook = worker.register_trigger_type(
- RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
- WebhookHandler(),
- )
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
```
```rust
- use iii_sdk::{
- InitOptions, RegisterTriggerType, TriggerConfig, TriggerHandler, register_worker,
- };
-
- let url = std::env::var("III_URL").expect("III_URL must be set");
- let worker = register_worker(&url, InitOptions::default());
-
- struct WebhookHandler;
-
- #[async_trait::async_trait]
- impl TriggerHandler for WebhookHandler {
- async fn register_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
- // Stash config.id, config.function_id, config.config
- Ok(())
- }
-
- async fn unregister_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
- Ok(())
- }
- }
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
- let webhook = worker.register_trigger_type(
- RegisterTriggerType::new("webhook", "Incoming webhook trigger", WebhookHandler),
- );
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
```
+
-For typed `config` and call-payload schemas, attach Pydantic, Zod, or `schemars::JsonSchema` types
-to the registration in your language of choice. See each SDK's reference for the exact builder.
+The `config` shape is defined per trigger type and documented in each publishing worker's
+[Worker Docs](https://workers.iii.dev).
-## Dispatch events to bound functions
+
+ Other binding mechanics are covered in [Using iii /
+ Triggers](/using-iii/triggers#register-a-trigger): the unregister handle, binding multiple
+ triggers to the same function, gating with `condition_function_id`, and the `TriggerAction`
+ variants (`Void`, `Enqueue`, etc.).
+
-There is no special "fire" API. When the underlying event source delivers something (an incoming
-HTTP request, a cron tick, a webhook hit), your worker looks up the bindings it stashed in its
-`registerTrigger` callback and invokes each bound function via `worker.trigger(...)`.
+## Attach metadata to a trigger
+
+Each trigger binding accepts an optional `metadata` JSON object set by the consumer at registration
+time. The engine stores it as-is and surfaces it in two places:
+
+1. The publishing worker's `TriggerHandler.registerTrigger(config)` callback sees it as
+ `config.metadata`, so the publisher can act on consumer-supplied tags (priority hints, audit
+ labels, routing keys for the publisher's own bookkeeping).
+2. [`engine::triggers::list`](/using-iii/functions#engine-functions-engine) returns it on each
+ `TriggerInfo`, so the console and any other worker doing discovery can read it.
```typescript
- // Inside the worker's HTTP listener, after matching a request to a binding:
- await worker.trigger({
- function_id: binding.function_id,
- payload: { method, headers, body },
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ metadata: { team: "platform", env: "staging" },
});
```
```python
- # Inside the worker's HTTP listener, after matching a request to a binding:
- worker.trigger({
- "function_id": binding["function_id"],
- "payload": {"method": method, "headers": headers, "body": body},
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ "metadata": {"team": "platform", "env": "staging"},
})
```
```rust
- use iii_sdk::TriggerRequest;
+ use iii_sdk::RegisterTriggerInput;
use serde_json::json;
- // Inside the worker's HTTP listener, after matching a request to a binding:
- worker
- .trigger(TriggerRequest {
- function_id: binding.function_id.clone(),
- payload: json!({ "method": method, "headers": headers, "body": body }),
- action: None,
- timeout_ms: None,
- })
- .await?;
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: Some(json!({ "team": "platform", "env": "staging" })),
+ })?;
```
+
-On each dispatched event, the engine evaluates the consumer's `config` and optional
-`condition_function_id`, then routes matching invocations to the bound function and returns the
-result to the caller.
+
+ Trigger _types_ have no metadata field of their own. Metadata is attached per binding, not per
+ type.
+
+Don't confuse trigger _metadata_ with trigger type [_schemas_](#attach-schemas-to-the-trigger-type)
+(`trigger_request_format` and `call_request_format`):
+
+- **Metadata** is set by the **consumer** on each binding (ie. the `worker.registerTrigger()` call).
+ It's a free-form tag bag the engine stores as-is, used for the publisher's bookkeeping and for
+ discovery. For example, a consumer binding to `http` might attach
+ `metadata: { team: "platform", env: "staging", on_call: "alice" }` so the publishing worker can
+ log the team, and [`engine::triggers::list`](/using-iii/functions#engine-functions-engine)
+ surfaces this information on request.
+- **Schemas** are set by the **publisher** when declaring the trigger type. They document the JSON
+ shapes the consumer interacts with. For example, the `http` type published by
+ [iii-http](https://workers.iii.dev/workers/iii-http) declares:
+ - `config` (what the consumer passes at bind time): `{ api_path, http_method }`.
+ - invocation payload (what their bound function receives on each request):
+ `{ method, headers, query_params, body }`.
+
+
+
+## Declaring a Trigger Type
+
+So far this documentation has focused on being the _consumer_: your worker's functions get bound to
+trigger types other workers publish. This section flips the roles. Your worker is now the
+_publisher_, and you want functions that other workers register to fire on events your worker
+observes (an HTTP request, a webhook hit, a file change, a database update).
+
+### Components of a Trigger Type
+
+A trigger type is two things bundled together:
+
+1. A string `id` that consumers reference when they bind to a trigger (ex. `type: "mini-http"`).
+2. A per-binding routing table that **your worker** maintains in-process. The engine's registry
+ records the binding canonically (this is what
+ [`engine::triggers::list`](/using-iii/functions#engine-functions-engine) returns), but the engine
+ doesn't dispatch on it. The engine forwards every bind/unbind from any consumer worker on the
+ network to the publisher worker as a callback, and this worker decides what to do with each one.
+
+You declare the trigger type once at startup with
+`worker.registerTriggerType({ id, description }, handler)`.
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+The `TriggerHandler` interface **you implement** exposes two callbacks. The engine invokes them on
+your publisher worker whenever a consumer binds or unbinds:
+
+- `registerTrigger(config)`: Runs when any consumer worker binds a function to your trigger type.
+ The `config` carries the trigger instance's `id`, the consumer's `function_id`, and the
+ consumer-supplied `config` matching the shape your type accepts. Stash it.
+- `unregisterTrigger(config)`: Runs on unbind. Drop it from your table.
+
+The trigger type can be torn down at any point during runtime with
+`worker.unregisterTriggerType(...)` (or `worker.unregister_trigger_type(...)` in Python and Rust).
+See the [Unregister a Trigger Type](#unregister-a-trigger-type) section below for per-language
+signatures.
+
+### Example: A mini `iii-http` from scratch
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+The example below sketches a tiny version of [iii-http](https://workers.iii.dev/workers/iii-http),
+the worker that publishes the real `http` trigger type. The publisher worker:
+
+1. Declares an HTTP-shaped trigger type called `mini-http`
+1. Maintains a `bindings` map of `{ trigger id → function_id, method+path }` as consumers
+ bind/unbind
+1. Is then ready to look up the right binding when an HTTP request is received. Firing the bound
+ function is covered in [Dispatch events to bound functions](#dispatch-events-to-bound-functions)
+ below.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+ import type { TriggerConfig, TriggerHandler } from "iii-sdk";
+
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
+
+ type MiniHttpConfig = {
+ api_path: string; // leading slash, e.g. "/orders"
+ http_method?: "GET" | "POST" | "PUT" | "DELETE";
+ };
+
+ const bindings = new Map>();
+
+ const httpHandler: TriggerHandler = {
+ async registerTrigger(config) {
+ bindings.set(config.id, config);
+ },
+ async unregisterTrigger(config) {
+ bindings.delete(config.id);
+ },
+ };
+
+ worker.registerTriggerType(
+ { id: "mini-http", description: "Routes HTTP requests to bound functions" },
+ httpHandler,
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import (
+ InitOptions,
+ RegisterTriggerTypeInput,
+ TriggerConfig,
+ TriggerHandler,
+ register_worker,
+ )
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="mini-http-worker"),
+ )
+
+ bindings: dict[str, TriggerConfig] = {}
-## Unregister a trigger type
+ class HttpHandler(TriggerHandler):
+ async def register_trigger(self, config: TriggerConfig) -> None:
+ bindings[config.id] = config
-`registerTriggerType` returns a handle with an `unregister()` method that tears down the trigger
-type at runtime. When the worker disconnects, all trigger types it advertised are removed
-automatically and the engine stops routing events that depended on them.
+ async def unregister_trigger(self, config: TriggerConfig) -> None:
+ bindings.pop(config.id, None)
+
+ worker.register_trigger_type(
+ RegisterTriggerTypeInput(
+ id="mini-http",
+ description="Routes HTTP requests to bound functions",
+ ),
+ HttpHandler(),
+ )
+ ```
+
+
+ ```rust
+ use std::collections::HashMap;
+ use std::sync::{Arc, Mutex};
+ use iii_sdk::{
+ InitOptions, RegisterTriggerType, TriggerConfig, TriggerHandler, register_worker,
+ };
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ #[derive(Default)]
+ struct HttpHandler {
+ bindings: Arc>>,
+ }
+
+ #[async_trait::async_trait]
+ impl TriggerHandler for HttpHandler {
+ async fn register_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
+ self.bindings.lock().unwrap().insert(config.id.clone(), config);
+ Ok(())
+ }
+
+ async fn unregister_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
+ self.bindings.lock().unwrap().remove(&config.id);
+ Ok(())
+ }
+ }
+
+ worker.register_trigger_type(
+ RegisterTriggerType::new(
+ "mini-http",
+ "Routes HTTP requests to bound functions",
+ HttpHandler::default(),
+ ),
+ );
+ ```
+
+
+
+
+
+### Attach schemas to the trigger type
+
+A trigger type can carry two optional JSON Schemas that describe its payloads:
+
+- **`trigger_request_format`**: The schema for the per-binding `config` consumers pass to
+ `worker.registerTrigger(...)` when they bind a function to your trigger type.
+- **`call_request_format`**: The schema for the payload your worker delivers to bound functions when
+ the trigger fires.
+
+Both feed the iii console, the agent-readable skills, and the
+[`engine::trigger-types::list`](/using-iii/functions#engine-functions-engine) output so consumers
+know what to send and what they'll receive.
+
+
+ Runtime validation is not yet supported. Attached schemas are informational only; the engine does
+ not reject `config` values or call payloads that don't match them. Treat the schemas as contract
+ documentation for consumers, agents, and the console; same caveat as [function request / response
+ schemas](/creating-workers/functions#attach-request-and-response-schemas).
+
+
+Each SDK accepts these in its own idiomatic way:
+
+| SDK | What you pass |
+| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| [Node](/sdk-reference/node-sdk#registertriggertype) / [Browser](/sdk-reference/browser-sdk#registertriggertype) | Raw JSON Schema objects on `trigger_request_format` / `call_request_format`. Convert Zod 4+ schemas with `z.toJSONSchema(...)`. |
+| [Python](/sdk-reference/python-sdk#register_trigger_type) | A Pydantic model class (auto-converted) or a raw dict on the same fields of `RegisterTriggerTypeInput`. |
+| [Rust](/sdk-reference/rust-sdk#register_trigger_type) | Builder methods on `RegisterTriggerType`: `.trigger_request_format::()` and `.call_request_format::()`, where `T: schemars::JsonSchema`. |
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+### Unregister a Trigger Type
+
+Tear down a trigger type at runtime when the work it routes is no longer needed. When the worker
+disconnects, all trigger types it advertised are removed automatically and the engine stops routing
+events that depended on them, so this step is only necessary if you want to drop a type while the
+worker stays connected.
+
+Call it any time after `registerTriggerType` while the worker stays connected (e.g. the underlying
+resource went into maintenance mode, a feature flag turned the surface off, or you want to rotate
+the type to a new schema without restarting). Continuing the `mini-http` example, here the worker
+drops `mini-http` because its HTTP listener was disabled by config:
```typescript
- webhook.unregister();
+ // e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ // while the worker keeps serving other trigger types.
+ worker.unregisterTriggerType({
+ id: "mini-http",
+ description: "Routes HTTP requests to bound functions",
+ });
```
```python
+ # e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ # while the worker keeps serving other trigger types.
worker.unregister_trigger_type(
- RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
+ {"id": "mini-http", "description": "Routes HTTP requests to bound functions"}
)
```
```rust
- worker.unregister_trigger_type("webhook");
+ // e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ // while the worker keeps serving other trigger types.
+ worker.unregister_trigger_type("mini-http");
```
-## What goes in Worker Docs
+{/* TODO: Update once this inconsistency is fixed */}
+
+
+ Node's `registerTriggerType` also returns a `TriggerTypeRef` with an `.unregister()` shortcut that
+ delegates to `worker.unregisterTriggerType(...)`. Python's `TriggerTypeRef` only exposes
+ `register_trigger` and `register_function`; tear down the trigger type itself via
+ `worker.unregister_trigger_type(...)`. Rust takes only the `id` string; Node and Python take the
+ full input object but only the `id` field is used to identify the type being torn down.
+
+
+## Dispatch events to bound functions
+
+There is no special "fire" API. When the underlying event source delivers something (an incoming
+HTTP request, a cron tick, a webhook hit), your publisher worker looks up the relevant entry in the
+`bindings` table it built inside its `registerTrigger` callback and invokes each matching function
+via `worker.trigger(...)`.
+
+Continuing the `mini-http` example from above:
-The trigger type _id_, the shape of the `config` consumers pass when binding, the call-payload
-shape your worker delivers to bound functions, the event ordering guarantees, and any
-back-pressure or retry semantics belong in this worker's Worker Docs so consumers know what to
-pass when they call `registerTrigger`. Keep iii-level concepts (the trigger binding model itself,
-condition gates) here; document the per-type specifics there.
+
+
+ ```typescript
+ // Inside the worker's HTTP listener, after matching method+path to an
+ // entry in the `bindings` map from the declare-trigger-type example:
+ const binding = bindings.get(matchedTriggerId);
+ await worker.trigger({
+ function_id: binding.function_id,
+ payload: { method, headers, body },
+ });
+ ```
+
+
+ ```python
+ # Inside the worker's HTTP listener, after matching method+path to an
+ # entry in the `bindings` dict from the declare-trigger-type example:
+ binding = bindings[matched_trigger_id]
+ worker.trigger({
+ "function_id": binding.function_id,
+ "payload": {"method": method, "headers": headers, "body": body},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ // Inside the worker's HTTP listener, after matching method+path to an
+ // entry in the handler's `bindings` map from the declare-trigger-type example:
+ let binding = handler.bindings.lock().unwrap().get(&matched_trigger_id).cloned();
+ if let Some(binding) = binding {
+ worker
+ .trigger(TriggerRequest {
+ function_id: binding.function_id.clone(),
+ payload: json!({ "method": method, "headers": headers, "body": body }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ }
+ ```
+
+
+
+
+On each dispatched event, the engine evaluates the consumer's `config` and optional
+`condition_function_id`, then routes matching invocations to the bound function and returns the
+result to the caller.
diff --git a/docs/creating-workers/triggers.mdx.skill.md b/docs/creating-workers/triggers.mdx.skill.md
index 45a2c31af..5ba902d87 100644
--- a/docs/creating-workers/triggers.mdx.skill.md
+++ b/docs/creating-workers/triggers.mdx.skill.md
@@ -3,195 +3,446 @@
## What "writing a trigger" means
-Workers don't only call functions; they can also _source_ events. When a worker advertises a
-trigger type (e.g. `webhook`, a custom schedule, or any external event source you implement), any
-other worker can bind its functions to that type. The engine routes binding requests to your
-trigger type's handler, and your worker invokes the bound functions when the underlying event
-fires.
-
-This page is about authoring new trigger types from inside your worker. For invoking functions and
-binding triggers to functions from the consumer side, see [Using iii / Triggers](/using-iii/triggers).
-
-## Declare a trigger type
-
-Inside the worker, register the trigger type once during startup. You pass an `id` and
-`description` plus a handler with `registerTrigger` / `unregisterTrigger` callbacks. The engine
-calls these whenever a consumer binds or unbinds a function against your trigger type, so your
-worker can keep its own table of `{ trigger id → function id, config }` bindings.
-
-`registerTriggerType` returns a typed handle. Use it later to bind functions you own to this
-trigger type, or to tear the type down.
+A worker uses triggers two ways. Most of the time, you bind the worker's functions by registering a
+trigger on an existing trigger type such as: [`http`](https://workers.iii.dev/workers/iii-http),
+[`cron`](https://workers.iii.dev/workers/iii-cron),
+[queue messages](https://workers.iii.dev/workers/iii-queue),
+[`state` changes](https://workers.iii.dev/workers/iii-state), and any other event source in the
+system. Less often, you register a new trigger type from your worker so other workers can bind their
+functions to events your worker emits.
+
+This page primarily covers the latter: making your own triggers, if you want to use existing
+triggers in new workers then refer to [Using iii / Triggers](/using-iii/triggers).
+
+
+ For the caller-side mechanics (direct invocation with `worker.trigger` or `iii trigger`, the
+ `TriggerAction` variants, gating with conditions, multiple bindings per function), see [Using iii
+ / Triggers](/using-iii/triggers).
+
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+## Bind a function to an existing trigger type
+
+Most workers consume trigger types that other workers already publish: `http` from
+[iii-http](https://workers.iii.dev/workers/iii-http) to expose a function as an endpoint, `cron`
+from [iii-cron](https://workers.iii.dev/workers/iii-cron) to run a function on a schedule, queue
+triggers from [iii-queue](https://workers.iii.dev/workers/iii-queue) to fire a function on each
+message, `state` from [iii-state](https://workers.iii.dev/workers/iii-state) to react to data
+changes. Bind one of your worker's functions to a trigger type with
+`worker.registerTrigger({ type, function_id, config })`. The worker that publishes the trigger type
+must be connected when you register; otherwise the registration fails.
```typescript
- import { registerWorker } from "iii-sdk";
- import type { TriggerConfig, TriggerHandler } from "iii-sdk";
-
- const worker = registerWorker(process.env.III_URL);
-
- type WebhookTriggerConfig = {
- path: string;
- secret?: string;
- };
-
- const webhookHandler: TriggerHandler = {
- async registerTrigger(config: TriggerConfig) {
- // Stash { config.id, config.function_id, config.config } so the
- // worker's HTTP listener knows which function to invoke when a
- // request matches `config.config.path`.
- },
- async unregisterTrigger(config: TriggerConfig) {
- // Remove the binding from the worker's table.
- },
- };
-
- const webhook = worker.registerTriggerType(
- { id: "webhook", description: "Incoming webhook trigger" },
- webhookHandler,
- );
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
```
```python
- import os
- from iii import (
- InitOptions,
- RegisterTriggerTypeInput,
- TriggerConfig,
- TriggerHandler,
- register_worker,
- )
-
- worker = register_worker(
- os.environ.get("III_URL"),
- InitOptions(worker_name="webhook-worker"),
- )
-
- class WebhookHandler(TriggerHandler):
- async def register_trigger(self, config: TriggerConfig) -> None:
- # Stash config.id, config.function_id, config.config
- ...
-
- async def unregister_trigger(self, config: TriggerConfig) -> None:
- ...
-
- webhook = worker.register_trigger_type(
- RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
- WebhookHandler(),
- )
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
```
```rust
- use iii_sdk::{
- InitOptions, RegisterTriggerType, TriggerConfig, TriggerHandler, register_worker,
- };
-
- let url = std::env::var("III_URL").expect("III_URL must be set");
- let worker = register_worker(&url, InitOptions::default());
-
- struct WebhookHandler;
-
- #[async_trait::async_trait]
- impl TriggerHandler for WebhookHandler {
- async fn register_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
- // Stash config.id, config.function_id, config.config
- Ok(())
- }
-
- async fn unregister_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
- Ok(())
- }
- }
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
- let webhook = worker.register_trigger_type(
- RegisterTriggerType::new("webhook", "Incoming webhook trigger", WebhookHandler),
- );
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
```
+
-For typed `config` and call-payload schemas, attach Pydantic, Zod, or `schemars::JsonSchema` types
-to the registration in your language of choice. See each SDK's reference for the exact builder.
+The `config` shape is defined per trigger type and documented in each publishing worker's
+[Worker Docs](https://workers.iii.dev).
-## Dispatch events to bound functions
+
+ Other binding mechanics are covered in [Using iii /
+ Triggers](/using-iii/triggers#register-a-trigger): the unregister handle, binding multiple
+ triggers to the same function, gating with `condition_function_id`, and the `TriggerAction`
+ variants (`Void`, `Enqueue`, etc.).
+
-There is no special "fire" API. When the underlying event source delivers something (an incoming
-HTTP request, a cron tick, a webhook hit), your worker looks up the bindings it stashed in its
-`registerTrigger` callback and invokes each bound function via `worker.trigger(...)`.
+## Attach metadata to a trigger
+
+Each trigger binding accepts an optional `metadata` JSON object set by the consumer at registration
+time. The engine stores it as-is and surfaces it in two places:
+
+1. The publishing worker's `TriggerHandler.registerTrigger(config)` callback sees it as
+ `config.metadata`, so the publisher can act on consumer-supplied tags (priority hints, audit
+ labels, routing keys for the publisher's own bookkeeping).
+2. [`engine::triggers::list`](/using-iii/functions#engine-functions-engine) returns it on each
+ `TriggerInfo`, so the console and any other worker doing discovery can read it.
```typescript
- // Inside the worker's HTTP listener, after matching a request to a binding:
- await worker.trigger({
- function_id: binding.function_id,
- payload: { method, headers, body },
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ metadata: { team: "platform", env: "staging" },
});
```
```python
- # Inside the worker's HTTP listener, after matching a request to a binding:
- worker.trigger({
- "function_id": binding["function_id"],
- "payload": {"method": method, "headers": headers, "body": body},
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ "metadata": {"team": "platform", "env": "staging"},
})
```
```rust
- use iii_sdk::TriggerRequest;
+ use iii_sdk::RegisterTriggerInput;
use serde_json::json;
- // Inside the worker's HTTP listener, after matching a request to a binding:
- worker
- .trigger(TriggerRequest {
- function_id: binding.function_id.clone(),
- payload: json!({ "method": method, "headers": headers, "body": body }),
- action: None,
- timeout_ms: None,
- })
- .await?;
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: Some(json!({ "team": "platform", "env": "staging" })),
+ })?;
```
+
-On each dispatched event, the engine evaluates the consumer's `config` and optional
-`condition_function_id`, then routes matching invocations to the bound function and returns the
-result to the caller.
-
-## Unregister a trigger type
-
-`registerTriggerType` returns a handle with an `unregister()` method that tears down the trigger
-type at runtime. When the worker disconnects, all trigger types it advertised are removed
-automatically and the engine stops routing events that depended on them.
+
+ Trigger _types_ have no metadata field of their own. Metadata is attached per binding, not per
+ type.
+
+Don't confuse trigger _metadata_ with trigger type [_schemas_](#attach-schemas-to-the-trigger-type)
+(`trigger_request_format` and `call_request_format`):
+
+- **Metadata** is set by the **consumer** on each binding (ie. the `worker.registerTrigger()` call).
+ It's a free-form tag bag the engine stores as-is, used for the publisher's bookkeeping and for
+ discovery. For example, a consumer binding to `http` might attach
+ `metadata: { team: "platform", env: "staging", on_call: "alice" }` so the publishing worker can
+ log the team, and [`engine::triggers::list`](/using-iii/functions#engine-functions-engine)
+ surfaces this information on request.
+- **Schemas** are set by the **publisher** when declaring the trigger type. They document the JSON
+ shapes the consumer interacts with. For example, the `http` type published by
+ [iii-http](https://workers.iii.dev/workers/iii-http) declares:
+ - `config` (what the consumer passes at bind time): `{ api_path, http_method }`.
+ - invocation payload (what their bound function receives on each request):
+ `{ method, headers, query_params, body }`.
+
+
+
+## Declaring a Trigger Type
+
+So far this documentation has focused on being the _consumer_: your worker's functions get bound to
+trigger types other workers publish. This section flips the roles. Your worker is now the
+_publisher_, and you want functions that other workers register to fire on events your worker
+observes (an HTTP request, a webhook hit, a file change, a database update).
+
+### Components of a Trigger Type
+
+A trigger type is two things bundled together:
+
+1. A string `id` that consumers reference when they bind to a trigger (ex. `type: "mini-http"`).
+2. A per-binding routing table that **your worker** maintains in-process. The engine's registry
+ records the binding canonically (this is what
+ [`engine::triggers::list`](/using-iii/functions#engine-functions-engine) returns), but the engine
+ doesn't dispatch on it. The engine forwards every bind/unbind from any consumer worker on the
+ network to the publisher worker as a callback, and this worker decides what to do with each one.
+
+You declare the trigger type once at startup with
+`worker.registerTriggerType({ id, description }, handler)`.
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+The `TriggerHandler` interface **you implement** exposes two callbacks. The engine invokes them on
+your publisher worker whenever a consumer binds or unbinds:
+
+- `registerTrigger(config)`: Runs when any consumer worker binds a function to your trigger type.
+ The `config` carries the trigger instance's `id`, the consumer's `function_id`, and the
+ consumer-supplied `config` matching the shape your type accepts. Stash it.
+- `unregisterTrigger(config)`: Runs on unbind. Drop it from your table.
+
+The trigger type can be torn down at any point during runtime with
+`worker.unregisterTriggerType(...)` (or `worker.unregister_trigger_type(...)` in Python and Rust).
+See the [Unregister a Trigger Type](#unregister-a-trigger-type) section below for per-language
+signatures.
+
+### Example: A mini `iii-http` from scratch
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+The example below sketches a tiny version of [iii-http](https://workers.iii.dev/workers/iii-http),
+the worker that publishes the real `http` trigger type. The publisher worker:
+
+1. Declares an HTTP-shaped trigger type called `mini-http`
+1. Maintains a `bindings` map of `{ trigger id → function_id, method+path }` as consumers
+ bind/unbind
+1. Is then ready to look up the right binding when an HTTP request is received. Firing the bound
+ function is covered in [Dispatch events to bound functions](#dispatch-events-to-bound-functions)
+ below.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+ import type { TriggerConfig, TriggerHandler } from "iii-sdk";
+
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
+
+ type MiniHttpConfig = {
+ api_path: string; // leading slash, e.g. "/orders"
+ http_method?: "GET" | "POST" | "PUT" | "DELETE";
+ };
+
+ const bindings = new Map>();
+
+ const httpHandler: TriggerHandler = {
+ async registerTrigger(config) {
+ bindings.set(config.id, config);
+ },
+ async unregisterTrigger(config) {
+ bindings.delete(config.id);
+ },
+ };
+
+ worker.registerTriggerType(
+ { id: "mini-http", description: "Routes HTTP requests to bound functions" },
+ httpHandler,
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import (
+ InitOptions,
+ RegisterTriggerTypeInput,
+ TriggerConfig,
+ TriggerHandler,
+ register_worker,
+ )
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="mini-http-worker"),
+ )
+
+ bindings: dict[str, TriggerConfig] = {}
+
+ class HttpHandler(TriggerHandler):
+ async def register_trigger(self, config: TriggerConfig) -> None:
+ bindings[config.id] = config
+
+ async def unregister_trigger(self, config: TriggerConfig) -> None:
+ bindings.pop(config.id, None)
+
+ worker.register_trigger_type(
+ RegisterTriggerTypeInput(
+ id="mini-http",
+ description="Routes HTTP requests to bound functions",
+ ),
+ HttpHandler(),
+ )
+ ```
+
+
+ ```rust
+ use std::collections::HashMap;
+ use std::sync::{Arc, Mutex};
+ use iii_sdk::{
+ InitOptions, RegisterTriggerType, TriggerConfig, TriggerHandler, register_worker,
+ };
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ #[derive(Default)]
+ struct HttpHandler {
+ bindings: Arc>>,
+ }
+
+ #[async_trait::async_trait]
+ impl TriggerHandler for HttpHandler {
+ async fn register_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
+ self.bindings.lock().unwrap().insert(config.id.clone(), config);
+ Ok(())
+ }
+
+ async fn unregister_trigger(&self, config: TriggerConfig) -> Result<(), iii_sdk::IIIError> {
+ self.bindings.lock().unwrap().remove(&config.id);
+ Ok(())
+ }
+ }
+
+ worker.register_trigger_type(
+ RegisterTriggerType::new(
+ "mini-http",
+ "Routes HTTP requests to bound functions",
+ HttpHandler::default(),
+ ),
+ );
+ ```
+
+
+
+
+
+### Attach schemas to the trigger type
+
+A trigger type can carry two optional JSON Schemas that describe its payloads:
+
+- **`trigger_request_format`**: The schema for the per-binding `config` consumers pass to
+ `worker.registerTrigger(...)` when they bind a function to your trigger type.
+- **`call_request_format`**: The schema for the payload your worker delivers to bound functions when
+ the trigger fires.
+
+Both feed the iii console, the agent-readable skills, and the
+[`engine::trigger-types::list`](/using-iii/functions#engine-functions-engine) output so consumers
+know what to send and what they'll receive.
+
+
+ Runtime validation is not yet supported. Attached schemas are informational only; the engine does
+ not reject `config` values or call payloads that don't match them. Treat the schemas as contract
+ documentation for consumers, agents, and the console; same caveat as [function request / response
+ schemas](/creating-workers/functions#attach-request-and-response-schemas).
+
+
+Each SDK accepts these in its own idiomatic way:
+
+| SDK | What you pass |
+| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| [Node](/sdk-reference/node-sdk#registertriggertype) / [Browser](/sdk-reference/browser-sdk#registertriggertype) | Raw JSON Schema objects on `trigger_request_format` / `call_request_format`. Convert Zod 4+ schemas with `z.toJSONSchema(...)`. |
+| [Python](/sdk-reference/python-sdk#register_trigger_type) | A Pydantic model class (auto-converted) or a raw dict on the same fields of `RegisterTriggerTypeInput`. |
+| [Rust](/sdk-reference/rust-sdk#register_trigger_type) | Builder methods on `RegisterTriggerType`: `.trigger_request_format::()` and `.call_request_format::()`, where `T: schemars::JsonSchema`. |
+
+{/* TODO: Review against real SDK/CLI surface (now, and post-sdk rework, separately) */}
+
+### Unregister a Trigger Type
+
+Tear down a trigger type at runtime when the work it routes is no longer needed. When the worker
+disconnects, all trigger types it advertised are removed automatically and the engine stops routing
+events that depended on them, so this step is only necessary if you want to drop a type while the
+worker stays connected.
+
+Call it any time after `registerTriggerType` while the worker stays connected (e.g. the underlying
+resource went into maintenance mode, a feature flag turned the surface off, or you want to rotate
+the type to a new schema without restarting). Continuing the `mini-http` example, here the worker
+drops `mini-http` because its HTTP listener was disabled by config:
```typescript
- webhook.unregister();
+ // e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ // while the worker keeps serving other trigger types.
+ worker.unregisterTriggerType({
+ id: "mini-http",
+ description: "Routes HTTP requests to bound functions",
+ });
```
```python
+ # e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ # while the worker keeps serving other trigger types.
worker.unregister_trigger_type(
- RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
+ {"id": "mini-http", "description": "Routes HTTP requests to bound functions"}
)
```
```rust
- worker.unregister_trigger_type("webhook");
+ // e.g. config reload disabled the HTTP listener; stop accepting new bindings
+ // while the worker keeps serving other trigger types.
+ worker.unregister_trigger_type("mini-http");
```
-## What goes in Worker Docs
+{/* TODO: Update once this inconsistency is fixed */}
-The trigger type _id_, the shape of the `config` consumers pass when binding, the call-payload
-shape your worker delivers to bound functions, the event ordering guarantees, and any
-back-pressure or retry semantics belong in this worker's Worker Docs so consumers know what to
-pass when they call `registerTrigger`. Keep iii-level concepts (the trigger binding model itself,
-condition gates) here; document the per-type specifics there.
+
+ Node's `registerTriggerType` also returns a `TriggerTypeRef` with an `.unregister()` shortcut that
+ delegates to `worker.unregisterTriggerType(...)`. Python's `TriggerTypeRef` only exposes
+ `register_trigger` and `register_function`; tear down the trigger type itself via
+ `worker.unregister_trigger_type(...)`. Rust takes only the `id` string; Node and Python take the
+ full input object but only the `id` field is used to identify the type being torn down.
+
+
+## Dispatch events to bound functions
+
+There is no special "fire" API. When the underlying event source delivers something (an incoming
+HTTP request, a cron tick, a webhook hit), your publisher worker looks up the relevant entry in the
+`bindings` table it built inside its `registerTrigger` callback and invokes each matching function
+via `worker.trigger(...)`.
+
+Continuing the `mini-http` example from above:
+
+
+
+ ```typescript
+ // Inside the worker's HTTP listener, after matching method+path to an
+ // entry in the `bindings` map from the declare-trigger-type example:
+ const binding = bindings.get(matchedTriggerId);
+ await worker.trigger({
+ function_id: binding.function_id,
+ payload: { method, headers, body },
+ });
+ ```
+
+
+ ```python
+ # Inside the worker's HTTP listener, after matching method+path to an
+ # entry in the `bindings` dict from the declare-trigger-type example:
+ binding = bindings[matched_trigger_id]
+ worker.trigger({
+ "function_id": binding.function_id,
+ "payload": {"method": method, "headers": headers, "body": body},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ // Inside the worker's HTTP listener, after matching method+path to an
+ // entry in the handler's `bindings` map from the declare-trigger-type example:
+ let binding = handler.bindings.lock().unwrap().get(&matched_trigger_id).cloned();
+ if let Some(binding) = binding {
+ worker
+ .trigger(TriggerRequest {
+ function_id: binding.function_id.clone(),
+ payload: json!({ "method": method, "headers": headers, "body": body }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ }
+ ```
+
+
+
+
+On each dispatched event, the engine evaluates the consumer's `config` and optional
+`condition_function_id`, then routes matching invocations to the bound function and returns the
+result to the caller.
diff --git a/docs/creating-workers/workers.mdx b/docs/creating-workers/workers.mdx
index 72305cb34..68264129f 100644
--- a/docs/creating-workers/workers.mdx
+++ b/docs/creating-workers/workers.mdx
@@ -1,6 +1,6 @@
---
title: "Workers"
-description: "Deploying and integrating workers into an iii project."
+description: "Deploying and integrating workers into a iii project."
owner: "devrel"
type: "how-to"
---
@@ -8,23 +8,67 @@ type: "how-to"
## How workers expand iii
Workers add capability to an iii system. Each one contributes functions and triggers the engine can
-route to. This page covers deploying and wiring workers into a project. For the authoring surface a
-worker uses to register its functions and triggers (the SDK), see each language's Worker Docs
-authoring guide.
+route to. This page covers deploying and wiring workers into a project.
+
+Once connected, a worker exposes:
+
+- Functions, callable by `function_id` from anywhere in the system (see
+ [Using iii / Functions](/using-iii/functions)).
+- Triggers it advertises, which other workers can bind their functions to (see
+ [Using iii / Triggers](/using-iii/triggers)).
+
+
+ For the full SDK surface each Worker can use when interacting with iii, see the complete SDK
+ reference by language: [Node](/sdk-reference/node-sdk), [Python](/sdk-reference/python-sdk),
+ [Rust](/sdk-reference/rust-sdk), or [Browser](/sdk-reference/browser-sdk).
+
+
+## Scaffold a new worker
+
+`iii worker init` creates a new standalone worker from scratch. The command writes a
+language-specific project directory with the iii SDK installed, an `iii.worker.yaml` manifest, and
+example function and trigger registrations you can replace with your own.
+
+```bash
+# Interactive: prompts for the language
+iii worker init my-worker
+
+# Fully scripted: pass --language to skip the prompt
+iii worker init my-worker --language typescript
+```
+
+Supported languages: `typescript` (`ts`), `javascript` (`js`), `python` (`py`), `rust` (`rs`).
+
+The positional `NAME` is the target directory; pass `--directory` to override it.
+
+Re-running `iii worker init` on a directory that already holds an iii worker (ie. has
+`.iii/worker.ini`) will make no changes to the worker.
+
+Worker init will fail by default when targeting a non-empty directory, use `--allow-non-empty` to
+scaffold into any other non-empty directory.
+
+
+ To install an existing worker from the registry instead of scaffolding a new one, use `iii worker
+ add`. See [Using iii / Workers](/using-iii/workers#finding-workers) for the registry surface.
+
## Connecting to the engine
-A worker connects to the engine over WebSocket. Set the engine URL via the `III_URL` environment
-variable, or pass it explicitly to `register_worker`. The connection string is the only coupling
-between a worker and the iii instance it joins, so the worker process can be deployed anywhere
-reachable on the network.
+A worker connects to the engine over WebSocket. The convention is to set the engine URL via the
+`III_URL` environment variable, but it can also be passed explicitly to `register_worker`. The
+connection string is the only coupling between a worker and the iii instance it joins, so the worker
+process can be deployed anywhere reachable on the network.
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url, {
+ workerName: "my-worker",
+ });
```
@@ -42,16 +86,27 @@ reachable on the network.
```rust
- use iii_sdk::{InitOptions, register_worker};
+ use iii_sdk::{InitOptions, WorkerMetadata, register_worker};
let url = std::env::var("III_URL").expect("III_URL must be set");
- let worker = register_worker(&url, InitOptions::default());
+ let worker = register_worker(
+ &url,
+ InitOptions {
+ metadata: Some(WorkerMetadata {
+ name: "my-worker".into(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ );
```
-## Worker lifecycle states
+## Worker lifecycle
+
+### States
Workers transition through a small set of states after connecting:
`connecting → connected → available / busy → disconnected`. `connecting` is the WebSocket handshake.
@@ -60,44 +115,328 @@ whether the Worker is currently handling invocations. `disconnected` is the term
WebSocket closes. The Engine tracks these transitions and surfaces them to other Workers and tooling
through its discovery functions, so the rest of the system can react.
-## Handling Worker disconnects
+### Inspecting the live registry
+
+To see what's currently connected to the Engine, invoke one of the `engine::*::list` Functions to
+get the current state of the registry. Each returns a list:
+
+| Function | What it returns |
+| ----------------------------- | --------------------------------------------------------------- |
+| `engine::workers::list` | Every connected Worker with metrics. |
+| `engine::functions::list` | Every registered Function. Filterable by `include_internal`. |
+| `engine::triggers::list` | Every registered Trigger. Filterable by `include_internal`. |
+| `engine::trigger-types::list` | Every advertised Trigger type with its config and call schemas. |
+
+
+
+
+ ```typescript
+ // engine::workers::list, pass { worker_id: "" } to look up one worker
+ const { workers } = await worker.trigger({
+ function_id: "engine::workers::list",
+ payload: {},
+ });
+
+ // engine::functions::list
+ const { functions } = await worker.trigger({
+ function_id: "engine::functions::list",
+ payload: { include_internal: false },
+ });
+
+ // engine::triggers::list
+ const { triggers } = await worker.trigger({
+ function_id: "engine::triggers::list",
+ payload: { include_internal: false },
+ });
+
+ // engine::trigger-types::list
+ const { trigger_types } = await worker.trigger({
+ function_id: "engine::trigger-types::list",
+ payload: { include_internal: false },
+ });
+ ```
+
+
+ ```python
+ # engine::workers::list, pass {"worker_id": ""} to look up one worker
+ workers = worker.trigger({
+ "function_id": "engine::workers::list",
+ "payload": {},
+ })["workers"]
+
+ # engine::functions::list
+ functions = worker.trigger({
+ "function_id": "engine::functions::list",
+ "payload": {"include_internal": False},
+ })["functions"]
+
+ # engine::triggers::list
+ triggers = worker.trigger({
+ "function_id": "engine::triggers::list",
+ "payload": {"include_internal": False},
+ })["triggers"]
+
+ # engine::trigger-types::list
+ trigger_types = worker.trigger({
+ "function_id": "engine::trigger-types::list",
+ "payload": {"include_internal": False},
+ })["trigger_types"]
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ // engine::workers::list, pass json!({ "worker_id": "" }) to look up one worker
+ let workers = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::workers::list".into(),
+ payload: json!({}),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::functions::list
+ let functions = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::functions::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::triggers::list
+ let triggers = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::triggers::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::trigger-types::list
+ let trigger_types = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::trigger-types::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+
+
+### Handling Worker disconnects
When a Worker's WebSocket closes, the Engine cleans up after it automatically. Its Functions and
Triggers leave the live registry, and any in-flight invocations of those Functions are cancelled.
-There are two things to handle on the caller side:
-
-1. **Catch `invocation_stopped`.** Callers waiting on a Function whose Worker disconnects mid-flight
- receive an `invocation_stopped` error rather than a timeout. Treat it like a cancellation, not a
- transient failure. Retrying will fail until a Worker reconnects and re-registers the Function.
-1. **Subscribe to the discovery events** if you need to react to topology changes:
- - `engine::workers-available` fires immediately when a Worker connects or disconnects.
- - `engine::functions-available` is eventually consistent; it fires on the next polling tick once
- the function-list hash changes.
-
-## Inspecting the live registry
-
-To see what's currently connected to the Engine, use one of two surfaces depending on whether you
-want a snapshot or a live subscription:
-
-- **Read a snapshot** by invoking one of the `engine::*::list` Functions:
- - `engine::workers::list`: every connected Worker with metrics.
- - `engine::functions::list`: every registered Function (filterable by `include_internal`).
- - `engine::triggers::list`: every registered Trigger (filterable by `include_internal`).
- - `engine::trigger-types::list`: every advertised Trigger type with its config and call schemas.
-- **Subscribe to changes** by registering a Trigger against `engine::workers-available` (fires when
- a Worker connects or disconnects) or `engine::functions-available` (fires when a Function is
- registered or unregistered). See [Handling Worker disconnects](#handling-worker-disconnects) for
- their consistency semantics.
-
- These are the high-level call surfaces. For the wire-level shapes, see [Engine
- protocol](/sdk-reference/engine-sdk#engine-discovery-functions).
-
+#### In flight requests
+
+In flight requests will get a `invocation_stopped` error, catch these errors and treat them like a
+cancellation. Retrying will fail until the Worker that owns this function reconnects.
+
+{/* TODO: Revisit and check code sample after SDK surface is reworked */}
+
+
+
+
+ ```typescript
+ import { IIIInvocationError } from "iii-sdk";
+
+ try {
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 1, b: 2 },
+ });
+ } catch (err) {
+ if (err instanceof IIIInvocationError && err.code === "invocation_stopped") {
+ // Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ // (see "Subscribe to changes" below) to know when to retry.
+ return;
+ }
+ throw err;
+ }
+ ```
+
+
+ ```python
+ from iii import IIIInvocationError
+
+ try:
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 1, "b": 2},
+ })
+ except IIIInvocationError as err:
+ if err.code == "invocation_stopped":
+ # Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ # (see "Subscribe to changes" below) to know when to retry.
+ return
+ raise
+ ```
+
+
+ ```rust
+ use iii_sdk::{IIIError, TriggerRequest};
+ use serde_json::json;
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".into(),
+ payload: json!({ "a": 1, "b": 2 }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await;
+
+ match result {
+ Err(IIIError::Remote { code, .. }) if code == "invocation_stopped" => {
+ // Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ // (see "Subscribe to changes" below) to know when to retry.
+ }
+ Err(e) => return Err(e.into()),
+ Ok(value) => { /* use value */ }
+ }
+ ```
+
+
+
+
+
+#### Subscribe to changes
+
+{/* TODO: Link out to SDK reference once SDK surface is stable */}
+
+You can register a Trigger against one of the engine's discovery events to react to topology changes
+as they happen. This is particularly useful for continuing work when a Worker comes back online.
+
+| Trigger | When it fires |
+| ----------------------------- | ----------------------------------------- |
+| `engine::workers-available` | A Worker connects or disconnects. |
+| `engine::functions-available` | A Function is registered or unregistered. |
+
+
+
+
+ ```typescript
+ worker.registerFunction(
+ "discovery::on-workers",
+ async (data: { event: string; worker_id: string }) => {
+ if (data.event === "worker_connected") {
+ // A Worker just joined the registry; its Functions are callable now.
+ }
+ },
+ );
+ worker.registerTrigger({
+ type: "engine::workers-available",
+ function_id: "discovery::on-workers",
+ config: {},
+ });
+
+ worker.registerFunction(
+ "discovery::on-functions",
+ async (data: { event: string; functions: { function_id: string }[] }) => {
+ // `functions` is the full snapshot after the change.
+ const ids = data.functions.map((f) => f.function_id);
+ },
+ );
+ worker.registerTrigger({
+ type: "engine::functions-available",
+ function_id: "discovery::on-functions",
+ config: {},
+ });
+ ```
+
+
+ ```python
+ async def on_workers(data: dict) -> None:
+ if data["event"] == "worker_connected":
+ # A Worker just joined the registry; its Functions are callable now.
+ pass
+
+ worker.register_function("discovery::on-workers", on_workers)
+ worker.register_trigger({
+ "type": "engine::workers-available",
+ "function_id": "discovery::on-workers",
+ "config": {},
+ })
+
+ async def on_functions(data: dict) -> None:
+ # `functions` is the full snapshot after the change.
+ ids = [f["function_id"] for f in data.get("functions", [])]
+
+ worker.register_function("discovery::on-functions", on_functions)
+ worker.register_trigger({
+ "type": "engine::functions-available",
+ "function_id": "discovery::on-functions",
+ "config": {},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{RegisterFunction, RegisterTriggerInput};
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+ use serde_json::{Value, json};
+
+ #[derive(Deserialize, JsonSchema)]
+ struct WorkersAvailable { event: String, worker_id: String }
+
+ #[derive(Deserialize, JsonSchema)]
+ struct FunctionsAvailable { event: String, functions: Vec }
+
+ worker.register_function(RegisterFunction::new_async(
+ "discovery::on-workers",
+ |input: WorkersAvailable| async move {
+ if input.event == "worker_connected" {
+ // A Worker just joined the registry; its Functions are callable now.
+ }
+ Ok::<_, String>(())
+ },
+ ));
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "engine::workers-available".into(),
+ function_id: "discovery::on-workers".into(),
+ config: json!({}),
+ metadata: None,
+ })?;
+
+ worker.register_function(RegisterFunction::new_async(
+ "discovery::on-functions",
+ |input: FunctionsAvailable| async move {
+ // `functions` is the full snapshot after the change.
+ let _count = input.functions.len();
+ Ok::<_, String>(())
+ },
+ ));
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "engine::functions-available".into(),
+ function_id: "discovery::on-functions".into(),
+ config: json!({}),
+ metadata: None,
+ })?;
+ ```
+
+
+
+
## Worker manifest
-When a worker is checked into a project so iii can launch it locally, `iii.worker.yaml` at the
-worker's root tells iii how to install dependencies, run the worker, and pass through configuration.
+`iii.worker.yaml` is the manifest at the worker's root that tells iii how to install dependencies,
+run the worker, and pass through configuration. This applies to both the iii worker CLI commands
+(e.g. [`start`, `stop`, `restart`](/using-iii/workers#starting-and-stopping-workers)) and to workers
+that iii starts automatically when they're specified in iii's
+[`config.yaml`](/using-iii/engine#configuration-file-structure).
```yaml
name: math-worker
@@ -110,30 +449,60 @@ scripts:
start: "python math_worker.py"
```
-The manifest is metadata about _starting_ the Worker. Once the Worker is running, the WebSocket
-connection to the Engine and the function registrations are what matter. A Worker started by
-`iii worker add` and a Worker started by hand in a container behave identically to the Engine.
+The manifest is metadata about _starting_ the Worker. Once the Worker is running iii treats them all
+the same. A Worker started by `iii` via its `config.yaml`, via `iii worker start`, or a manually run
+process that uses the iii SDK all behave identically with the Engine.
-For the full manifest field schema, see [Using iii / Workers](/using-iii/workers).
+
+ If a worker isn't starting correctly then make sure to check its manifest and [`iii worker
+ logs`](/using-iii/workers#inspecting-a-worker).
+
-## What a worker contributes
+{/* TODO: link to the canonical iii.worker.yaml reference page once it exists. */}
-Once connected, a worker exposes:
+## Shutting down a worker
-- Functions, callable by `function_id` from anywhere in the system (see
- [Using iii / Functions](/using-iii/functions)).
-- Triggers it advertises, which other workers can bind their functions to (see
- [Using iii / Triggers](/using-iii/triggers)).
+Call the SDK's `shutdown` to close the WebSocket cleanly. The engine removes the worker's Functions
+and Triggers from the registry, fires `engine::workers-available` with `worker_disconnected`, and
+cancels in-flight invocations targeting them with `invocation_stopped`.
-The SDK calls a worker uses to register these are language-specific and documented in each
-language's Worker Docs authoring guide.
+Without `shutdown`, an abrupt process exit reaches the same state once the engine notices the
+dropped socket; graceful shutdown makes it deterministic and faster.
-## Run an ephemeral worker
+
+
+ ```typescript
+ process.on("SIGTERM", async () => {
+ await worker.shutdown();
+ process.exit(0);
+ });
+ ```
+
+
+ ```python
+ import signal
-For one-shot jobs (Kubernetes Jobs, serverless containers, scheduled scripts), an SDK worker can
-connect to a remote engine, register its functions, do the work, and exit. The engine cleans up the
-worker's registrations on disconnect.
+ def _on_term(*_):
+ worker.shutdown()
+ raise SystemExit(0)
+
+ signal.signal(signal.SIGTERM, _on_term)
+ ```
+
+
+
+ ```rust
+ // Rust threads do not keep the process alive on their own; await this
+ // before `main` returns so the connection thread exits cleanly.
+ worker.shutdown_async().await;
+ ```
+
+
+
+
+ Shutdown is very useful for **One-shot / ephemeral workers**. Kubernetes Jobs, serverless
+ containers, or scheduled scripts can connect just like any other Worker, do their work, and
+ `shutdown()` (`shutdown_async().await` in Rust).
+
-Set `III_URL` to point the worker at the remote engine (see
-[Connecting to the engine](#connecting-to-the-engine)), register the work the job needs to expose,
-and let the process exit when the job is done.
+{/* TODO: confirm the SDK shutdown call (e.g. `worker.shutdown()`) and add a minimal Node / TypeScript, Python, Rust example that registers a function, awaits a single invocation, and exits cleanly. */}
diff --git a/docs/creating-workers/workers.mdx.skill.md b/docs/creating-workers/workers.mdx.skill.md
index 33cc03769..5ffee5dcb 100644
--- a/docs/creating-workers/workers.mdx.skill.md
+++ b/docs/creating-workers/workers.mdx.skill.md
@@ -1,28 +1,70 @@
-# Workers
-
## How workers expand iii
Workers add capability to an iii system. Each one contributes functions and triggers the engine can
-route to. This page covers deploying and wiring workers into a project. For the authoring surface a
-worker uses to register its functions and triggers (the SDK), see each language's Worker Docs
-authoring guide.
+route to. This page covers deploying and wiring workers into a project.
+
+Once connected, a worker exposes:
+
+- Functions, callable by `function_id` from anywhere in the system (see
+ [Using iii / Functions](/using-iii/functions)).
+- Triggers it advertises, which other workers can bind their functions to (see
+ [Using iii / Triggers](/using-iii/triggers)).
+
+
+ For the full SDK surface each Worker can use when interacting with iii, see the complete SDK
+ reference by language: [Node](/sdk-reference/node-sdk), [Python](/sdk-reference/python-sdk),
+ [Rust](/sdk-reference/rust-sdk), or [Browser](/sdk-reference/browser-sdk).
+
+
+## Scaffold a new worker
+
+`iii worker init` creates a new standalone worker from scratch. The command writes a
+language-specific project directory with the iii SDK installed, an `iii.worker.yaml` manifest, and
+example function and trigger registrations you can replace with your own.
+
+```bash
+# Interactive: prompts for the language
+iii worker init my-worker
+
+# Fully scripted: pass --language to skip the prompt
+iii worker init my-worker --language typescript
+```
+
+Supported languages: `typescript` (`ts`), `javascript` (`js`), `python` (`py`), `rust` (`rs`).
+
+The positional `NAME` is the target directory; pass `--directory` to override it.
+
+Re-running `iii worker init` on a directory that already holds an iii worker (ie. has
+`.iii/worker.ini`) will make no changes to the worker.
+
+Worker init will fail by default when targeting a non-empty directory, use `--allow-non-empty` to
+scaffold into any other non-empty directory.
+
+
+ To install an existing worker from the registry instead of scaffolding a new one, use `iii worker
+ add`. See [Using iii / Workers](/using-iii/workers#finding-workers) for the registry surface.
+
## Connecting to the engine
-A worker connects to the engine over WebSocket. Set the engine URL via the `III_URL` environment
-variable, or pass it explicitly to `register_worker`. The connection string is the only coupling
-between a worker and the iii instance it joins, so the worker process can be deployed anywhere
-reachable on the network.
+A worker connects to the engine over WebSocket. The convention is to set the engine URL via the
+`III_URL` environment variable, but it can also be passed explicitly to `register_worker`. The
+connection string is the only coupling between a worker and the iii instance it joins, so the worker
+process can be deployed anywhere reachable on the network.
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url, {
+ workerName: "my-worker",
+ });
```
@@ -40,16 +82,27 @@ reachable on the network.
```rust
- use iii_sdk::{InitOptions, register_worker};
+ use iii_sdk::{InitOptions, WorkerMetadata, register_worker};
let url = std::env::var("III_URL").expect("III_URL must be set");
- let worker = register_worker(&url, InitOptions::default());
+ let worker = register_worker(
+ &url,
+ InitOptions {
+ metadata: Some(WorkerMetadata {
+ name: "my-worker".into(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ );
```
-## Worker lifecycle states
+## Worker lifecycle
+
+### States
Workers transition through a small set of states after connecting:
`connecting → connected → available / busy → disconnected`. `connecting` is the WebSocket handshake.
@@ -58,44 +111,328 @@ whether the Worker is currently handling invocations. `disconnected` is the term
WebSocket closes. The Engine tracks these transitions and surfaces them to other Workers and tooling
through its discovery functions, so the rest of the system can react.
-## Handling Worker disconnects
+### Inspecting the live registry
+
+To see what's currently connected to the Engine, invoke one of the `engine::*::list` Functions to
+get the current state of the registry. Each returns a list:
+
+| Function | What it returns |
+| ----------------------------- | --------------------------------------------------------------- |
+| `engine::workers::list` | Every connected Worker with metrics. |
+| `engine::functions::list` | Every registered Function. Filterable by `include_internal`. |
+| `engine::triggers::list` | Every registered Trigger. Filterable by `include_internal`. |
+| `engine::trigger-types::list` | Every advertised Trigger type with its config and call schemas. |
+
+
+
+
+ ```typescript
+ // engine::workers::list, pass { worker_id: "" } to look up one worker
+ const { workers } = await worker.trigger({
+ function_id: "engine::workers::list",
+ payload: {},
+ });
+
+ // engine::functions::list
+ const { functions } = await worker.trigger({
+ function_id: "engine::functions::list",
+ payload: { include_internal: false },
+ });
+
+ // engine::triggers::list
+ const { triggers } = await worker.trigger({
+ function_id: "engine::triggers::list",
+ payload: { include_internal: false },
+ });
+
+ // engine::trigger-types::list
+ const { trigger_types } = await worker.trigger({
+ function_id: "engine::trigger-types::list",
+ payload: { include_internal: false },
+ });
+ ```
+
+
+ ```python
+ # engine::workers::list, pass {"worker_id": ""} to look up one worker
+ workers = worker.trigger({
+ "function_id": "engine::workers::list",
+ "payload": {},
+ })["workers"]
+
+ # engine::functions::list
+ functions = worker.trigger({
+ "function_id": "engine::functions::list",
+ "payload": {"include_internal": False},
+ })["functions"]
+
+ # engine::triggers::list
+ triggers = worker.trigger({
+ "function_id": "engine::triggers::list",
+ "payload": {"include_internal": False},
+ })["triggers"]
+
+ # engine::trigger-types::list
+ trigger_types = worker.trigger({
+ "function_id": "engine::trigger-types::list",
+ "payload": {"include_internal": False},
+ })["trigger_types"]
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ // engine::workers::list, pass json!({ "worker_id": "" }) to look up one worker
+ let workers = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::workers::list".into(),
+ payload: json!({}),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::functions::list
+ let functions = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::functions::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::triggers::list
+ let triggers = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::triggers::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+
+ // engine::trigger-types::list
+ let trigger_types = worker
+ .trigger(TriggerRequest {
+ function_id: "engine::trigger-types::list".into(),
+ payload: json!({ "include_internal": false }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+
+
+### Handling Worker disconnects
When a Worker's WebSocket closes, the Engine cleans up after it automatically. Its Functions and
Triggers leave the live registry, and any in-flight invocations of those Functions are cancelled.
-There are two things to handle on the caller side:
-
-1. **Catch `invocation_stopped`.** Callers waiting on a Function whose Worker disconnects mid-flight
- receive an `invocation_stopped` error rather than a timeout. Treat it like a cancellation, not a
- transient failure. Retrying will fail until a Worker reconnects and re-registers the Function.
-1. **Subscribe to the discovery events** if you need to react to topology changes:
- - `engine::workers-available` fires immediately when a Worker connects or disconnects.
- - `engine::functions-available` is eventually consistent; it fires on the next polling tick once
- the function-list hash changes.
-
-## Inspecting the live registry
-
-To see what's currently connected to the Engine, use one of two surfaces depending on whether you
-want a snapshot or a live subscription:
-
-- **Read a snapshot** by invoking one of the `engine::*::list` Functions:
- - `engine::workers::list`: every connected Worker with metrics.
- - `engine::functions::list`: every registered Function (filterable by `include_internal`).
- - `engine::triggers::list`: every registered Trigger (filterable by `include_internal`).
- - `engine::trigger-types::list`: every advertised Trigger type with its config and call schemas.
-- **Subscribe to changes** by registering a Trigger against `engine::workers-available` (fires when
- a Worker connects or disconnects) or `engine::functions-available` (fires when a Function is
- registered or unregistered). See [Handling Worker disconnects](#handling-worker-disconnects) for
- their consistency semantics.
-
- These are the high-level call surfaces. For the wire-level shapes, see [Engine
- protocol](/sdk-reference/engine-sdk#engine-discovery-functions).
-
+#### In flight requests
+
+In flight requests will get a `invocation_stopped` error, catch these errors and treat them like a
+cancellation. Retrying will fail until the Worker that owns this function reconnects.
+
+{/* TODO: Revisit and check code sample after SDK surface is reworked */}
+
+
+
+
+ ```typescript
+ import { IIIInvocationError } from "iii-sdk";
+
+ try {
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 1, b: 2 },
+ });
+ } catch (err) {
+ if (err instanceof IIIInvocationError && err.code === "invocation_stopped") {
+ // Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ // (see "Subscribe to changes" below) to know when to retry.
+ return;
+ }
+ throw err;
+ }
+ ```
+
+
+ ```python
+ from iii import IIIInvocationError
+
+ try:
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 1, "b": 2},
+ })
+ except IIIInvocationError as err:
+ if err.code == "invocation_stopped":
+ # Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ # (see "Subscribe to changes" below) to know when to retry.
+ return
+ raise
+ ```
+
+
+ ```rust
+ use iii_sdk::{IIIError, TriggerRequest};
+ use serde_json::json;
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".into(),
+ payload: json!({ "a": 1, "b": 2 }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await;
+
+ match result {
+ Err(IIIError::Remote { code, .. }) if code == "invocation_stopped" => {
+ // Worker disconnected mid-invocation. Subscribe to `engine::functions-available`
+ // (see "Subscribe to changes" below) to know when to retry.
+ }
+ Err(e) => return Err(e.into()),
+ Ok(value) => { /* use value */ }
+ }
+ ```
+
+
+
+
+
+#### Subscribe to changes
+
+{/* TODO: Link out to SDK reference once SDK surface is stable */}
+
+You can register a Trigger against one of the engine's discovery events to react to topology changes
+as they happen. This is particularly useful for continuing work when a Worker comes back online.
+
+| Trigger | When it fires |
+| ----------------------------- | ----------------------------------------- |
+| `engine::workers-available` | A Worker connects or disconnects. |
+| `engine::functions-available` | A Function is registered or unregistered. |
+
+
+
+
+ ```typescript
+ worker.registerFunction(
+ "discovery::on-workers",
+ async (data: { event: string; worker_id: string }) => {
+ if (data.event === "worker_connected") {
+ // A Worker just joined the registry; its Functions are callable now.
+ }
+ },
+ );
+ worker.registerTrigger({
+ type: "engine::workers-available",
+ function_id: "discovery::on-workers",
+ config: {},
+ });
+
+ worker.registerFunction(
+ "discovery::on-functions",
+ async (data: { event: string; functions: { function_id: string }[] }) => {
+ // `functions` is the full snapshot after the change.
+ const ids = data.functions.map((f) => f.function_id);
+ },
+ );
+ worker.registerTrigger({
+ type: "engine::functions-available",
+ function_id: "discovery::on-functions",
+ config: {},
+ });
+ ```
+
+
+ ```python
+ async def on_workers(data: dict) -> None:
+ if data["event"] == "worker_connected":
+ # A Worker just joined the registry; its Functions are callable now.
+ pass
+
+ worker.register_function("discovery::on-workers", on_workers)
+ worker.register_trigger({
+ "type": "engine::workers-available",
+ "function_id": "discovery::on-workers",
+ "config": {},
+ })
+
+ async def on_functions(data: dict) -> None:
+ # `functions` is the full snapshot after the change.
+ ids = [f["function_id"] for f in data.get("functions", [])]
+
+ worker.register_function("discovery::on-functions", on_functions)
+ worker.register_trigger({
+ "type": "engine::functions-available",
+ "function_id": "discovery::on-functions",
+ "config": {},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{RegisterFunction, RegisterTriggerInput};
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+ use serde_json::{Value, json};
+
+ #[derive(Deserialize, JsonSchema)]
+ struct WorkersAvailable { event: String, worker_id: String }
+
+ #[derive(Deserialize, JsonSchema)]
+ struct FunctionsAvailable { event: String, functions: Vec }
+
+ worker.register_function(RegisterFunction::new_async(
+ "discovery::on-workers",
+ |input: WorkersAvailable| async move {
+ if input.event == "worker_connected" {
+ // A Worker just joined the registry; its Functions are callable now.
+ }
+ Ok::<_, String>(())
+ },
+ ));
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "engine::workers-available".into(),
+ function_id: "discovery::on-workers".into(),
+ config: json!({}),
+ metadata: None,
+ })?;
+
+ worker.register_function(RegisterFunction::new_async(
+ "discovery::on-functions",
+ |input: FunctionsAvailable| async move {
+ // `functions` is the full snapshot after the change.
+ let _count = input.functions.len();
+ Ok::<_, String>(())
+ },
+ ));
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "engine::functions-available".into(),
+ function_id: "discovery::on-functions".into(),
+ config: json!({}),
+ metadata: None,
+ })?;
+ ```
+
+
+
+
## Worker manifest
-When a worker is checked into a project so iii can launch it locally, `iii.worker.yaml` at the
-worker's root tells iii how to install dependencies, run the worker, and pass through configuration.
+`iii.worker.yaml` is the manifest at the worker's root that tells iii how to install dependencies,
+run the worker, and pass through configuration. This applies to both the iii worker CLI commands
+(e.g. [`start`, `stop`, `restart`](/using-iii/workers#starting-and-stopping-workers)) and to workers
+that iii starts automatically when they're specified in iii's
+[`config.yaml`](/using-iii/engine#configuration-file-structure).
```yaml
name: math-worker
@@ -108,30 +445,60 @@ scripts:
start: "python math_worker.py"
```
-The manifest is metadata about _starting_ the Worker. Once the Worker is running, the WebSocket
-connection to the Engine and the function registrations are what matter. A Worker started by
-`iii worker add` and a Worker started by hand in a container behave identically to the Engine.
+The manifest is metadata about _starting_ the Worker. Once the Worker is running iii treats them all
+the same. A Worker started by `iii` via its `config.yaml`, via `iii worker start`, or a manually run
+process that uses the iii SDK all behave identically with the Engine.
-For the full manifest field schema, see [Using iii / Workers](/using-iii/workers).
+
+ If a worker isn't starting correctly then make sure to check its manifest and [`iii worker
+ logs`](/using-iii/workers#inspecting-a-worker).
+
-## What a worker contributes
+{/* TODO: link to the canonical iii.worker.yaml reference page once it exists. */}
-Once connected, a worker exposes:
+## Shutting down a worker
-- Functions, callable by `function_id` from anywhere in the system (see
- [Using iii / Functions](/using-iii/functions)).
-- Triggers it advertises, which other workers can bind their functions to (see
- [Using iii / Triggers](/using-iii/triggers)).
+Call the SDK's `shutdown` to close the WebSocket cleanly. The engine removes the worker's Functions
+and Triggers from the registry, fires `engine::workers-available` with `worker_disconnected`, and
+cancels in-flight invocations targeting them with `invocation_stopped`.
+
+Without `shutdown`, an abrupt process exit reaches the same state once the engine notices the
+dropped socket; graceful shutdown makes it deterministic and faster.
+
+
+
+ ```typescript
+ process.on("SIGTERM", async () => {
+ await worker.shutdown();
+ process.exit(0);
+ });
+ ```
+
+
+ ```python
+ import signal
-The SDK calls a worker uses to register these are language-specific and documented in each
-language's Worker Docs authoring guide.
+ def _on_term(*_):
+ worker.shutdown()
+ raise SystemExit(0)
-## Run an ephemeral worker
+ signal.signal(signal.SIGTERM, _on_term)
+ ```
-For one-shot jobs (Kubernetes Jobs, serverless containers, scheduled scripts), an SDK worker can
-connect to a remote engine, register its functions, do the work, and exit. The engine cleans up the
-worker's registrations on disconnect.
+
+
+ ```rust
+ // Rust threads do not keep the process alive on their own; await this
+ // before `main` returns so the connection thread exits cleanly.
+ worker.shutdown_async().await;
+ ```
+
+
+
+
+ Shutdown is very useful for **One-shot / ephemeral workers**. Kubernetes Jobs, serverless
+ containers, or scheduled scripts can connect just like any other Worker, do their work, and
+ `shutdown()` (`shutdown_async().await` in Rust).
+
-Set `III_URL` to point the worker at the remote engine (see
-[Connecting to the engine](#connecting-to-the-engine)), register the work the job needs to expose,
-and let the process exit when the job is done.
+{/* TODO: confirm the SDK shutdown call (e.g. `worker.shutdown()`) and add a minimal Node / TypeScript, Python, Rust example that registers a function, awaits a single invocation, and exits cleanly. */}
diff --git a/docs/custom.css b/docs/custom.css
index d3afd760f..b362fff02 100644
--- a/docs/custom.css
+++ b/docs/custom.css
@@ -175,6 +175,14 @@ pre code {
color: inherit !important;
}
+pre,
+pre code,
+.code-block [data-line],
+.code-block .line {
+ font-variant-numeric: tabular-nums;
+ line-height: 1.45 !important;
+}
+
.code-block {
background: var(--iii-panel) !important;
border: 1px solid var(--iii-rule) !important;
@@ -183,7 +191,7 @@ pre code {
.code-block [data-component-part="code-block-root"] {
background: transparent !important;
- padding: 14px 16px !important;
+ padding: 10px 14px !important;
}
.code-block [data-component-part="code-block-root"] > pre {
@@ -339,3 +347,41 @@ a:has(> span.bg-primary-dark):hover svg {
[data-fade-overlay] {
display: none !important;
}
+
+/* Match accordion borders to table borders. Container is transparent so its
+ children control fill: trigger/header always uses inline-code panel bg,
+ expanded content uses the page bg. */
+details,
+[data-component-part="accordion"],
+[data-component-part="accordion-group"] > * {
+ background-color: transparent !important;
+ border: 1px solid var(--iii-rule-2) !important;
+ box-shadow: none !important;
+ color: var(--iii-ink) !important;
+}
+
+details > summary,
+[data-component-part="accordion"] > [data-component-part="accordion-trigger"],
+[data-component-part="accordion"] > button:first-child {
+ background-color: var(--iii-panel) !important;
+ color: var(--iii-ink) !important;
+}
+
+details[open] > summary,
+[data-component-part="accordion"][data-state="open"] > [data-component-part="accordion-trigger"],
+[data-component-part="accordion"][data-state="open"] > button:first-child {
+ border-bottom: 1px solid var(--iii-rule-2) !important;
+}
+
+details > *:not(summary),
+[data-component-part="accordion-content"] {
+ background-color: var(--iii-bg) !important;
+}
+
+[data-component-part="accordion-group"] {
+ border: 0 !important;
+}
+
+[data-component-part="accordion-group"] > * + * {
+ border-top: 0 !important;
+}
diff --git a/docs/docs.json b/docs/docs.json
index 5f301ba6c..2df83968d 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -253,7 +253,6 @@
"using-iii/workers-registry",
"using-iii/triggers",
"using-iii/functions",
- "using-iii/channels",
"using-iii/engine",
"using-iii/console",
"using-iii/cli",
@@ -265,19 +264,18 @@
"expanded": false,
"pages": [
"understanding-iii/index",
- "understanding-iii/workers",
- "understanding-iii/functions",
- "understanding-iii/triggers",
- "understanding-iii/channels",
- "understanding-iii/engine"
+ "understanding-iii/engine",
+ "understanding-iii/channels"
]
},
{
"group": "Creating Workers",
"pages": [
+ "creating-workers/index",
"creating-workers/workers",
+ "creating-workers/triggers",
"creating-workers/functions",
- "creating-workers/triggers"
+ "creating-workers/channels"
]
}
]
diff --git a/docs/how-to/schedule-cron-task.mdx b/docs/how-to/schedule-cron-task.mdx
new file mode 100644
index 000000000..be46b77b9
--- /dev/null
+++ b/docs/how-to/schedule-cron-task.mdx
@@ -0,0 +1,8 @@
+---
+title: "Schedule a cron task"
+description: "Run a function on a recurring schedule with the iii-cron trigger type."
+owner: "devrel"
+type: "how-to"
+---
+
+{/* TODO: write the how-to. Cover installing/enabling iii-cron, registering a function, binding it to a `cron` trigger with a schedule expression, verifying it fires, common schedule strings, timezone behaviour, what happens on overlap (skip vs queue), and how to manually invoke for testing. */}
diff --git a/docs/how-to/schedule-cron-task.mdx.skill.md b/docs/how-to/schedule-cron-task.mdx.skill.md
new file mode 100644
index 000000000..ac5116f23
--- /dev/null
+++ b/docs/how-to/schedule-cron-task.mdx.skill.md
@@ -0,0 +1,6 @@
+
+
+# Schedule a cron task
+
+
+{/* TODO: write the how-to. Cover installing/enabling iii-cron, registering a function, binding it to a `cron` trigger with a schedule expression, verifying it fires, common schedule strings, timezone behaviour, what happens on overlap (skip vs queue), and how to manually invoke for testing. */}
diff --git a/docs/how-to/use-trigger-conditions.mdx b/docs/how-to/use-trigger-conditions.mdx
new file mode 100644
index 000000000..f9e3573db
--- /dev/null
+++ b/docs/how-to/use-trigger-conditions.mdx
@@ -0,0 +1,8 @@
+---
+title: "Gate a trigger with a condition"
+description: "Run a small function before a trigger handler fires and skip the invocation when the condition returns falsey."
+owner: "devrel"
+type: "how-to"
+---
+
+{/* TODO: write the how-to. Cover registering a condition function (returns truthy/falsey from the same payload the handler would see), registering the trigger with `condition_function_id` set, verifying the handler runs only when the condition passes. Show one realistic example (e.g. HTTP trigger gated by an auth check) and call out when to prefer this over branching inside the handler. */}
diff --git a/docs/how-to/use-trigger-conditions.mdx.skill.md b/docs/how-to/use-trigger-conditions.mdx.skill.md
new file mode 100644
index 000000000..3d5451ae9
--- /dev/null
+++ b/docs/how-to/use-trigger-conditions.mdx.skill.md
@@ -0,0 +1,6 @@
+
+
+# Gate a trigger with a condition
+
+
+{/* TODO: write the how-to. Cover registering a condition function (returns truthy/falsey from the same payload the handler would see), registering the trigger with `condition_function_id` set, verifying the handler runs only when the condition passes. Show one realistic example (e.g. HTTP trigger gated by an auth check) and call out when to prefer this over branching inside the handler. */}
diff --git a/docs/index.mdx b/docs/index.mdx
index 848be1f0d..6cb0d25ea 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -24,20 +24,12 @@ stack.
## The Solution
-iii organizes software into three primitives: **Worker**, **Trigger**, **Function**. Something hosts
-work, something causes it, something does it. Every capability in every system can be built from
-these three things: queues, cron, streaming, sandboxing, observability, agents, business logic,
-devices, even frontend browser UIs.
+iii reduces that quadratic effort to zero. Adding two workers or two hundred is the same operation.
-## Quadratic to Zero
-
-The integration problem doesn't only manifest where two systems connect. It creates downstream
-friction in logging, debugging, and upgrading, and upstream friction that blocks growth initiatives,
-new verticals, and partnerships.
-
-iii reduces all of that effort to zero. Adding two workers or two hundred is the same operation. A
-new worker joins the system by opening a single WebSocket connection and becomes immediately
-available to every other worker at that moment.
+iii accomplishes this by organizing software into three primitives: **Worker**, **Trigger**,
+**Function**. Something hosts work, something causes it, something does it. Every capability in
+every system can be built from these three things: queues, cron, streaming, sandboxing,
+observability, agents, business logic, devices, even frontend browser UIs.
## Have a Need? Add a Worker
diff --git a/docs/index.mdx.skill.md b/docs/index.mdx.skill.md
index cc83bafca..78ecc6ba6 100644
--- a/docs/index.mdx.skill.md
+++ b/docs/index.mdx.skill.md
@@ -20,20 +20,12 @@ stack.
## The Solution
-iii organizes software into three primitives: **Worker**, **Trigger**, **Function**. Something hosts
-work, something causes it, something does it. Every capability in every system can be built from
-these three things: queues, cron, streaming, sandboxing, observability, agents, business logic,
-devices, even frontend browser UIs.
+iii reduces that quadratic effort to zero. Adding two workers or two hundred is the same operation.
-## Quadratic to Zero
-
-The integration problem doesn't only manifest where two systems connect. It creates downstream
-friction in logging, debugging, and upgrading, and upstream friction that blocks growth initiatives,
-new verticals, and partnerships.
-
-iii reduces all of that effort to zero. Adding two workers or two hundred is the same operation. A
-new worker joins the system by opening a single WebSocket connection and becomes immediately
-available to every other worker at that moment.
+iii accomplishes this by organizing software into three primitives: **Worker**, **Trigger**,
+**Function**. Something hosts work, something causes it, something does it. Every capability in
+every system can be built from these three things: queues, cron, streaming, sandboxing,
+observability, agents, business logic, devices, even frontend browser UIs.
## Have a Need? Add a Worker
diff --git a/docs/install.mdx b/docs/install.mdx
index 9b040dc6a..e6cf1a09c 100644
--- a/docs/install.mdx
+++ b/docs/install.mdx
@@ -30,7 +30,7 @@ iii --version
otherwise.
-{/* TODO: re-enable the "## 3. Install the VS Code Extension (Optional)" section once the iii-lsp extension is more thoroughly tested across VS Code, Cursor, Windsurf, and VSCodium. The Frame demo also needs `/images/lsp.mp4` to be captured and committed before the section is re-added. ## 3. Install the VS Code Extension (Optional) The iii Language Server extension gives your editor live awareness of your iii project: - **Completions**: function IDs, trigger types, payload properties, and known values like stream names and API paths. - **Hover documentation**: request and response schemas for any registered function. - **Diagnostics**: validates function IDs, required payload fields, trigger configs, cron expressions, and HTTP methods as you type. Works with TypeScript, Python, and Rust files. Requires a running iii engine. Open the Extensions panel and search for `iii-lsp`, or install from the terminal: code --install-extension iii-hq.iii-lsp cursor --install-extension iii-hq.iii-lsp windsurf --install-extension iii-hq.iii-lsp codium --install-extension iii-hq.iii-lsp */}
+{/* TODO: re-enable the "## 3. Install the VS Code Extension (Optional)" section once the iii-lsp extension is more thoroughly tested across VS Code, Cursor, Windsurf, and VSCodium. The Frame demo also needs `/images/lsp.mp4` to be captured and committed before the section is re-added. Before re-enabling, move the capability description (what completions/hover/diagnostics the extension provides) to an overview/explanation page for the extension and link to it from a single-sentence description here. ## 3. Install the VS Code Extension (Optional) The iii Language Server extension adds iii-aware editor support. See the extension overview for details. Open the Extensions panel and search for `iii-lsp`, or install from the terminal: code --install-extension iii-hq.iii-lsp cursor --install-extension iii-hq.iii-lsp windsurf --install-extension iii-hq.iii-lsp codium --install-extension iii-hq.iii-lsp */}
{/* TODO: re-add a "## 4. Add Agent Skills (Optional)" section with `npx skills add iii-hq/iii/skills` once the iii skills worker ships (owned by Sergio). */}
diff --git a/docs/install.mdx.skill.md b/docs/install.mdx.skill.md
index b835b8c90..aa185eae7 100644
--- a/docs/install.mdx.skill.md
+++ b/docs/install.mdx.skill.md
@@ -28,7 +28,7 @@ iii --version
otherwise.
-{/* TODO: re-enable the "## 3. Install the VS Code Extension (Optional)" section once the iii-lsp extension is more thoroughly tested across VS Code, Cursor, Windsurf, and VSCodium. The Frame demo also needs `/images/lsp.mp4` to be captured and committed before the section is re-added. ## 3. Install the VS Code Extension (Optional) The iii Language Server extension gives your editor live awareness of your iii project: - **Completions**: function IDs, trigger types, payload properties, and known values like stream names and API paths. - **Hover documentation**: request and response schemas for any registered function. - **Diagnostics**: validates function IDs, required payload fields, trigger configs, cron expressions, and HTTP methods as you type. Works with TypeScript, Python, and Rust files. Requires a running iii engine. Open the Extensions panel and search for `iii-lsp`, or install from the terminal: code --install-extension iii-hq.iii-lsp cursor --install-extension iii-hq.iii-lsp windsurf --install-extension iii-hq.iii-lsp codium --install-extension iii-hq.iii-lsp */}
+{/* TODO: re-enable the "## 3. Install the VS Code Extension (Optional)" section once the iii-lsp extension is more thoroughly tested across VS Code, Cursor, Windsurf, and VSCodium. The Frame demo also needs `/images/lsp.mp4` to be captured and committed before the section is re-added. Before re-enabling, move the capability description (what completions/hover/diagnostics the extension provides) to an overview/explanation page for the extension and link to it from a single-sentence description here. ## 3. Install the VS Code Extension (Optional) The iii Language Server extension adds iii-aware editor support. See the extension overview for details. Open the Extensions panel and search for `iii-lsp`, or install from the terminal: code --install-extension iii-hq.iii-lsp cursor --install-extension iii-hq.iii-lsp windsurf --install-extension iii-hq.iii-lsp codium --install-extension iii-hq.iii-lsp */}
{/* TODO: re-add a "## 4. Add Agent Skills (Optional)" section with `npx skills add iii-hq/iii/skills` once the iii skills worker ships (owned by Sergio). */}
diff --git a/docs/understanding-iii/channels.mdx b/docs/understanding-iii/channels.mdx
index 9050d7b59..72a024c71 100644
--- a/docs/understanding-iii/channels.mdx
+++ b/docs/understanding-iii/channels.mdx
@@ -11,17 +11,14 @@ Channels are stream pipes between iii workers. They let one function write bytes
function reads those bytes in real time, even when the functions run in different processes or
languages.
-Use channels when the data is too large, too binary, or too incremental for a normal JSON function
-payload.
-
## The model
-| Concept | What it does |
-| ------- | ------------ |
-| Channel | A WebSocket-backed pipe managed by the engine. |
-| Writer | Sends bytes or text messages into the pipe. |
-| Reader | Receives bytes or text messages from the pipe. |
-| Ref | A small serializable token passed through `trigger()` payloads. |
+| Concept | What it does |
+| ------- | --------------------------------------------------------------- |
+| Channel | A WebSocket-backed pipe managed by the engine. |
+| Writer | Sends bytes or text messages into the pipe. |
+| Reader | Receives bytes or text messages from the pipe. |
+| Ref | A small serializable token passed through `trigger()` payloads. |
The key idea is that refs travel through regular function calls, but the data itself travels over
the channel.
@@ -29,13 +26,14 @@ the channel.
## Why channels exist
Function invocations are JSON messages. That is perfect for structured events and command payloads,
-but it is the wrong shape for large files, media, stream responses, and long-running partial output.
+but it is the wrong approach for large files, media, streaming responses (agents, chats), and
+long-running partial output.
Channels split coordination from data transfer:
- A function call coordinates the work.
- A channel carries the stream.
-- The engine keeps tracing and routing tied to the same system.
+- The engine tracks tracing and routing.
## Runtime flow
@@ -70,23 +68,3 @@ When the writer closes, the reader receives the stream end. When a worker discon
connections close with it.
For bidirectional communication, create two channels: one for each direction.
-
-## Channels versus triggers
-
-Use a trigger when the payload is a discrete event or command. Use a channel when the payload is a
-stream.
-
-| Need | Use |
-| ---- | --- |
-| Invoke a handler with a small JSON object | Function trigger |
-| Serve a file download | Channel |
-| Process a large upload | Channel |
-| Send progress during a long task | Channel messages |
-| Dispatch background work | Queue trigger |
-
-## Related pages
-
-- [How to use channels](/using-iii/channels)
-- [Node SDK channels](/sdk-reference/node-sdk#channels)
-- [Python SDK channels](/sdk-reference/python-sdk#channels)
-- [Rust SDK channels](/sdk-reference/rust-sdk#channels)
diff --git a/docs/understanding-iii/channels.mdx.skill.md b/docs/understanding-iii/channels.mdx.skill.md
index 620623fdf..ab22e6880 100644
--- a/docs/understanding-iii/channels.mdx.skill.md
+++ b/docs/understanding-iii/channels.mdx.skill.md
@@ -9,17 +9,14 @@ Channels are stream pipes between iii workers. They let one function write bytes
function reads those bytes in real time, even when the functions run in different processes or
languages.
-Use channels when the data is too large, too binary, or too incremental for a normal JSON function
-payload.
-
## The model
-| Concept | What it does |
-| ------- | ------------ |
-| Channel | A WebSocket-backed pipe managed by the engine. |
-| Writer | Sends bytes or text messages into the pipe. |
-| Reader | Receives bytes or text messages from the pipe. |
-| Ref | A small serializable token passed through `trigger()` payloads. |
+| Concept | What it does |
+| ------- | --------------------------------------------------------------- |
+| Channel | A WebSocket-backed pipe managed by the engine. |
+| Writer | Sends bytes or text messages into the pipe. |
+| Reader | Receives bytes or text messages from the pipe. |
+| Ref | A small serializable token passed through `trigger()` payloads. |
The key idea is that refs travel through regular function calls, but the data itself travels over
the channel.
@@ -27,13 +24,14 @@ the channel.
## Why channels exist
Function invocations are JSON messages. That is perfect for structured events and command payloads,
-but it is the wrong shape for large files, media, stream responses, and long-running partial output.
+but it is the wrong approach for large files, media, streaming responses (agents, chats), and
+long-running partial output.
Channels split coordination from data transfer:
- A function call coordinates the work.
- A channel carries the stream.
-- The engine keeps tracing and routing tied to the same system.
+- The engine tracks tracing and routing.
## Runtime flow
@@ -68,23 +66,3 @@ When the writer closes, the reader receives the stream end. When a worker discon
connections close with it.
For bidirectional communication, create two channels: one for each direction.
-
-## Channels versus triggers
-
-Use a trigger when the payload is a discrete event or command. Use a channel when the payload is a
-stream.
-
-| Need | Use |
-| ---- | --- |
-| Invoke a handler with a small JSON object | Function trigger |
-| Serve a file download | Channel |
-| Process a large upload | Channel |
-| Send progress during a long task | Channel messages |
-| Dispatch background work | Queue trigger |
-
-## Related pages
-
-- [How to use channels](/using-iii/channels)
-- [Node SDK channels](/sdk-reference/node-sdk#channels)
-- [Python SDK channels](/sdk-reference/python-sdk#channels)
-- [Rust SDK channels](/sdk-reference/rust-sdk#channels)
diff --git a/docs/understanding-iii/functions.mdx b/docs/understanding-iii/functions.mdx
deleted file mode 100644
index e56060c88..000000000
--- a/docs/understanding-iii/functions.mdx
+++ /dev/null
@@ -1,46 +0,0 @@
----
-title: "Functions"
-description: "The named handlers a Worker exposes to the rest of the iii system."
-owner: "devrel"
-type: "explanation"
----
-
-## What a Function is
-
-A Function is a named handler inside a Worker. It takes a payload and returns a result. From the iii
-system's perspective, a Function is identified by its name and addressable across language and
-location boundaries. Callers do not know what Worker is providing the Function, what language the
-handler is written in, or where the Worker is running. The Engine routes each invocation to a Worker
-that currently provides the target Function.
-
-A Function has no fixed shape beyond payload-in / result-out. Some Functions are pure computation.
-Some perform side effects (state writes, HTTP calls, queue enqueues). Some are agentic, invoking
-other Functions in turn. The Engine does not distinguish: routing is the same for all of them.
-
-## Function identifiers
-
-Function identifiers use the `service::name` convention. The `service` segment groups related
-Functions together as a namespace, scope, or worker name. The `name` segment is the specific
-handler. Identifiers like `math::add`, `state::get`, and `http::serve` follow this convention.
-
-The convention is a recommendation, not a hard rule. Any string is a valid function ID at the engine
-level, but the `service::name` form makes the Function's intent obvious to readers and avoids
-collisions between unrelated Functions registered by different Workers.
-
-{/* TODO: Confirm if we still have restricted string prefixes */}
-
-## Direct invocation
-
-Registering a Function with `registerFunction()` makes it directly invokable through
-`worker.trigger()` from any connected Worker and through the `iii trigger` CLI command. No explicit
-Trigger registration is required for these two paths; they are the baseline call surface every
-registered Function gets. Other trigger sources (HTTP, cron, queue, state, stream) bind an explicit
-Trigger to the same `function_id`.
-
-## Multiple Triggers per Function
-
-A single Function can be the target of any number of Triggers. The same Function can be invoked by
-an HTTP request, a cron schedule, and a queue message at once, by registering three separate
-Triggers that share the same `function_id`. The function code does not change; only the trigger
-registrations differ. This is what lets a single business-logic Function answer to many event
-sources without per-source variants.
diff --git a/docs/understanding-iii/functions.mdx.skill.md b/docs/understanding-iii/functions.mdx.skill.md
deleted file mode 100644
index e7111ce1c..000000000
--- a/docs/understanding-iii/functions.mdx.skill.md
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-# Functions
-
-
-## What a Function is
-
-A Function is a named handler inside a Worker. It takes a payload and returns a result. From the iii
-system's perspective, a Function is identified by its name and addressable across language and
-location boundaries. Callers do not know what Worker is providing the Function, what language the
-handler is written in, or where the Worker is running. The Engine routes each invocation to a Worker
-that currently provides the target Function.
-
-A Function has no fixed shape beyond payload-in / result-out. Some Functions are pure computation.
-Some perform side effects (state writes, HTTP calls, queue enqueues). Some are agentic, invoking
-other Functions in turn. The Engine does not distinguish: routing is the same for all of them.
-
-## Function identifiers
-
-Function identifiers use the `service::name` convention. The `service` segment groups related
-Functions together as a namespace, scope, or worker name. The `name` segment is the specific
-handler. Identifiers like `math::add`, `state::get`, and `http::serve` follow this convention.
-
-The convention is a recommendation, not a hard rule. Any string is a valid function ID at the engine
-level, but the `service::name` form makes the Function's intent obvious to readers and avoids
-collisions between unrelated Functions registered by different Workers.
-
-{/* TODO: Confirm if we still have restricted string prefixes */}
-
-## Direct invocation
-
-Registering a Function with `registerFunction()` makes it directly invokable through
-`worker.trigger()` from any connected Worker and through the `iii trigger` CLI command. No explicit
-Trigger registration is required for these two paths; they are the baseline call surface every
-registered Function gets. Other trigger sources (HTTP, cron, queue, state, stream) bind an explicit
-Trigger to the same `function_id`.
-
-## Multiple Triggers per Function
-
-A single Function can be the target of any number of Triggers. The same Function can be invoked by
-an HTTP request, a cron schedule, and a queue message at once, by registering three separate
-Triggers that share the same `function_id`. The function code does not change; only the trigger
-registrations differ. This is what lets a single business-logic Function answer to many event
-sources without per-source variants.
diff --git a/docs/understanding-iii/index.mdx b/docs/understanding-iii/index.mdx
index 452b42103..6f4409b71 100644
--- a/docs/understanding-iii/index.mdx
+++ b/docs/understanding-iii/index.mdx
@@ -1,5 +1,5 @@
---
-title: "Overview"
+title: "Workers, Triggers, and Functions"
description:
"A walkthrough of the four pieces that make up every iii system (Workers, Triggers, Functions, and
the Engine), using the Quickstart tutorial as an example."
@@ -17,8 +17,9 @@ pieces, everything else in iii is a variation on a theme.
## The four pieces
-This is a brief recap of the four pieces. More details about their actual usage are in
-[Using iii / Workers](/using-iii/workers) and the rest of the "Using iii" section.
+This is a brief recap of the four pieces; the sections below expand each one with the Quickstart as
+an example. More details about their actual usage are in [Using iii / Workers](/using-iii/workers)
+and the rest of the "Using iii" section.
### Worker
@@ -76,6 +77,31 @@ to `math-worker`.
## Workers
+Workers are what actually do things in an iii system. Every category of capability is built as a
+Worker: queues, scheduling, sandboxing, observability, agents, business logic, devices, and even
+code executing in a browser.
+
+Specifically, a Worker is a process that connects to the Engine over WebSocket and announces a set
+of Functions it can run and Triggers to register. Once connected, those Functions are invocable from
+anywhere in the system and those Triggers will respond to their events without per-pair integration
+code between the caller and the Worker.
+
+The Worker concept is intentionally narrow. A Worker is not a microservice, a job runner, or a
+sidecar. It is a participant in the Engine's live registry that contributes Functions and Triggers.
+Whether the Worker is a long-lived process serving thousands of invocations per second or a
+short-lived process that connects, registers, runs once, and shuts down, the Engine treats it the
+same.
+
+### Worker isolation
+
+Workers are intended and designed to be independent processes. One Worker crashing does not affect
+others. The Engine connects to each Worker over a separate WebSocket and routes invocations only to
+Workers that are currently connected. A crash, restart, or network partition affecting one Worker
+does not propagate to the others. The crashed Worker's Functions and Triggers drop out of the
+routing table on disconnect, and every other Worker keeps serving.
+
+### In the Quickstart
+
Both Workers in the Quickstart fulfill the same contract: open a WebSocket connection to the Engine.
Once connected they can register Functions, register Triggers, and `trigger()` other Functions. A
Worker will typically do at least one of these things but ultimately isn't required to do any of
@@ -90,15 +116,78 @@ implement in any language that can use a WebSocket and JSON, and the Engine trea
same regardless of how it was built or where it runs.
- See [Workers](/understanding-iii/workers) for process isolation, and
- [Creating Workers / Workers](/creating-workers/workers#worker-lifecycle-states) for the connection
- lifecycle.
+ For the connection lifecycle from worker code, see [Creating Workers /
+ Workers](/creating-workers/workers#worker-lifecycle-states).
## Triggers
-A Trigger has three parts: a type, a configuration, and the function ID it invokes. The Quickstart
-tutorial invokes Functions with Triggers in three different ways:
+A Trigger is a binding that tells iii when to invoke a Function. The Trigger declares a type (the
+kind of event that causes it to fire), a configuration (the per-type details, like an HTTP path or a
+cron expression), and the function ID it invokes. When the corresponding event happens, the Trigger
+fires and the Engine routes the invocation to a Worker that provides the Function. HTTP requests,
+cron schedules, queue messages, state changes, log events, and stream events all become Function
+invocations through Triggers.
+
+### Trigger types
+
+
+ `worker.trigger()` and the `iii trigger` CLI command can invoke any registered Function via its
+ `function_id` (see [Direct invocation](#direct-invocation) below). The trigger types described
+ here are how Functions get bound to other event sources (HTTP requests, cron schedules, queue
+ messages, etc.). Workers can define their own trigger types.
+
+
+Trigger types come from connected Workers. A Worker that can source events declares one or more
+trigger types alongside their configuration schemas. The iii-http Worker provides the `http` trigger
+type. The iii-cron Worker provides the `cron` trigger type. The iii-state Worker provides the
+`state` trigger type. A Trigger of a given type can only be registered while a Worker advertising
+that type is connected, because that Worker is what produces the events that fire it.
+
+### Trigger components
+
+A Trigger has three parts: a `type` (the kind of event, like `http` or `cron`), a `config` (the
+per-type details, like an HTTP path or a cron expression), and a `function_id` (the Function to
+invoke). Together they tell iii what event to listen for, how to listen, and what to call when the
+event happens.
+
+A Trigger can also specify an optional `condition_function_id` that runs before the handler. When
+the Trigger fires, the Engine invokes the condition function with the same payload the handler would
+receive. If the condition returns a truthy value, the handler runs; if not, the invocation is
+skipped. Since Triggers are concerned with "when to do" and Functions are concerned with "what to
+do", conditional functions preserve that separation: the Function stays focused on its work instead
+of accumulating per-Trigger guards.
+
+### Trigger pipeline
+
+When a Trigger fires, the Engine looks up its `function_id` in the live registry, finds a Worker
+that currently provides the Function, and dispatches the invocation. The function handler sees the
+payload alone, never the source of the Trigger or the type of event that fired it.
+
+### Trigger Actions
+
+Function invocation can be controlled via Trigger Actions. The default, synchronous mode blocks
+until the Function returns its result or the configured timeout fires. The fire-and-forget mode
+(`TriggerAction.Void`) returns immediately, scheduling the Function to run without waiting for a
+result. Synchronous invocations are appropriate when the caller needs the value the Function
+returns. Fire-and-forget is for side-effect work where the caller does not need to wait.
+
+
+ Workers can also define their own `TriggerAction`s. The iii-queue Worker provides
+ `TriggerAction.Enqueue({queue})`, which routes the invocation through a named queue with retries.
+ See iii-queue for the queue mechanics.
+
+
+### Trigger lifecycle
+
+Triggers move through four states. `registered` means the Trigger has been declared with the Engine.
+`active` means the Trigger is currently listening for its event. `invoked` means an event has fired
+the Trigger. `unregistered` means the Trigger has been removed. When the Worker that owns a Trigger
+disconnects, all of its Triggers are unregistered automatically along with its Functions.
+
+### In the Quickstart
+
+The Quickstart tutorial invokes Functions with Triggers in three different ways:
1. The CLI `iii trigger math::add a=2 b=3` is a Trigger fired by the CLI itself. The Engine routes
the invocation to whatever Worker provides `math::add`.
@@ -123,13 +212,48 @@ tutorial invokes Functions with Triggers in three different ways:
One Function can have many Triggers. The same Function could be invoked by a cron schedule, a queue
message, and a direct CLI call.
-
- See [Triggers](/understanding-iii/triggers) for trigger types, invocation modes, the trigger
- pipeline, the trigger lifecycle, and trigger conditions.
-
-
## Functions
+A Function is a named handler inside a Worker. It takes a payload and returns a result. From the iii
+system's perspective, a Function is identified by its name and addressable across language and
+location boundaries. Callers do not know what Worker is providing the Function, what language the
+handler is written in, or where the Worker is running. The Engine routes each invocation to a Worker
+that currently provides the target Function.
+
+A Function has no fixed shape beyond payload-in / result-out. Some Functions are pure computation.
+Some perform side effects (state writes, HTTP calls, queue enqueues). Some are agentic, invoking
+other Functions in turn. The Engine does not distinguish: routing is the same for all of them.
+
+### Function identifiers
+
+Function identifiers use the `service::name` convention. The `service` segment groups related
+Functions together as a namespace, scope, or worker name. The `name` segment is the specific
+handler. Identifiers like `math::add`, `state::get`, and `http::serve` follow this convention.
+
+The convention is a recommendation, not a hard rule. Any string is a valid function ID at the engine
+level, but the `service::name` form makes the Function's intent obvious to readers and avoids
+collisions between unrelated Functions registered by different Workers.
+
+{/* TODO: Confirm if we still have restricted string prefixes */}
+
+### Direct invocation
+
+Registering a Function with `registerFunction()` makes it directly invokable through
+`worker.trigger()` from any connected Worker and through the `iii trigger` CLI command. No explicit
+Trigger registration is required for these two paths; they are the baseline call surface every
+registered Function gets. Other trigger sources (HTTP, cron, queue, state, stream) bind an explicit
+Trigger to the same `function_id`.
+
+### Multiple Triggers per Function
+
+A single Function can be the target of any number of Triggers. The same Function can be invoked by
+an HTTP request, a cron schedule, and a queue message at once, by registering three separate
+Triggers that share the same `function_id`. The function code does not change; only the trigger
+registrations differ. This is what lets a single business-logic Function answer to many event
+sources without per-source variants.
+
+### In the Quickstart
+
`math::add` and `math::add_two_numbers` are Functions. Their identifiers follow `service::name`. The
`math` namespace groups related Functions together, and the name identifies the specific handler.
However grouping is arbitrary, and while we recommend using a structured `path::to::functions` there
@@ -142,13 +266,6 @@ instance currently provides that Function.
Functions are defined synchronously but can be invoked asynchronously due to the decoupling between
Triggers and Functions.
-
- See [Functions](/understanding-iii/functions) for identifier conventions, direct invocation, and
- multiple Triggers per Function. See
- [Triggers / Invocation modes](/understanding-iii/triggers#invocation-modes) for sync and
- fire-and-forget behavior.
-
-
## The Engine
The Engine is a single process that holds the registry of every connected Worker and every
diff --git a/docs/understanding-iii/index.mdx.skill.md b/docs/understanding-iii/index.mdx.skill.md
index af34ae3c6..c6f1a4d9b 100644
--- a/docs/understanding-iii/index.mdx.skill.md
+++ b/docs/understanding-iii/index.mdx.skill.md
@@ -1,6 +1,6 @@
-# Overview
+# Workers, Triggers, and Functions
Unix gave processes a single interface. React gave components a single interface. iii gives every
@@ -13,8 +13,9 @@ pieces, everything else in iii is a variation on a theme.
## The four pieces
-This is a brief recap of the four pieces. More details about their actual usage are in
-[Using iii / Workers](/using-iii/workers) and the rest of the "Using iii" section.
+This is a brief recap of the four pieces; the sections below expand each one with the Quickstart as
+an example. More details about their actual usage are in [Using iii / Workers](/using-iii/workers)
+and the rest of the "Using iii" section.
### Worker
@@ -72,6 +73,31 @@ to `math-worker`.
## Workers
+Workers are what actually do things in an iii system. Every category of capability is built as a
+Worker: queues, scheduling, sandboxing, observability, agents, business logic, devices, and even
+code executing in a browser.
+
+Specifically, a Worker is a process that connects to the Engine over WebSocket and announces a set
+of Functions it can run and Triggers to register. Once connected, those Functions are invocable from
+anywhere in the system and those Triggers will respond to their events without per-pair integration
+code between the caller and the Worker.
+
+The Worker concept is intentionally narrow. A Worker is not a microservice, a job runner, or a
+sidecar. It is a participant in the Engine's live registry that contributes Functions and Triggers.
+Whether the Worker is a long-lived process serving thousands of invocations per second or a
+short-lived process that connects, registers, runs once, and shuts down, the Engine treats it the
+same.
+
+### Worker isolation
+
+Workers are intended and designed to be independent processes. One Worker crashing does not affect
+others. The Engine connects to each Worker over a separate WebSocket and routes invocations only to
+Workers that are currently connected. A crash, restart, or network partition affecting one Worker
+does not propagate to the others. The crashed Worker's Functions and Triggers drop out of the
+routing table on disconnect, and every other Worker keeps serving.
+
+### In the Quickstart
+
Both Workers in the Quickstart fulfill the same contract: open a WebSocket connection to the Engine.
Once connected they can register Functions, register Triggers, and `trigger()` other Functions. A
Worker will typically do at least one of these things but ultimately isn't required to do any of
@@ -86,15 +112,78 @@ implement in any language that can use a WebSocket and JSON, and the Engine trea
same regardless of how it was built or where it runs.
- See [Workers](/understanding-iii/workers) for process isolation, and
- [Creating Workers / Workers](/creating-workers/workers#worker-lifecycle-states) for the connection
- lifecycle.
+ For the connection lifecycle from worker code, see [Creating Workers /
+ Workers](/creating-workers/workers#worker-lifecycle-states).
## Triggers
-A Trigger has three parts: a type, a configuration, and the function ID it invokes. The Quickstart
-tutorial invokes Functions with Triggers in three different ways:
+A Trigger is a binding that tells iii when to invoke a Function. The Trigger declares a type (the
+kind of event that causes it to fire), a configuration (the per-type details, like an HTTP path or a
+cron expression), and the function ID it invokes. When the corresponding event happens, the Trigger
+fires and the Engine routes the invocation to a Worker that provides the Function. HTTP requests,
+cron schedules, queue messages, state changes, log events, and stream events all become Function
+invocations through Triggers.
+
+### Trigger types
+
+
+ `worker.trigger()` and the `iii trigger` CLI command can invoke any registered Function via its
+ `function_id` (see [Direct invocation](#direct-invocation) below). The trigger types described
+ here are how Functions get bound to other event sources (HTTP requests, cron schedules, queue
+ messages, etc.). Workers can define their own trigger types.
+
+
+Trigger types come from connected Workers. A Worker that can source events declares one or more
+trigger types alongside their configuration schemas. The iii-http Worker provides the `http` trigger
+type. The iii-cron Worker provides the `cron` trigger type. The iii-state Worker provides the
+`state` trigger type. A Trigger of a given type can only be registered while a Worker advertising
+that type is connected, because that Worker is what produces the events that fire it.
+
+### Trigger components
+
+A Trigger has three parts: a `type` (the kind of event, like `http` or `cron`), a `config` (the
+per-type details, like an HTTP path or a cron expression), and a `function_id` (the Function to
+invoke). Together they tell iii what event to listen for, how to listen, and what to call when the
+event happens.
+
+A Trigger can also specify an optional `condition_function_id` that runs before the handler. When
+the Trigger fires, the Engine invokes the condition function with the same payload the handler would
+receive. If the condition returns a truthy value, the handler runs; if not, the invocation is
+skipped. Since Triggers are concerned with "when to do" and Functions are concerned with "what to
+do", conditional functions preserve that separation: the Function stays focused on its work instead
+of accumulating per-Trigger guards.
+
+### Trigger pipeline
+
+When a Trigger fires, the Engine looks up its `function_id` in the live registry, finds a Worker
+that currently provides the Function, and dispatches the invocation. The function handler sees the
+payload alone, never the source of the Trigger or the type of event that fired it.
+
+### Trigger Actions
+
+Function invocation can be controlled via Trigger Actions. The default, synchronous mode blocks
+until the Function returns its result or the configured timeout fires. The fire-and-forget mode
+(`TriggerAction.Void`) returns immediately, scheduling the Function to run without waiting for a
+result. Synchronous invocations are appropriate when the caller needs the value the Function
+returns. Fire-and-forget is for side-effect work where the caller does not need to wait.
+
+
+ Workers can also define their own `TriggerAction`s. The iii-queue Worker provides
+ `TriggerAction.Enqueue({queue})`, which routes the invocation through a named queue with retries.
+ See iii-queue for the queue mechanics.
+
+
+### Trigger lifecycle
+
+Triggers move through four states. `registered` means the Trigger has been declared with the Engine.
+`active` means the Trigger is currently listening for its event. `invoked` means an event has fired
+the Trigger. `unregistered` means the Trigger has been removed. When the Worker that owns a Trigger
+disconnects, all of its Triggers are unregistered automatically along with its Functions.
+
+### In the Quickstart
+
+The Quickstart tutorial invokes Functions with Triggers in three different ways:
1. The CLI `iii trigger math::add a=2 b=3` is a Trigger fired by the CLI itself. The Engine routes
the invocation to whatever Worker provides `math::add`.
@@ -119,13 +208,48 @@ tutorial invokes Functions with Triggers in three different ways:
One Function can have many Triggers. The same Function could be invoked by a cron schedule, a queue
message, and a direct CLI call.
-
- See [Triggers](/understanding-iii/triggers) for trigger types, invocation modes, the trigger
- pipeline, the trigger lifecycle, and trigger conditions.
-
-
## Functions
+A Function is a named handler inside a Worker. It takes a payload and returns a result. From the iii
+system's perspective, a Function is identified by its name and addressable across language and
+location boundaries. Callers do not know what Worker is providing the Function, what language the
+handler is written in, or where the Worker is running. The Engine routes each invocation to a Worker
+that currently provides the target Function.
+
+A Function has no fixed shape beyond payload-in / result-out. Some Functions are pure computation.
+Some perform side effects (state writes, HTTP calls, queue enqueues). Some are agentic, invoking
+other Functions in turn. The Engine does not distinguish: routing is the same for all of them.
+
+### Function identifiers
+
+Function identifiers use the `service::name` convention. The `service` segment groups related
+Functions together as a namespace, scope, or worker name. The `name` segment is the specific
+handler. Identifiers like `math::add`, `state::get`, and `http::serve` follow this convention.
+
+The convention is a recommendation, not a hard rule. Any string is a valid function ID at the engine
+level, but the `service::name` form makes the Function's intent obvious to readers and avoids
+collisions between unrelated Functions registered by different Workers.
+
+{/* TODO: Confirm if we still have restricted string prefixes */}
+
+### Direct invocation
+
+Registering a Function with `registerFunction()` makes it directly invokable through
+`worker.trigger()` from any connected Worker and through the `iii trigger` CLI command. No explicit
+Trigger registration is required for these two paths; they are the baseline call surface every
+registered Function gets. Other trigger sources (HTTP, cron, queue, state, stream) bind an explicit
+Trigger to the same `function_id`.
+
+### Multiple Triggers per Function
+
+A single Function can be the target of any number of Triggers. The same Function can be invoked by
+an HTTP request, a cron schedule, and a queue message at once, by registering three separate
+Triggers that share the same `function_id`. The function code does not change; only the trigger
+registrations differ. This is what lets a single business-logic Function answer to many event
+sources without per-source variants.
+
+### In the Quickstart
+
`math::add` and `math::add_two_numbers` are Functions. Their identifiers follow `service::name`. The
`math` namespace groups related Functions together, and the name identifies the specific handler.
However grouping is arbitrary, and while we recommend using a structured `path::to::functions` there
@@ -138,13 +262,6 @@ instance currently provides that Function.
Functions are defined synchronously but can be invoked asynchronously due to the decoupling between
Triggers and Functions.
-
- See [Functions](/understanding-iii/functions) for identifier conventions, direct invocation, and
- multiple Triggers per Function. See
- [Triggers / Invocation modes](/understanding-iii/triggers#invocation-modes) for sync and
- fire-and-forget behavior.
-
-
## The Engine
The Engine is a single process that holds the registry of every connected Worker and every
diff --git a/docs/understanding-iii/triggers.mdx b/docs/understanding-iii/triggers.mdx
deleted file mode 100644
index 7bc2d968a..000000000
--- a/docs/understanding-iii/triggers.mdx
+++ /dev/null
@@ -1,84 +0,0 @@
----
-title: "Triggers"
-description: "How Functions get invoked in an iii system."
-owner: "devrel"
-type: "explanation"
----
-
-## What a Trigger is
-
-A Trigger is a binding that tells iii when to invoke a Function. The Trigger declares a type (the
-kind of event that causes it to fire), a configuration (the per-type details, like an HTTP path or a
-cron expression), and the function ID it invokes. When the corresponding event happens, the Trigger
-fires and the Engine routes the invocation to a Worker that provides the Function. HTTP requests,
-cron schedules, queue messages, state changes, log events, and stream events all become Function
-invocations through Triggers.
-
-## Trigger types
-
-
- `worker.trigger()` and the `iii trigger` CLI command can invoke any registered Function via its
- `function_id` (see [Functions / Direct
- invocation](/understanding-iii/functions#direct-invocation)). The trigger types described below
- are how Functions get bound to other event sources (HTTP requests, cron schedules, queue messages,
- etc.). Workers can define their own trigger types.
-
-
-Trigger types come from connected Workers. A Worker that can source events declares one or more
-trigger types alongside their configuration schemas. The iii-http Worker provides the `http` trigger
-type. The iii-cron Worker provides the `cron` trigger type. The iii-state Worker provides the
-`state` trigger type. A Trigger of a given type can only be registered while a Worker advertising
-that type is connected, because that Worker is what produces the events that fire it.
-
-## Trigger components
-
-A Trigger has three parts: a `type` (the kind of event, like `http` or `cron`), a `config` (the
-per-type details, like an HTTP path or a cron expression), and a `function_id` (the Function to
-invoke). Together they tell iii what event to listen for, how to listen, and what to call when the
-event happens.
-
-### Conditional triggers
-
-A Trigger can also specify an optional `condition_function_id`, which can be any gating function
-that returns truthy/falsey result. If specified the Engine invokes the condition function first and
-the `function_id`'s handler will only execute if the condition function returns a truthy value.
-
-Since Triggers are concerned with "when to do" and Functions are concerned with "what to do" these
-conditional functions preserve that separation: the Function stays focused on its work instead of
-accumulating per-Trigger guards that the developer has to link specific invocation paths. Using
-condition functions correctly helps maintain Worker and Function composability.
-
-## Trigger pipeline
-
-When a Trigger fires, the Engine looks up its `function_id` in the live registry, finds a Worker
-that currently provides the Function, and dispatches the invocation. The function handler sees the
-payload alone, never the source of the Trigger or the type of event that fired it.
-
-## Trigger Actions
-
-Functions invocation can be controlled via Trigger Actions. The default, synchronous mode blocks
-until the Function returns its result or the configured timeout fires. The fire-and-forget mode
-(`TriggerAction.Void`) returns immediately, scheduling the Function to run without waiting for a
-result. Synchronous invocations are appropriate when the caller needs the value the Function
-returns. Fire-and-forget is for side-effect work where the caller does not need to wait.
-
-
- Workers can also define their own `TriggerAction`s. The iii-queue Worker provides
- `TriggerAction.Enqueue({queue})`, which routes the invocation through a named queue with retries.
- See iii-queue for the queue mechanics.
-
-
-## Trigger lifecycle
-
-Triggers move through four states. `registered` means the Trigger has been declared with the Engine.
-`active` means the Trigger is currently listening for its event. `invoked` means an event has fired
-the Trigger. `unregistered` means the Trigger has been removed. When the Worker that owns a Trigger
-disconnects, all of its Triggers are unregistered automatically along with its Functions.
-
-## Trigger conditions
-
-A Trigger can carry an optional `condition_function_id` that runs before the handler. When the
-Trigger fires, the Engine invokes the condition function with the same payload the handler would
-receive. If the condition returns a truthy value, the handler runs; if not, the invocation is
-skipped. Use this when the same event source should sometimes fire the Function and sometimes not,
-without splitting the Trigger into separate event-source registrations.
diff --git a/docs/understanding-iii/triggers.mdx.skill.md b/docs/understanding-iii/triggers.mdx.skill.md
deleted file mode 100644
index 3446d7d53..000000000
--- a/docs/understanding-iii/triggers.mdx.skill.md
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-# Triggers
-
-
-## What a Trigger is
-
-A Trigger is a binding that tells iii when to invoke a Function. The Trigger declares a type (the
-kind of event that causes it to fire), a configuration (the per-type details, like an HTTP path or a
-cron expression), and the function ID it invokes. When the corresponding event happens, the Trigger
-fires and the Engine routes the invocation to a Worker that provides the Function. HTTP requests,
-cron schedules, queue messages, state changes, log events, and stream events all become Function
-invocations through Triggers.
-
-## Trigger types
-
-
- `worker.trigger()` and the `iii trigger` CLI command can invoke any registered Function via its
- `function_id` (see [Functions / Direct
- invocation](/understanding-iii/functions#direct-invocation)). The trigger types described below
- are how Functions get bound to other event sources (HTTP requests, cron schedules, queue messages,
- etc.). Workers can define their own trigger types.
-
-
-Trigger types come from connected Workers. A Worker that can source events declares one or more
-trigger types alongside their configuration schemas. The iii-http Worker provides the `http` trigger
-type. The iii-cron Worker provides the `cron` trigger type. The iii-state Worker provides the
-`state` trigger type. A Trigger of a given type can only be registered while a Worker advertising
-that type is connected, because that Worker is what produces the events that fire it.
-
-## Trigger components
-
-A Trigger has three parts: a `type` (the kind of event, like `http` or `cron`), a `config` (the
-per-type details, like an HTTP path or a cron expression), and a `function_id` (the Function to
-invoke). Together they tell iii what event to listen for, how to listen, and what to call when the
-event happens.
-
-### Conditional triggers
-
-A Trigger can also specify an optional `condition_function_id`, which can be any gating function
-that returns truthy/falsey result. If specified the Engine invokes the condition function first and
-the `function_id`'s handler will only execute if the condition function returns a truthy value.
-
-Since Triggers are concerned with "when to do" and Functions are concerned with "what to do" these
-conditional functions preserve that separation: the Function stays focused on its work instead of
-accumulating per-Trigger guards that the developer has to link specific invocation paths. Using
-condition functions correctly helps maintain Worker and Function composability.
-
-## Trigger pipeline
-
-When a Trigger fires, the Engine looks up its `function_id` in the live registry, finds a Worker
-that currently provides the Function, and dispatches the invocation. The function handler sees the
-payload alone, never the source of the Trigger or the type of event that fired it.
-
-## Trigger Actions
-
-Functions invocation can be controlled via Trigger Actions. The default, synchronous mode blocks
-until the Function returns its result or the configured timeout fires. The fire-and-forget mode
-(`TriggerAction.Void`) returns immediately, scheduling the Function to run without waiting for a
-result. Synchronous invocations are appropriate when the caller needs the value the Function
-returns. Fire-and-forget is for side-effect work where the caller does not need to wait.
-
-
- Workers can also define their own `TriggerAction`s. The iii-queue Worker provides
- `TriggerAction.Enqueue({queue})`, which routes the invocation through a named queue with retries.
- See iii-queue for the queue mechanics.
-
-
-## Trigger lifecycle
-
-Triggers move through four states. `registered` means the Trigger has been declared with the Engine.
-`active` means the Trigger is currently listening for its event. `invoked` means an event has fired
-the Trigger. `unregistered` means the Trigger has been removed. When the Worker that owns a Trigger
-disconnects, all of its Triggers are unregistered automatically along with its Functions.
-
-## Trigger conditions
-
-A Trigger can carry an optional `condition_function_id` that runs before the handler. When the
-Trigger fires, the Engine invokes the condition function with the same payload the handler would
-receive. If the condition returns a truthy value, the handler runs; if not, the invocation is
-skipped. Use this when the same event source should sometimes fire the Function and sometimes not,
-without splitting the Trigger into separate event-source registrations.
diff --git a/docs/understanding-iii/workers.mdx b/docs/understanding-iii/workers.mdx
deleted file mode 100644
index d21647146..000000000
--- a/docs/understanding-iii/workers.mdx
+++ /dev/null
@@ -1,33 +0,0 @@
----
-title: "Workers"
-description: "The processes that do the actual work in an iii system."
-owner: "devrel"
-type: "explanation"
----
-
-{/* TODO: Consider combining with Functions and Triggers into one doc. */}
-
-## What a Worker is
-
-Workers are what actually do things in an iii system. Every category of capability is built as a
-Worker: queues, scheduling, sandboxing, observability, agents, business logic, devices, and even
-code executing in a browser.
-
-Specifically, a Worker is a process that connects to the Engine over WebSocket and announces a set
-of Functions it can run and Triggers to register. Once connected, those Functions are invocable from
-anywhere in the system and those Triggers will respond to their events without per-pair integration
-code between the caller and the Worker.
-
-The Worker concept is intentionally narrow. A Worker is not a microservice, a job runner, or a
-sidecar. It is a participant in the Engine's live registry that contributes Functions and Triggers.
-Whether the Worker is a long-lived process serving thousands of invocations per second or a
-short-lived process that connects, registers, runs once, and shuts down, the Engine treats it the
-same.
-
-## Worker isolation
-
-Workers are intended and designed to be independent processes. One Worker crashing does not affect
-others. The Engine connects to each Worker over a separate WebSocket and routes invocations only to
-Workers that are currently connected. A crash, restart, or network partition affecting one Worker
-does not propagate to the others. The crashed Worker's Functions and Triggers drop out of the
-routing table on disconnect, and every other Worker keeps serving.
diff --git a/docs/understanding-iii/workers.mdx.skill.md b/docs/understanding-iii/workers.mdx.skill.md
deleted file mode 100644
index c9ec9db25..000000000
--- a/docs/understanding-iii/workers.mdx.skill.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-# Workers
-
-
-{/* TODO: Consider combining with Functions and Triggers into one doc. */}
-
-## What a Worker is
-
-Workers are what actually do things in an iii system. Every category of capability is built as a
-Worker: queues, scheduling, sandboxing, observability, agents, business logic, devices, and even
-code executing in a browser.
-
-Specifically, a Worker is a process that connects to the Engine over WebSocket and announces a set
-of Functions it can run and Triggers to register. Once connected, those Functions are invocable from
-anywhere in the system and those Triggers will respond to their events without per-pair integration
-code between the caller and the Worker.
-
-The Worker concept is intentionally narrow. A Worker is not a microservice, a job runner, or a
-sidecar. It is a participant in the Engine's live registry that contributes Functions and Triggers.
-Whether the Worker is a long-lived process serving thousands of invocations per second or a
-short-lived process that connects, registers, runs once, and shuts down, the Engine treats it the
-same.
-
-## Worker isolation
-
-Workers are intended and designed to be independent processes. One Worker crashing does not affect
-others. The Engine connects to each Worker over a separate WebSocket and routes invocations only to
-Workers that are currently connected. A crash, restart, or network partition affecting one Worker
-does not propagate to the others. The crashed Worker's Functions and Triggers drop out of the
-routing table on disconnect, and every other Worker keeps serving.
diff --git a/docs/using-iii/channels.mdx b/docs/using-iii/channels.mdx
deleted file mode 100644
index 8f4653660..000000000
--- a/docs/using-iii/channels.mdx
+++ /dev/null
@@ -1,218 +0,0 @@
----
-title: "Channels"
-description: "Stream large or binary payloads between iii workers."
-owner: "devrel"
-type: "how-to"
----
-
-## Goal
-
-Stream a large or binary payload from one function to another without putting the data itself in a
-JSON function payload.
-
-Use a normal function invocation when the payload is small JSON and can be handled as one request.
-Use a channel when the payload is large, binary, or naturally stream-shaped.
-
-Good fits for channels:
-
-- File uploads and downloads.
-- Images, audio, video, PDFs, and datasets.
-- Progress updates during long-running work.
-- Producer and consumer pipelines where the data should move as a stream.
-
-
- For the underlying model, see [Channels architecture](/understanding-iii/channels).
-
-
-## Steps
-
-### 1. Create a channel
-
-A channel has two local stream objects and two serializable refs:
-
-- `writer`: local writable stream.
-- `reader`: local readable stream.
-- `writerRef`: serializable token for the writer end.
-- `readerRef`: serializable token for the reader end.
-
-
-
- ```typescript
- const channel = await iii.createChannel();
-
- // channel.writer
- // channel.reader
- // channel.writerRef
- // channel.readerRef
- ```
-
-
- ```python
- channel = iii_client.create_channel()
-
- # channel.writer
- # channel.reader
- # channel.writer_ref
- # channel.reader_ref
- ```
-
-
- ```rust
- let channel = iii.create_channel(None).await?;
-
- // channel.writer
- // channel.reader
- // channel.writer_ref
- // channel.reader_ref
- ```
-
-
-
-### 2. Write to the channel
-
-Write the stream payload to the local writer and close it when you are done.
-
-
-
- ```typescript
- const channel = await iii.createChannel();
-
- channel.writer.stream.end(Buffer.from("file contents"));
- ```
-
-
- ```python
- channel = await iii_client.create_channel_async()
-
- await channel.writer.write(b"file contents")
- await channel.writer.close_async()
- ```
-
-
- ```rust
- let channel = iii.create_channel(None).await?;
-
- channel.writer.write(b"file contents").await?;
- channel.writer.close().await?;
- ```
-
-
-
-### 3. Pass the reader ref to another function
-
-Pass the `readerRef` / `reader_ref` as part of a normal function invocation. The receiving function
-uses that ref to read from the channel.
-
-
-
- ```typescript
- const result = await iii.trigger({
- function_id: "files::process",
- payload: {
- filename: "report.csv",
- reader: channel.readerRef,
- },
- });
- ```
-
-
- ```python
- result = await iii_client.trigger_async({
- "function_id": "files::process",
- "payload": {
- "filename": "report.csv",
- "reader": channel.reader_ref.model_dump(),
- },
- })
- ```
-
-
- ```rust
- use iii_sdk::TriggerRequest;
- use serde_json::json;
-
- let result = iii
- .trigger(TriggerRequest {
- function_id: "files::process".to_string(),
- payload: json!({
- "filename": "report.csv",
- "reader": channel.reader_ref,
- }),
- action: None,
- timeout_ms: None,
- })
- .await?;
- ```
-
-
-
-### 4. Read from the channel
-
-Node and Python deserialize channel refs into live channel objects before your handler runs. Rust
-receives the ref in JSON and reconstructs the reader with `ChannelReader::new(...)`.
-
-
-
- ```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 };
- });
- ```
-
-
- ```python
- async def process_file(input: dict) -> dict:
- reader = input["reader"]
- chunks = []
-
- async for chunk in reader:
- chunks.append(chunk)
-
- return {"bytes": sum(len(chunk) for chunk in chunks)}
-
- worker.register_function("files::process", process_file)
- ```
-
-
- ```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(|(key, ref_)| key == "reader" && matches!(ref_.direction, ChannelDirection::Read))
- .map(|(_, ref_)| ref_.clone())
- .ok_or_else(|| IIIError::Handler("missing reader channel ref".to_string()))?;
-
- let reader = ChannelReader::new(iii.address(), &reader_ref);
- let mut bytes = 0;
-
- while let Some(chunk) = reader.next_binary().await? {
- bytes += chunk.len();
- }
-
- Ok(json!({ "bytes": bytes }))
- ```
-
-
-
-## Result
-
-The caller passes only a small ref through `trigger()`. The stream payload travels over the channel,
-and the receiving function reads it incrementally.
-
-## Related pages
-
-- [Channels architecture](/understanding-iii/channels)
-- [Node SDK channels](/sdk-reference/node-sdk#channels)
-- [Python SDK channels](/sdk-reference/python-sdk#channels)
-- [Rust SDK channels](/sdk-reference/rust-sdk#channels)
diff --git a/docs/using-iii/channels.mdx.skill.md b/docs/using-iii/channels.mdx.skill.md
deleted file mode 100644
index 4c9b60dca..000000000
--- a/docs/using-iii/channels.mdx.skill.md
+++ /dev/null
@@ -1,214 +0,0 @@
-
-
-
-## Goal
-
-Stream a large or binary payload from one function to another without putting the data itself in a
-JSON function payload.
-
-Use a normal function invocation when the payload is small JSON and can be handled as one request.
-Use a channel when the payload is large, binary, or naturally stream-shaped.
-
-Good fits for channels:
-
-- File uploads and downloads.
-- Images, audio, video, PDFs, and datasets.
-- Progress updates during long-running work.
-- Producer and consumer pipelines where the data should move as a stream.
-
-
- For the underlying model, see [Channels architecture](/understanding-iii/channels).
-
-
-## Steps
-
-### 1. Create a channel
-
-A channel has two local stream objects and two serializable refs:
-
-- `writer`: local writable stream.
-- `reader`: local readable stream.
-- `writerRef`: serializable token for the writer end.
-- `readerRef`: serializable token for the reader end.
-
-
-
- ```typescript
- const channel = await iii.createChannel();
-
- // channel.writer
- // channel.reader
- // channel.writerRef
- // channel.readerRef
- ```
-
-
- ```python
- channel = iii_client.create_channel()
-
- # channel.writer
- # channel.reader
- # channel.writer_ref
- # channel.reader_ref
- ```
-
-
- ```rust
- let channel = iii.create_channel(None).await?;
-
- // channel.writer
- // channel.reader
- // channel.writer_ref
- // channel.reader_ref
- ```
-
-
-
-### 2. Write to the channel
-
-Write the stream payload to the local writer and close it when you are done.
-
-
-
- ```typescript
- const channel = await iii.createChannel();
-
- channel.writer.stream.end(Buffer.from("file contents"));
- ```
-
-
- ```python
- channel = await iii_client.create_channel_async()
-
- await channel.writer.write(b"file contents")
- await channel.writer.close_async()
- ```
-
-
- ```rust
- let channel = iii.create_channel(None).await?;
-
- channel.writer.write(b"file contents").await?;
- channel.writer.close().await?;
- ```
-
-
-
-### 3. Pass the reader ref to another function
-
-Pass the `readerRef` / `reader_ref` as part of a normal function invocation. The receiving function
-uses that ref to read from the channel.
-
-
-
- ```typescript
- const result = await iii.trigger({
- function_id: "files::process",
- payload: {
- filename: "report.csv",
- reader: channel.readerRef,
- },
- });
- ```
-
-
- ```python
- result = await iii_client.trigger_async({
- "function_id": "files::process",
- "payload": {
- "filename": "report.csv",
- "reader": channel.reader_ref.model_dump(),
- },
- })
- ```
-
-
- ```rust
- use iii_sdk::TriggerRequest;
- use serde_json::json;
-
- let result = iii
- .trigger(TriggerRequest {
- function_id: "files::process".to_string(),
- payload: json!({
- "filename": "report.csv",
- "reader": channel.reader_ref,
- }),
- action: None,
- timeout_ms: None,
- })
- .await?;
- ```
-
-
-
-### 4. Read from the channel
-
-Node and Python deserialize channel refs into live channel objects before your handler runs. Rust
-receives the ref in JSON and reconstructs the reader with `ChannelReader::new(...)`.
-
-
-
- ```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 };
- });
- ```
-
-
- ```python
- async def process_file(input: dict) -> dict:
- reader = input["reader"]
- chunks = []
-
- async for chunk in reader:
- chunks.append(chunk)
-
- return {"bytes": sum(len(chunk) for chunk in chunks)}
-
- worker.register_function("files::process", process_file)
- ```
-
-
- ```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(|(key, ref_)| key == "reader" && matches!(ref_.direction, ChannelDirection::Read))
- .map(|(_, ref_)| ref_.clone())
- .ok_or_else(|| IIIError::Handler("missing reader channel ref".to_string()))?;
-
- let reader = ChannelReader::new(iii.address(), &reader_ref);
- let mut bytes = 0;
-
- while let Some(chunk) = reader.next_binary().await? {
- bytes += chunk.len();
- }
-
- Ok(json!({ "bytes": bytes }))
- ```
-
-
-
-## Result
-
-The caller passes only a small ref through `trigger()`. The stream payload travels over the channel,
-and the receiving function reads it incrementally.
-
-## Related pages
-
-- [Channels architecture](/understanding-iii/channels)
-- [Node SDK channels](/sdk-reference/node-sdk#channels)
-- [Python SDK channels](/sdk-reference/python-sdk#channels)
-- [Rust SDK channels](/sdk-reference/rust-sdk#channels)
diff --git a/docs/using-iii/console.mdx b/docs/using-iii/console.mdx
index f4383f406..386449f7c 100644
--- a/docs/using-iii/console.mdx
+++ b/docs/using-iii/console.mdx
@@ -8,23 +8,17 @@ type: "how-to"
{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
- The console is the visual UI for iii systems. It lists workers, functions, and triggers, and
- shows traces, logs, and metrics from the
- iii-observability worker. For scripting or
- agent integration, call worker functions directly.
+ The console is the visual UI for iii systems. It lists workers, functions, and triggers, and shows
+ traces, logs, and metrics from the iii-observability worker. For scripting or agent integration,
+ call worker functions directly.
## Launch the console
-The console connects to a running iii engine, so start the engine first, then launch the console
-in a second terminal:
+The console connects to a running iii engine, so start the engine first, then launch the console in
+a second terminal:
```bash
-# 1. Start the engine. Use --use-default-config for a scratch instance,
-# or run `iii` (no flags) to load ./config.yaml.
-iii --use-default-config
-
-# 2. In another terminal, launch the console.
iii console
```
@@ -40,12 +34,12 @@ Open `http://127.0.0.1:3113` in your browser.
Lists every worker process currently connected to the engine. Each row shows the worker's name (or
short ID), the project / framework / language it reported, a runtime badge with SDK version, an
-optional isolation badge (`libkrun`, `docker`, etc. as reported by the worker), IP, PID, the
-number of functions it has registered, in-flight invocations, and how long it has been connected.
+optional isolation badge (`libkrun`, `docker`, etc. as reported by the worker), IP, PID, the number
+of functions it has registered, in-flight invocations, and how long it has been connected.
Selecting a worker opens a detail panel with full metadata, the project / framework / language
-fields the worker reported, live metrics (memory
-breakdown, CPU, event-loop lag, uptime), and the list of functions the worker has registered.
+fields the worker reported, live metrics (memory breakdown, CPU, event-loop lag, uptime), and the
+list of functions the worker has registered.
The isolation badge reflects what the worker self-reports via the `III_ISOLATION` environment
@@ -56,8 +50,8 @@ breakdown, CPU, event-loop lag, uptime), and the list of functions the worker ha
## Functions page
-Lists every registered function, grouped by namespace prefix (the part before `::`). A toggle in
-the header includes or excludes system functions.
+Lists every registered function, grouped by namespace prefix (the part before `::`). A toggle in the
+header includes or excludes system functions.
Select a function to open its detail panel. The panel includes:
@@ -93,33 +87,31 @@ Supported operations:
- **Add**. Open the Add Item modal to write a new entry; persisted via `state::set`.
- **Edit**. Update the value inline from the detail panel; fires any registered `state:updated`
triggers.
-- **Delete**. Remove an entry from the detail panel; fires any registered `state:deleted`
- triggers.
+- **Delete**. Remove an entry from the detail panel; fires any registered `state:deleted` triggers.
Use the search bar to filter items by key; pagination handles large groups. The page does not push
live updates over WebSocket; use the refresh control after external writes.
## Streams page
-Live WebSocket monitor for the messages flowing through the engine's stream connections. The
-header surfaces inbound / outbound counters, total bytes, and latency. A subscriptions bar lists
-the streams you're currently watching (subscribe and unsubscribe through the modal).
+Live WebSocket monitor for the messages flowing through the engine's stream connections. The header
+surfaces inbound / outbound counters, total bytes, and latency. A subscriptions bar lists the
+streams you're currently watching (subscribe and unsubscribe through the modal).
-Message rows show timestamp, stream name, event type, a truncated data preview, and message size.
-A direction filter and pause/resume controls let you isolate flows; selected messages open a
-detail panel with the full payload. The filtered set can be exported to JSON.
+Message rows show timestamp, stream name, event type, a truncated data preview, and message size. A
+direction filter and pause/resume controls let you isolate flows; selected messages open a detail
+panel with the full payload. The filtered set can be exported to JSON.
## Queues page
-Lists durable queue topics with **Topic**, **Broker**, a **DLQ** badge (when dead-lettered
-messages exist), and **Subscribers** columns. Selecting a topic opens a resizable detail panel
-with two tabs:
+Lists durable queue topics with **Topic**, **Broker**, a **DLQ** badge (when dead-lettered messages
+exist), and **Subscribers** columns. Selecting a topic opens a resizable detail panel with two tabs:
- **Overview**. Live topic stats and a JSON publisher for sending test messages.
- **Dead Letters**. Failed messages with retry and delete actions.
-Counts and stats refresh by polling. Keyboard shortcuts: `j` / `k` to move through the list,
-`Enter` to select, `1` / `2` to switch tabs.
+Counts and stats refresh by polling. Keyboard shortcuts: `j` / `k` to move through the list, `Enter`
+to select, `1` / `2` to switch tabs.
## Traces page
@@ -131,30 +123,27 @@ OpenTelemetry trace visualization across four view modes:
- **Flow**. Node-based execution flow of parent-child span relationships.
Filter controls cover trace ID, service name, span / operation name, status (`ok` / `error` /
-`unset`), duration range, time range, and arbitrary span attributes; results can be sorted by
-start time, duration, or service. The selected span's detail panel includes its name, service,
-duration, status, IDs, tags, logs, errors, and baggage.
+`unset`), duration range, time range, and arbitrary span attributes; results can be sorted by start
+time, duration, or service. The selected span's detail panel includes its name, service, duration,
+status, IDs, tags, logs, errors, and baggage.
- Trace collection requires the
- iii-observability worker with traces
- enabled.
+ Trace collection requires the iii-observability worker with traces enabled.
## Logs page
Structured OpenTelemetry log viewer. Each row shows timestamp, severity badge
(`DEBUG`/`INFO`/`WARN`/`ERROR`), a truncated trace ID, the source function / service, the message,
-and a context-field count badge. Expanding a row reveals the full attributes, resource metadata,
-and trace context.
+and a context-field count badge. Expanding a row reveals the full attributes, resource metadata, and
+trace context.
Filters cover severity, time range, and full-text search across message / trace ID / source. The
-trace ID on each row is clickable: it pivots the view to show only logs from that trace, which
-also lets you jump to the corresponding entry on the [Traces page](#traces-page).
+trace ID on each row is clickable: it pivots the view to show only logs from that trace, which also
+lets you jump to the corresponding entry on the [Traces page](#traces-page).
- Log collection requires the
- iii-observability worker with logs enabled.
+ Log collection requires the iii-observability worker with logs enabled.
## Configuration
@@ -167,7 +156,7 @@ To list every flag the binary accepts (with its default), run:
iii console --help
```
-The corresponding environment variables share the same set; each flag's help text names the env
-var that overrides it. The **Config** page in the sidebar shows the resolved values at runtime
-(the engine endpoints, ports, OpenTelemetry settings, and version the console is currently using),
-so you can confirm what's actually in effect without re-reading the flags.
+The corresponding environment variables share the same set; each flag's help text names the env var
+that overrides it. The **Config** page in the sidebar shows the resolved values at runtime (the
+engine endpoints, ports, OpenTelemetry settings, and version the console is currently using), so you
+can confirm what's actually in effect without re-reading the flags.
diff --git a/docs/using-iii/console.mdx.skill.md b/docs/using-iii/console.mdx.skill.md
index 180e92069..b70075461 100644
--- a/docs/using-iii/console.mdx.skill.md
+++ b/docs/using-iii/console.mdx.skill.md
@@ -1,26 +1,22 @@
+# Console
+
{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
- The console is the visual UI for iii systems. It lists workers, functions, and triggers, and
- shows traces, logs, and metrics from the
- iii-observability worker. For scripting or
- agent integration, call worker functions directly.
+ The console is the visual UI for iii systems. It lists workers, functions, and triggers, and shows
+ traces, logs, and metrics from the iii-observability worker. For scripting or agent integration,
+ call worker functions directly.
## Launch the console
-The console connects to a running iii engine, so start the engine first, then launch the console
-in a second terminal:
+The console connects to a running iii engine, so start the engine first, then launch the console in
+a second terminal:
```bash
-# 1. Start the engine. Use --use-default-config for a scratch instance,
-# or run `iii` (no flags) to load ./config.yaml.
-iii --use-default-config
-
-# 2. In another terminal, launch the console.
iii console
```
@@ -36,12 +32,12 @@ Open `http://127.0.0.1:3113` in your browser.
Lists every worker process currently connected to the engine. Each row shows the worker's name (or
short ID), the project / framework / language it reported, a runtime badge with SDK version, an
-optional isolation badge (`libkrun`, `docker`, etc. as reported by the worker), IP, PID, the
-number of functions it has registered, in-flight invocations, and how long it has been connected.
+optional isolation badge (`libkrun`, `docker`, etc. as reported by the worker), IP, PID, the number
+of functions it has registered, in-flight invocations, and how long it has been connected.
Selecting a worker opens a detail panel with full metadata, the project / framework / language
-fields the worker reported, live metrics (memory
-breakdown, CPU, event-loop lag, uptime), and the list of functions the worker has registered.
+fields the worker reported, live metrics (memory breakdown, CPU, event-loop lag, uptime), and the
+list of functions the worker has registered.
The isolation badge reflects what the worker self-reports via the `III_ISOLATION` environment
@@ -52,8 +48,8 @@ breakdown, CPU, event-loop lag, uptime), and the list of functions the worker ha
## Functions page
-Lists every registered function, grouped by namespace prefix (the part before `::`). A toggle in
-the header includes or excludes system functions.
+Lists every registered function, grouped by namespace prefix (the part before `::`). A toggle in the
+header includes or excludes system functions.
Select a function to open its detail panel. The panel includes:
@@ -89,33 +85,31 @@ Supported operations:
- **Add**. Open the Add Item modal to write a new entry; persisted via `state::set`.
- **Edit**. Update the value inline from the detail panel; fires any registered `state:updated`
triggers.
-- **Delete**. Remove an entry from the detail panel; fires any registered `state:deleted`
- triggers.
+- **Delete**. Remove an entry from the detail panel; fires any registered `state:deleted` triggers.
Use the search bar to filter items by key; pagination handles large groups. The page does not push
live updates over WebSocket; use the refresh control after external writes.
## Streams page
-Live WebSocket monitor for the messages flowing through the engine's stream connections. The
-header surfaces inbound / outbound counters, total bytes, and latency. A subscriptions bar lists
-the streams you're currently watching (subscribe and unsubscribe through the modal).
+Live WebSocket monitor for the messages flowing through the engine's stream connections. The header
+surfaces inbound / outbound counters, total bytes, and latency. A subscriptions bar lists the
+streams you're currently watching (subscribe and unsubscribe through the modal).
-Message rows show timestamp, stream name, event type, a truncated data preview, and message size.
-A direction filter and pause/resume controls let you isolate flows; selected messages open a
-detail panel with the full payload. The filtered set can be exported to JSON.
+Message rows show timestamp, stream name, event type, a truncated data preview, and message size. A
+direction filter and pause/resume controls let you isolate flows; selected messages open a detail
+panel with the full payload. The filtered set can be exported to JSON.
## Queues page
-Lists durable queue topics with **Topic**, **Broker**, a **DLQ** badge (when dead-lettered
-messages exist), and **Subscribers** columns. Selecting a topic opens a resizable detail panel
-with two tabs:
+Lists durable queue topics with **Topic**, **Broker**, a **DLQ** badge (when dead-lettered messages
+exist), and **Subscribers** columns. Selecting a topic opens a resizable detail panel with two tabs:
- **Overview**. Live topic stats and a JSON publisher for sending test messages.
- **Dead Letters**. Failed messages with retry and delete actions.
-Counts and stats refresh by polling. Keyboard shortcuts: `j` / `k` to move through the list,
-`Enter` to select, `1` / `2` to switch tabs.
+Counts and stats refresh by polling. Keyboard shortcuts: `j` / `k` to move through the list, `Enter`
+to select, `1` / `2` to switch tabs.
## Traces page
@@ -127,30 +121,27 @@ OpenTelemetry trace visualization across four view modes:
- **Flow**. Node-based execution flow of parent-child span relationships.
Filter controls cover trace ID, service name, span / operation name, status (`ok` / `error` /
-`unset`), duration range, time range, and arbitrary span attributes; results can be sorted by
-start time, duration, or service. The selected span's detail panel includes its name, service,
-duration, status, IDs, tags, logs, errors, and baggage.
+`unset`), duration range, time range, and arbitrary span attributes; results can be sorted by start
+time, duration, or service. The selected span's detail panel includes its name, service, duration,
+status, IDs, tags, logs, errors, and baggage.
- Trace collection requires the
- iii-observability worker with traces
- enabled.
+ Trace collection requires the iii-observability worker with traces enabled.
## Logs page
Structured OpenTelemetry log viewer. Each row shows timestamp, severity badge
(`DEBUG`/`INFO`/`WARN`/`ERROR`), a truncated trace ID, the source function / service, the message,
-and a context-field count badge. Expanding a row reveals the full attributes, resource metadata,
-and trace context.
+and a context-field count badge. Expanding a row reveals the full attributes, resource metadata, and
+trace context.
Filters cover severity, time range, and full-text search across message / trace ID / source. The
-trace ID on each row is clickable: it pivots the view to show only logs from that trace, which
-also lets you jump to the corresponding entry on the [Traces page](#traces-page).
+trace ID on each row is clickable: it pivots the view to show only logs from that trace, which also
+lets you jump to the corresponding entry on the [Traces page](#traces-page).
- Log collection requires the
- iii-observability worker with logs enabled.
+ Log collection requires the iii-observability worker with logs enabled.
## Configuration
@@ -163,7 +154,7 @@ To list every flag the binary accepts (with its default), run:
iii console --help
```
-The corresponding environment variables share the same set; each flag's help text names the env
-var that overrides it. The **Config** page in the sidebar shows the resolved values at runtime
-(the engine endpoints, ports, OpenTelemetry settings, and version the console is currently using),
-so you can confirm what's actually in effect without re-reading the flags.
+The corresponding environment variables share the same set; each flag's help text names the env var
+that overrides it. The **Config** page in the sidebar shows the resolved values at runtime (the
+engine endpoints, ports, OpenTelemetry settings, and version the console is currently using), so you
+can confirm what's actually in effect without re-reading the flags.
diff --git a/docs/using-iii/deployment.mdx b/docs/using-iii/deployment.mdx
index 80ba65241..71d0bfd06 100644
--- a/docs/using-iii/deployment.mdx
+++ b/docs/using-iii/deployment.mdx
@@ -44,7 +44,6 @@ The generated `docker-compose.yml` exposes:
| 49134 | SDK WebSocket (worker connections) |
| 3111 | REST API |
| 3112 | Stream API |
-| 9464 | Prometheus metrics |
The Dockerfile builds against `iiidev/iii:latest` (distroless, non-root). The compose file ships
commented-out Redis and RabbitMQ services that can be uncommented when workers need external
diff --git a/docs/using-iii/deployment.mdx.skill.md b/docs/using-iii/deployment.mdx.skill.md
index 92130089d..0e6b004fb 100644
--- a/docs/using-iii/deployment.mdx.skill.md
+++ b/docs/using-iii/deployment.mdx.skill.md
@@ -42,7 +42,6 @@ The generated `docker-compose.yml` exposes:
| 49134 | SDK WebSocket (worker connections) |
| 3111 | REST API |
| 3112 | Stream API |
-| 9464 | Prometheus metrics |
The Dockerfile builds against `iiidev/iii:latest` (distroless, non-root). The compose file ships
commented-out Redis and RabbitMQ services that can be uncommented when workers need external
diff --git a/docs/using-iii/engine.mdx b/docs/using-iii/engine.mdx
index af8eee3d6..e5ae715cf 100644
--- a/docs/using-iii/engine.mdx
+++ b/docs/using-iii/engine.mdx
@@ -48,6 +48,13 @@ workers:
Per-worker config schemas live on each worker's Worker Docs page. See
[Worker Registry](./workers-registry) for where to find a worker's config reference.
+
+ The engine reads `config.yaml` and launches the worker installations on disk; it does not read
+ `iii.lock`. The lockfile is a worker-installation concern, written and consumed by `iii worker
+ sync` / `update` / `verify` to make installs reproducible. See [Workers / The lockfile
+ (iii.lock)](/using-iii/workers#the-lockfile-iii-lock) for the full story.
+
+
Workers do not need to be running alongside iii; configuring them in config.yaml is a convenience. A worker
can be deployed anywhere and only needs a connection string to the iii instance. See
diff --git a/docs/using-iii/engine.mdx.skill.md b/docs/using-iii/engine.mdx.skill.md
index bce43453a..ddca70494 100644
--- a/docs/using-iii/engine.mdx.skill.md
+++ b/docs/using-iii/engine.mdx.skill.md
@@ -40,6 +40,13 @@ workers:
Per-worker config schemas live on each worker's Worker Docs page. See
[Worker Registry](./workers-registry) for where to find a worker's config reference.
+
+ The engine reads `config.yaml` and launches the worker installations on disk; it does not read
+ `iii.lock`. The lockfile is a worker-installation concern, written and consumed by `iii worker
+ sync` / `update` / `verify` to make installs reproducible. See [Workers / The lockfile
+ (iii.lock)](/using-iii/workers#the-lockfile-iii-lock) for the full story.
+
+
Workers do not need to be running alongside iii; configuring them in config.yaml is a convenience. A worker
can be deployed anywhere and only needs a connection string to the iii instance. See
diff --git a/docs/using-iii/functions.mdx b/docs/using-iii/functions.mdx
index 37ccd1415..ad2c60cce 100644
--- a/docs/using-iii/functions.mdx
+++ b/docs/using-iii/functions.mdx
@@ -7,21 +7,6 @@ type: "how-to"
{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
-## Invoking functions
-
-A function runs when a trigger fires. The same function can be invoked from many trigger types at
-once (direct CLI calls, an HTTP route, and a cron schedule, for example) without changing the
-handler.
-
-
- Some trigger types are: [`iii trigger`](/using-iii/cli), [`worker.trigger`](/using-iii/triggers),
- iii-http,
- iii-cron,
- iii-queue,
- iii-state,
- iii-stream.
-
-
## Register a function
Inside a worker, `worker.registerFunction(id, handler)` makes a function callable from anywhere in
@@ -33,7 +18,9 @@ and returns the result.
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
return { c: payload.a + payload.b };
@@ -73,26 +60,116 @@ and returns the result.
-## Define request and response formats
+## Invoking functions
-Functions can carry JSON Schemas for their request payload and response shape. The schemas are
-stored with the function and feed the iii console and the agent-readable skills.
+A function runs when a trigger fires. The same function can be invoked from many trigger types at
+once: direct CLI calls (`iii trigger`), in-process SDK calls (`worker.trigger`), or bindings to
+event-source workers like iii-http, iii-cron, iii-queue, iii-state, and iii-stream. All paths leave
+the handler unchanged.
+
+The two most common ways to invoke a function directly are from worker code with `worker.trigger` or
+from the terminal with `iii trigger`:
+
+
+
+ ```typescript
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 2, b: 3 },
+ });
+ ```
+
+
+ ```python
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 2, "b": 3},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".into(),
+ payload: json!({ "a": 2, "b": 3 }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+ ```bash
+ iii trigger math::add a=2 b=3
+ ```
+
+
- Runtime validation is not yet supported. Attached schemas are metadata only; the engine does not
- reject payloads or responses that don't match. Treat the schemas as contract documentation for
- callers, agents, and the console until validation lands.
+ Both calls are synchronous by default; they wait for the function to return. For fire-and-forget
+ (`TriggerAction.Void`), queue-routed delivery (`TriggerAction.Enqueue`), per-worker custom
+ actions, condition gating, and binding to event-source triggers, see [Triggers / Call a function
+ directly](/using-iii/triggers#call-a-function-directly).
+## Define request and response formats
+
+Functions can carry JSON Schemas for their request payload and response shape. The schemas are
+stored with the function and feed the iii console and the agent-readable skills.
+
For how to attach schemas when registering a function, see [Creating Workers /
Functions](/creating-workers/functions#attach-request-and-response-schemas).
-## Invoke a function
-
-
- For how to call a registered function from worker code or the terminal (with optional delivery
- actions like fire-and-forget or queue-routed), see
- [Triggers / Call a function directly](/using-iii/triggers#call-a-function-directly).
-
+## Common functions
+
+A handful of functions ship with the iii engine and the standard workers. You'll likely call them
+from almost every iii project. They look like any function you'd register yourself and are invoked
+the same way (via [`iii trigger`](/using-iii/cli) or
+[`worker.trigger`](/using-iii/triggers#call-a-function-directly)). The only thing special about them
+is that you didn't have to register them.
+
+### Engine functions (`engine::*`)
+
+The engine itself registers a small set of introspection and lifecycle functions. Full request and
+response schemas are in the
+[engine protocol reference](/sdk-reference/engine-sdk#engine-discovery-functions).
+
+| Function | What it does |
+| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| `engine::functions::list` | List every registered function. Pass `{ include_internal: true }` to include engine internals. |
+| `engine::workers::list` | List every connected worker with its metrics. Pass `{ worker_id: "" }` to look one up. |
+| `engine::triggers::list` | List every registered trigger binding. |
+| `engine::trigger-types::list` | List every advertised trigger type along with its config and call-request schemas. |
+| `engine::channels::create` | Allocate a streaming channel reader / writer pair. The SDK wraps this as `worker.createChannel()`; rarely called directly. |
+| `engine::workers::register` | Publish the calling worker's metadata (runtime, version, OS, PID). The SDK calls this automatically on connect. |
+
+The engine also publishes two subscription triggers in the same family. Bind a function to one of
+these to react to the registry changing:
+
+| Trigger | Fires when |
+| ----------------------------- | ----------------------------------------- |
+| `engine::functions-available` | A function is registered or unregistered. |
+| `engine::workers-available` | A worker connects or disconnects. |
+
+### Common workers
+
+Each of these is published by a separate worker. Function ids, payload shapes, and per-function
+behaviour are in the worker's own docs at [workers.iii.dev](https://workers.iii.dev):
+
+- **State**: KV-style state with scoped namespaces and reactive triggers on create/update/delete.
+ See [iii-state](https://workers.iii.dev/workers/iii-state).
+- **Stream**: Real-time push to connected clients over WebSocket. See
+ [iii-stream](https://workers.iii.dev/workers/iii-stream).
+- **Queue**: Durable, ordered job processing with retries, concurrency limits, and a dead-letter
+ queue. See [iii-queue](https://workers.iii.dev/workers/iii-queue).
+- **Pub/Sub**: Lightweight in-engine topic subscription for fan-out without durability guarantees.
+ See [iii-pubsub](https://workers.iii.dev/workers/iii-pubsub).
+- **Observability**: Traces, logs, metrics, alerts, sampling rules, and rollups. See
+ [iii-observability](https://workers.iii.dev/workers/iii-observability).
diff --git a/docs/using-iii/functions.mdx.skill.md b/docs/using-iii/functions.mdx.skill.md
index 2f729669e..a399dfefb 100644
--- a/docs/using-iii/functions.mdx.skill.md
+++ b/docs/using-iii/functions.mdx.skill.md
@@ -5,21 +5,6 @@
{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
-## Invoking functions
-
-A function runs when a trigger fires. The same function can be invoked from many trigger types at
-once (direct CLI calls, an HTTP route, and a cron schedule, for example) without changing the
-handler.
-
-
- Some trigger types are: [`iii trigger`](/using-iii/cli), [`worker.trigger`](/using-iii/triggers),
- iii-http,
- iii-cron,
- iii-queue,
- iii-state,
- iii-stream.
-
-
## Register a function
Inside a worker, `worker.registerFunction(id, handler)` makes a function callable from anywhere in
@@ -31,7 +16,9 @@ and returns the result.
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
return { c: payload.a + payload.b };
@@ -71,26 +58,116 @@ and returns the result.
-## Define request and response formats
+## Invoking functions
-Functions can carry JSON Schemas for their request payload and response shape. The schemas are
-stored with the function and feed the iii console and the agent-readable skills.
+A function runs when a trigger fires. The same function can be invoked from many trigger types at
+once: direct CLI calls (`iii trigger`), in-process SDK calls (`worker.trigger`), or bindings to
+event-source workers like iii-http, iii-cron, iii-queue, iii-state, and iii-stream. All paths leave
+the handler unchanged.
+
+The two most common ways to invoke a function directly are from worker code with `worker.trigger` or
+from the terminal with `iii trigger`:
+
+
+
+ ```typescript
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 2, b: 3 },
+ });
+ ```
+
+
+ ```python
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 2, "b": 3},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ use serde_json::json;
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".into(),
+ payload: json!({ "a": 2, "b": 3 }),
+ action: None,
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+ ```bash
+ iii trigger math::add a=2 b=3
+ ```
+
+
- Runtime validation is not yet supported. Attached schemas are metadata only; the engine does not
- reject payloads or responses that don't match. Treat the schemas as contract documentation for
- callers, agents, and the console until validation lands.
+ Both calls are synchronous by default; they wait for the function to return. For fire-and-forget
+ (`TriggerAction.Void`), queue-routed delivery (`TriggerAction.Enqueue`), per-worker custom
+ actions, condition gating, and binding to event-source triggers, see [Triggers / Call a function
+ directly](/using-iii/triggers#call-a-function-directly).
+## Define request and response formats
+
+Functions can carry JSON Schemas for their request payload and response shape. The schemas are
+stored with the function and feed the iii console and the agent-readable skills.
+
For how to attach schemas when registering a function, see [Creating Workers /
Functions](/creating-workers/functions#attach-request-and-response-schemas).
-## Invoke a function
-
-
- For how to call a registered function from worker code or the terminal (with optional delivery
- actions like fire-and-forget or queue-routed), see
- [Triggers / Call a function directly](/using-iii/triggers#call-a-function-directly).
-
+## Common functions
+
+A handful of functions ship with the iii engine and the standard workers. You'll likely call them
+from almost every iii project. They look like any function you'd register yourself and are invoked
+the same way (via [`iii trigger`](/using-iii/cli) or
+[`worker.trigger`](/using-iii/triggers#call-a-function-directly)). The only thing special about them
+is that you didn't have to register them.
+
+### Engine functions (`engine::*`)
+
+The engine itself registers a small set of introspection and lifecycle functions. Full request and
+response schemas are in the
+[engine protocol reference](/sdk-reference/engine-sdk#engine-discovery-functions).
+
+| Function | What it does |
+| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| `engine::functions::list` | List every registered function. Pass `{ include_internal: true }` to include engine internals. |
+| `engine::workers::list` | List every connected worker with its metrics. Pass `{ worker_id: "" }` to look one up. |
+| `engine::triggers::list` | List every registered trigger binding. |
+| `engine::trigger-types::list` | List every advertised trigger type along with its config and call-request schemas. |
+| `engine::channels::create` | Allocate a streaming channel reader / writer pair. The SDK wraps this as `worker.createChannel()`; rarely called directly. |
+| `engine::workers::register` | Publish the calling worker's metadata (runtime, version, OS, PID). The SDK calls this automatically on connect. |
+
+The engine also publishes two subscription triggers in the same family. Bind a function to one of
+these to react to the registry changing:
+
+| Trigger | Fires when |
+| ----------------------------- | ----------------------------------------- |
+| `engine::functions-available` | A function is registered or unregistered. |
+| `engine::workers-available` | A worker connects or disconnects. |
+
+### Common workers
+
+Each of these is published by a separate worker. Function ids, payload shapes, and per-function
+behaviour are in the worker's own docs at [workers.iii.dev](https://workers.iii.dev):
+
+- **State**: KV-style state with scoped namespaces and reactive triggers on create/update/delete.
+ See [iii-state](https://workers.iii.dev/workers/iii-state).
+- **Stream**: Real-time push to connected clients over WebSocket. See
+ [iii-stream](https://workers.iii.dev/workers/iii-stream).
+- **Queue**: Durable, ordered job processing with retries, concurrency limits, and a dead-letter
+ queue. See [iii-queue](https://workers.iii.dev/workers/iii-queue).
+- **Pub/Sub**: Lightweight in-engine topic subscription for fan-out without durability guarantees.
+ See [iii-pubsub](https://workers.iii.dev/workers/iii-pubsub).
+- **Observability**: Traces, logs, metrics, alerts, sampling rules, and rollups. See
+ [iii-observability](https://workers.iii.dev/workers/iii-observability).
diff --git a/docs/using-iii/triggers.mdx b/docs/using-iii/triggers.mdx
index 1e143f3b8..1d48b0750 100644
--- a/docs/using-iii/triggers.mdx
+++ b/docs/using-iii/triggers.mdx
@@ -20,7 +20,9 @@ function to return its result, or for the configured timeout to fire. Pass a dif
```typescript
import { registerWorker, TriggerAction } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
const result = await worker.trigger({
function_id: "math::add",
@@ -47,6 +49,7 @@ function to return its result, or for the configured timeout to fire. Pass a dif
# "action": TriggerAction.Void(), # fire-and-forget
# "action": TriggerAction.Enqueue(queue="math"), # route through iii-queue
})
+ # result = await worker.trigger_async({...}) # awaitable form for asyncio callers
```
@@ -85,7 +88,7 @@ Some common actions are:
- **`TriggerAction.Void()`**. Fire-and-forget. The call returns immediately; the function still runs
but the caller doesn't see the result.
- **`TriggerAction.Enqueue({ queue })`**. Provided by
- iii-queue. Routes the invocation through a named
+ [iii-queue](https://workers.iii.dev/workers/iii-queue). Routes the invocation through a named
queue with retries; the call returns once the message is enqueued.
@@ -93,8 +96,20 @@ Some common actions are:
documentation](https://workers.iii.dev) for the action types it offers.
+
+ In Python, every blocking method has an awaitable twin (`trigger_async`, `shutdown_async`,
+ `create_channel_async`) for use inside `asyncio`. See the [Python SDK
+ reference](/sdk-reference/python-sdk#trigger--trigger_async).
+
+
## Register a trigger
+
+ If you're authoring a worker, you'll want to refer to [Creating Workers /
+ Triggers](/creating-workers/triggers#bind-a-function-to-an-existing-trigger-type) to learn the
+ difference between registering a trigger, and registering a trigger type.
+
+
Functions can also run when a trigger is satisfied. A trigger can be any event that happens such as
a request to an `http` endpoint, a `cron` job, a change in `state`, or any other trigger that a
worker supports. You can also [write your own](/creating-workers/triggers).
@@ -107,7 +122,9 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerTrigger({
type: "http",
@@ -115,6 +132,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
config: { api_path: "/math/add", http_method: "POST" },
});
```
+
```python
@@ -132,6 +150,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
"config": {"api_path": "/math/add", "http_method": "POST"},
})
```
+
```rust
@@ -153,7 +172,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
Per-type configuration is documented in each worker's Worker Docs (e.g.
-iii-http for the `http` type).
+[iii-http](https://workers.iii.dev/workers/iii-http) for the `http` type).
## Bind multiple triggers to one function
@@ -162,13 +181,176 @@ number of types. Register a second trigger with the same `function_id` and a dif
config; the function runs unchanged whether the call arrives over HTTP, on a cron schedule, or from
a queue message.
+
+
+ ```typescript
+ // Same handler runs for an HTTP POST and a weekly cron tick.
+ worker.registerTrigger({
+ type: "http",
+ function_id: "reports::generate",
+ config: { api_path: "/reports/generate", http_method: "POST" },
+ });
+
+ worker.registerTrigger({
+ type: "cron",
+ function_id: "reports::generate",
+ config: { expression: "0 0 9 * * 1" }, // Every Monday at 09:00
+ });
+ ```
+
+
+ ```python
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "reports::generate",
+ "config": {"api_path": "/reports/generate", "http_method": "POST"},
+ })
+
+ worker.register_trigger({
+ "type": "cron",
+ "function_id": "reports::generate",
+ "config": {"expression": "0 0 9 * * 1"}, # Every Monday at 09:00
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "reports::generate".into(),
+ config: json!({ "api_path": "/reports/generate", "http_method": "POST" }),
+ metadata: None,
+ })?;
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "cron".into(),
+ function_id: "reports::generate".into(),
+ config: json!({ "expression": "0 0 9 * * 1" }), // Every Monday at 09:00
+ metadata: None,
+ })?;
+ ```
+
+
+
## Gate a trigger with a condition
-A trigger can carry an optional `condition_function_id`. When the trigger fires, the engine invokes
-the condition function first; the target `function_id` only runs if the condition returns truthy.
-Use this when the same event source should sometimes fire the function and sometimes skip it.
+A trigger can carry an optional `condition_function_id` (set inside the trigger's `config`). When
+the trigger fires, the engine invokes the condition function first with the same payload the
+handler would receive; the target `function_id` only runs when the condition returns truthy. The
+condition is a regular registered function.
+
+
+
+ ```typescript
+ worker.registerFunction(
+ "orders::is-priority",
+ async (payload: { customer_tier: string }) => payload.customer_tier === "gold",
+ );
+
+ worker.registerTrigger({
+ type: "http",
+ function_id: "orders::expedite",
+ config: {
+ api_path: "/orders/expedite",
+ http_method: "POST",
+ condition_function_id: "orders::is-priority",
+ },
+ });
+ ```
+
+
+ ```python
+ def is_priority(payload: dict) -> bool:
+ return payload.get("customer_tier") == "gold"
+
+ worker.register_function("orders::is-priority", is_priority)
+
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "orders::expedite",
+ "config": {
+ "api_path": "/orders/expedite",
+ "http_method": "POST",
+ "condition_function_id": "orders::is-priority",
+ },
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{RegisterFunction, RegisterTriggerInput};
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+ use serde_json::json;
+
+ #[derive(Deserialize, JsonSchema)]
+ struct Payload { customer_tier: String }
+
+ worker.register_function(RegisterFunction::new(
+ "orders::is-priority",
+ |input: Payload| -> Result {
+ Ok(input.customer_tier == "gold")
+ },
+ ));
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "orders::expedite".into(),
+ config: json!({
+ "api_path": "/orders/expedite",
+ "http_method": "POST",
+ "condition_function_id": "orders::is-priority",
+ }),
+ metadata: None,
+ })?;
+ ```
+
+
## Unregister a trigger
-Trigger registration returns a handle. Pass that handle to `worker.unregisterTrigger(...)` to drop
-the trigger at runtime. When the worker disconnects, all of its triggers are removed automatically.
+Trigger registration returns a handle with an `unregister()` method. Call it to drop the trigger
+at runtime; when the worker disconnects, all of its triggers are removed automatically.
+
+
+
+ ```typescript
+ const trigger = worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
+
+ trigger.unregister();
+ ```
+
+
+ ```python
+ trigger = worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
+
+ trigger.unregister()
+ ```
+
+
+ ```rust
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
+
+ let trigger = worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
+
+ trigger.unregister();
+ ```
+
+
diff --git a/docs/using-iii/triggers.mdx.skill.md b/docs/using-iii/triggers.mdx.skill.md
index fbd398310..d96c7e1a5 100644
--- a/docs/using-iii/triggers.mdx.skill.md
+++ b/docs/using-iii/triggers.mdx.skill.md
@@ -16,7 +16,9 @@ function to return its result, or for the configured timeout to fire. Pass a dif
```typescript
import { registerWorker, TriggerAction } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
const result = await worker.trigger({
function_id: "math::add",
@@ -43,6 +45,7 @@ function to return its result, or for the configured timeout to fire. Pass a dif
# "action": TriggerAction.Void(), # fire-and-forget
# "action": TriggerAction.Enqueue(queue="math"), # route through iii-queue
})
+ # result = await worker.trigger_async({...}) # awaitable form for asyncio callers
```
@@ -81,7 +84,7 @@ Some common actions are:
- **`TriggerAction.Void()`**. Fire-and-forget. The call returns immediately; the function still runs
but the caller doesn't see the result.
- **`TriggerAction.Enqueue({ queue })`**. Provided by
- iii-queue. Routes the invocation through a named
+ [iii-queue](https://workers.iii.dev/workers/iii-queue). Routes the invocation through a named
queue with retries; the call returns once the message is enqueued.
@@ -89,8 +92,20 @@ Some common actions are:
documentation](https://workers.iii.dev) for the action types it offers.
+
+ In Python, every blocking method has an awaitable twin (`trigger_async`, `shutdown_async`,
+ `create_channel_async`) for use inside `asyncio`. See the [Python SDK
+ reference](/sdk-reference/python-sdk#trigger--trigger_async).
+
+
## Register a trigger
+
+ If you're authoring a worker, you'll want to refer to [Creating Workers /
+ Triggers](/creating-workers/triggers#bind-a-function-to-an-existing-trigger-type) to learn the
+ difference between registering a trigger, and registering a trigger type.
+
+
Functions can also run when a trigger is satisfied. A trigger can be any event that happens such as
a request to an `http` endpoint, a `cron` job, a change in `state`, or any other trigger that a
worker supports. You can also [write your own](/creating-workers/triggers).
@@ -103,7 +118,9 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
```typescript
import { registerWorker } from "iii-sdk";
- const worker = registerWorker(process.env.III_URL);
+ const url = process.env.III_URL;
+ if (!url) throw new Error("III_URL must be set");
+ const worker = registerWorker(url);
worker.registerTrigger({
type: "http",
@@ -111,6 +128,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
config: { api_path: "/math/add", http_method: "POST" },
});
```
+
```python
@@ -128,6 +146,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
"config": {"api_path": "/math/add", "http_method": "POST"},
})
```
+
```rust
@@ -149,7 +168,7 @@ You bind triggers to functions via the `function_id`. The trigger declares its `
Per-type configuration is documented in each worker's Worker Docs (e.g.
-iii-http for the `http` type).
+[iii-http](https://workers.iii.dev/workers/iii-http) for the `http` type).
## Bind multiple triggers to one function
@@ -158,13 +177,176 @@ number of types. Register a second trigger with the same `function_id` and a dif
config; the function runs unchanged whether the call arrives over HTTP, on a cron schedule, or from
a queue message.
+
+
+ ```typescript
+ // Same handler runs for an HTTP POST and a weekly cron tick.
+ worker.registerTrigger({
+ type: "http",
+ function_id: "reports::generate",
+ config: { api_path: "/reports/generate", http_method: "POST" },
+ });
+
+ worker.registerTrigger({
+ type: "cron",
+ function_id: "reports::generate",
+ config: { expression: "0 0 9 * * 1" }, // Every Monday at 09:00
+ });
+ ```
+
+
+ ```python
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "reports::generate",
+ "config": {"api_path": "/reports/generate", "http_method": "POST"},
+ })
+
+ worker.register_trigger({
+ "type": "cron",
+ "function_id": "reports::generate",
+ "config": {"expression": "0 0 9 * * 1"}, # Every Monday at 09:00
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "reports::generate".into(),
+ config: json!({ "api_path": "/reports/generate", "http_method": "POST" }),
+ metadata: None,
+ })?;
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "cron".into(),
+ function_id: "reports::generate".into(),
+ config: json!({ "expression": "0 0 9 * * 1" }), // Every Monday at 09:00
+ metadata: None,
+ })?;
+ ```
+
+
+
## Gate a trigger with a condition
-A trigger can carry an optional `condition_function_id`. When the trigger fires, the engine invokes
-the condition function first; the target `function_id` only runs if the condition returns truthy.
-Use this when the same event source should sometimes fire the function and sometimes skip it.
+A trigger can carry an optional `condition_function_id` (set inside the trigger's `config`). When
+the trigger fires, the engine invokes the condition function first with the same payload the
+handler would receive; the target `function_id` only runs when the condition returns truthy. The
+condition is a regular registered function.
+
+
+
+ ```typescript
+ worker.registerFunction(
+ "orders::is-priority",
+ async (payload: { customer_tier: string }) => payload.customer_tier === "gold",
+ );
+
+ worker.registerTrigger({
+ type: "http",
+ function_id: "orders::expedite",
+ config: {
+ api_path: "/orders/expedite",
+ http_method: "POST",
+ condition_function_id: "orders::is-priority",
+ },
+ });
+ ```
+
+
+ ```python
+ def is_priority(payload: dict) -> bool:
+ return payload.get("customer_tier") == "gold"
+
+ worker.register_function("orders::is-priority", is_priority)
+
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "orders::expedite",
+ "config": {
+ "api_path": "/orders/expedite",
+ "http_method": "POST",
+ "condition_function_id": "orders::is-priority",
+ },
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{RegisterFunction, RegisterTriggerInput};
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+ use serde_json::json;
+
+ #[derive(Deserialize, JsonSchema)]
+ struct Payload { customer_tier: String }
+
+ worker.register_function(RegisterFunction::new(
+ "orders::is-priority",
+ |input: Payload| -> Result {
+ Ok(input.customer_tier == "gold")
+ },
+ ));
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "orders::expedite".into(),
+ config: json!({
+ "api_path": "/orders/expedite",
+ "http_method": "POST",
+ "condition_function_id": "orders::is-priority",
+ }),
+ metadata: None,
+ })?;
+ ```
+
+
## Unregister a trigger
-Trigger registration returns a handle. Pass that handle to `worker.unregisterTrigger(...)` to drop
-the trigger at runtime. When the worker disconnects, all of its triggers are removed automatically.
+Trigger registration returns a handle with an `unregister()` method. Call it to drop the trigger
+at runtime; when the worker disconnects, all of its triggers are removed automatically.
+
+
+
+ ```typescript
+ const trigger = worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
+
+ trigger.unregister();
+ ```
+
+
+ ```python
+ trigger = worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
+
+ trigger.unregister()
+ ```
+
+
+ ```rust
+ use iii_sdk::RegisterTriggerInput;
+ use serde_json::json;
+
+ let trigger = worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
+
+ trigger.unregister();
+ ```
+
+
diff --git a/docs/using-iii/workers.mdx b/docs/using-iii/workers.mdx
index c9e0ece1d..232367dbf 100644
--- a/docs/using-iii/workers.mdx
+++ b/docs/using-iii/workers.mdx
@@ -33,13 +33,19 @@ being callable until it reconnects.
Workers](/creating-workers/workers#connecting-to-the-engine).
-## Finding workers
+## Managing workers
+
+The `iii worker` CLI commands cover the full lifecycle of every worker in your project: finding new
+ones in the registry, installing them into `config.yaml` and `iii.lock`, controlling their running
+state, inspecting their logs, and removing them when they're no longer needed.
+
+### Finding workers
We maintain a worker registry which you can explore at [workers.iii.dev](https://workers.iii.dev/).
The registry contains many workers that encapsulate common services. See
[Worker Registry](./workers-registry) for more information on the worker registry.
-## Adding a worker
+### Adding a worker
You need iii [installed](/install) and [running](/using-iii/engine) before adding a worker. To
@@ -61,7 +67,7 @@ worker, use `iii worker reinstall ` (equivalent to `add --force`).
worker](./workers-registry#adding-a-worker).
-## Listing workers
+### Listing workers
`iii worker list` shows every worker declared in your project's `config.yaml` along with its current
status:
@@ -70,23 +76,25 @@ status:
iii worker list
```
-## Starting and stopping workers
+### Starting and stopping workers
Added workers start automatically with the engine. To control them manually, use the `start`,
`stop`, and `restart` commands:
+{/* TODO: drop the `-y` once `iii worker stop` is made non-interactive (planned). */}
+
```bash
-iii worker start # start one worker
-iii worker stop # stop one worker
-iii worker restart # stop then start
+iii worker start # start one worker
+iii worker stop -y # stop one worker (-y skips the confirmation prompt)
+iii worker restart # stop then start
```
- To call functions inside running workers (directly with `iii.trigger` / `iii trigger`, or by
+ To call functions inside running workers (directly with `worker.trigger` / `iii trigger`, or by
binding them to events with optional condition gates), see [Triggers](/using-iii/triggers).
-## Inspecting a worker
+### Inspecting a worker
To check a specific worker's state, follow its logs, or run a command inside the worker's sandbox,
use:
@@ -97,6 +105,30 @@ iii worker logs # stream the worker's logs
iii worker exec -- # run a command inside the worker
```
+### Updating a worker
+
+`iii worker update` re-resolves locked workers and writes the new pins back to `iii.lock`. Pass a
+worker name to update one, or omit it to update every locked worker:
+
+```bash
+iii worker update # one worker
+iii worker update # every locked worker
+```
+
+### Removing a worker
+
+`iii worker remove` drops a worker from `config.yaml` and the engine tears down the running worker
+process:
+
+{/* TODO: drop the `-y` once `iii worker remove` / `iii worker clear` are made non-interactive (planned). */}
+
+```bash
+iii worker remove -y # -y skips the confirmation when the worker is running
+```
+
+Downloaded artifacts remain on disk after removal. To delete them too, use
+`iii worker clear -y `. Omit the name to clear every worker's artifacts.
+
## Worker skills
Every worker also ships with skills for Agentic work. Skills are managed by the `skills` worker, an
@@ -117,30 +149,23 @@ worker that provides it to be connected. For example if you add `http` triggers
worker then you can now expose endpoints for your function just as you would in a web framework like
Express or FastAPI.
-## Versioning and pinning
-
-Workers are published with semver versions. Installing without a version specifier picks the latest
-release. Append `@` to a registry name to pin a specific release rather than tracking the
-latest:
+## Versioning
-```bash
-iii worker add iii-state@1.2.0
-```
+iii workers follow semver. A project records the resolved version of every managed worker in
+`iii.lock`, which makes installs reproducible across machines and platforms.
-The pin is recorded in `iii.lock` and replays on every subsequent install, so the same deployment of
-an iii system is reproducible across machines.
+### Version pins
-## Updating a worker
-
-`iii worker update` re-resolves locked workers and writes the new pins back to `iii.lock`. Pass a
-worker name to update one, or omit it to update every locked worker:
+Installing without a version specifier picks the latest release. Append `@` to a registry
+name to pin a specific release rather than tracking the latest:
```bash
-iii worker update # one worker
-iii worker update # every locked worker
+iii worker add iii-state@1.2.0
```
-## The lockfile (iii.lock)
+The pin is recorded in `iii.lock` and replays on every subsequent install.
+
+### The lockfile (iii.lock)
`iii.lock` is a YAML file at your project root. It pins each managed worker to a specific version
and source so the same worker set installs the same way across machines and platforms. Binary
@@ -155,26 +180,13 @@ iii worker sync --frozen # CI form: verify the lockfile without mutating local
iii worker verify # report drift between config.yaml and iii.lock
```
-`iii worker update` (above) is the third lockfile command; it re-resolves pins to the latest
-permitted versions and writes them back to `iii.lock`.
+[`iii worker update`](#updating-a-worker) is the third lockfile-related command; it re-resolves pins
+to the latest permitted versions and writes them back to `iii.lock`.
{/* TODO: Add a dedicated lockfile reference page for the per-field schema (top-level fields, LockedWorker, BinaryArtifact, ImageSource, manifest hash format). The dx-improves source includes `docs/workers/managed-worker-lockfile.mdx` which can be ported. */}
-## Removing a worker
-
-`iii worker remove` drops a worker from `config.yaml` and the engine tears down the running worker
-process:
-
-```bash
-iii worker remove
-```
-
-Downloaded artifacts remain on disk after removal. To delete them too, use
-`iii worker clear `. Omit the name to clear every worker's artifacts.
-
## Authoring workers
Creating a new worker, registering functions and triggers in worker code, and building or publishing
a worker image are out of scope for this page. See
-[Creating Workers / Workers](/creating-workers/workers) and
-[Creating Workers / Worker Registry](/creating-workers/workers-registry).
+[Creating Workers / Workers](/creating-workers/workers).
diff --git a/docs/using-iii/workers.mdx.skill.md b/docs/using-iii/workers.mdx.skill.md
index 74ac404df..24c08607b 100644
--- a/docs/using-iii/workers.mdx.skill.md
+++ b/docs/using-iii/workers.mdx.skill.md
@@ -18,13 +18,19 @@ being callable until it reconnects.
Workers](/creating-workers/workers#connecting-to-the-engine).
-## Finding workers
+## Managing workers
+
+The `iii worker` CLI commands cover the full lifecycle of every worker in your project: finding new
+ones in the registry, installing them into `config.yaml` and `iii.lock`, controlling their running
+state, inspecting their logs, and removing them when they're no longer needed.
+
+### Finding workers
We maintain a worker registry which you can explore at [workers.iii.dev](https://workers.iii.dev/).
The registry contains many workers that encapsulate common services. See
[Worker Registry](./workers-registry) for more information on the worker registry.
-## Adding a worker
+### Adding a worker
You need iii [installed](/install) and [running](/using-iii/engine) before adding a worker. To
@@ -46,7 +52,7 @@ worker, use `iii worker reinstall ` (equivalent to `add --force`).
worker](./workers-registry#adding-a-worker).
-## Listing workers
+### Listing workers
`iii worker list` shows every worker declared in your project's `config.yaml` along with its current
status:
@@ -55,23 +61,25 @@ status:
iii worker list
```
-## Starting and stopping workers
+### Starting and stopping workers
Added workers start automatically with the engine. To control them manually, use the `start`,
`stop`, and `restart` commands:
+{/* TODO: drop the `-y` once `iii worker stop` is made non-interactive (planned). */}
+
```bash
-iii worker start # start one worker
-iii worker stop # stop one worker
-iii worker restart # stop then start
+iii worker start # start one worker
+iii worker stop -y # stop one worker (-y skips the confirmation prompt)
+iii worker restart # stop then start
```
- To call functions inside running workers (directly with `iii.trigger` / `iii trigger`, or by
+ To call functions inside running workers (directly with `worker.trigger` / `iii trigger`, or by
binding them to events with optional condition gates), see [Triggers](/using-iii/triggers).
-## Inspecting a worker
+### Inspecting a worker
To check a specific worker's state, follow its logs, or run a command inside the worker's sandbox,
use:
@@ -82,6 +90,30 @@ iii worker logs # stream the worker's logs
iii worker exec -- # run a command inside the worker
```
+### Updating a worker
+
+`iii worker update` re-resolves locked workers and writes the new pins back to `iii.lock`. Pass a
+worker name to update one, or omit it to update every locked worker:
+
+```bash
+iii worker update # one worker
+iii worker update # every locked worker
+```
+
+### Removing a worker
+
+`iii worker remove` drops a worker from `config.yaml` and the engine tears down the running worker
+process:
+
+{/* TODO: drop the `-y` once `iii worker remove` / `iii worker clear` are made non-interactive (planned). */}
+
+```bash
+iii worker remove -y # -y skips the confirmation when the worker is running
+```
+
+Downloaded artifacts remain on disk after removal. To delete them too, use
+`iii worker clear -y `. Omit the name to clear every worker's artifacts.
+
## Worker skills
Every worker also ships with skills for Agentic work. Skills are managed by the `skills` worker, an
@@ -102,30 +134,23 @@ worker that provides it to be connected. For example if you add `http` triggers
worker then you can now expose endpoints for your function just as you would in a web framework like
Express or FastAPI.
-## Versioning and pinning
-
-Workers are published with semver versions. Installing without a version specifier picks the latest
-release. Append `@` to a registry name to pin a specific release rather than tracking the
-latest:
+## Versioning
-```bash
-iii worker add iii-state@1.2.0
-```
+iii workers follow semver. A project records the resolved version of every managed worker in
+`iii.lock`, which makes installs reproducible across machines and platforms.
-The pin is recorded in `iii.lock` and replays on every subsequent install, so the same deployment of
-an iii system is reproducible across machines.
+### Version pins
-## Updating a worker
-
-`iii worker update` re-resolves locked workers and writes the new pins back to `iii.lock`. Pass a
-worker name to update one, or omit it to update every locked worker:
+Installing without a version specifier picks the latest release. Append `@` to a registry
+name to pin a specific release rather than tracking the latest:
```bash
-iii worker update # one worker
-iii worker update # every locked worker
+iii worker add iii-state@1.2.0
```
-## The lockfile (iii.lock)
+The pin is recorded in `iii.lock` and replays on every subsequent install.
+
+### The lockfile (iii.lock)
`iii.lock` is a YAML file at your project root. It pins each managed worker to a specific version
and source so the same worker set installs the same way across machines and platforms. Binary
@@ -140,26 +165,13 @@ iii worker sync --frozen # CI form: verify the lockfile without mutating local
iii worker verify # report drift between config.yaml and iii.lock
```
-`iii worker update` (above) is the third lockfile command; it re-resolves pins to the latest
-permitted versions and writes them back to `iii.lock`.
+[`iii worker update`](#updating-a-worker) is the third lockfile-related command; it re-resolves pins
+to the latest permitted versions and writes them back to `iii.lock`.
{/* TODO: Add a dedicated lockfile reference page for the per-field schema (top-level fields, LockedWorker, BinaryArtifact, ImageSource, manifest hash format). The dx-improves source includes `docs/workers/managed-worker-lockfile.mdx` which can be ported. */}
-## Removing a worker
-
-`iii worker remove` drops a worker from `config.yaml` and the engine tears down the running worker
-process:
-
-```bash
-iii worker remove
-```
-
-Downloaded artifacts remain on disk after removal. To delete them too, use
-`iii worker clear `. Omit the name to clear every worker's artifacts.
-
## Authoring workers
Creating a new worker, registering functions and triggers in worker code, and building or publishing
a worker image are out of scope for this page. See
-[Creating Workers / Workers](/creating-workers/workers) and
-[Creating Workers / Worker Registry](/creating-workers/workers-registry).
+[Creating Workers / Workers](/creating-workers/workers).