diff --git a/docs/0-13-0/assets/favicon.svg b/docs/0-13-0/assets/favicon.svg
new file mode 100644
index 000000000..1312d00ad
--- /dev/null
+++ b/docs/0-13-0/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/0-13-0/assets/iii-black.svg b/docs/0-13-0/assets/iii-black.svg
new file mode 100644
index 000000000..5cdd91eb2
--- /dev/null
+++ b/docs/0-13-0/assets/iii-black.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/0-13-0/assets/iii-white.svg b/docs/0-13-0/assets/iii-white.svg
new file mode 100644
index 000000000..1449f07b3
--- /dev/null
+++ b/docs/0-13-0/assets/iii-white.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/0-13-0/changelog/0-11-0/everything-is-a-worker.mdx b/docs/0-13-0/changelog/0-11-0/everything-is-a-worker.mdx
new file mode 100644
index 000000000..7d1aebecc
--- /dev/null
+++ b/docs/0-13-0/changelog/0-11-0/everything-is-a-worker.mdx
@@ -0,0 +1,342 @@
+---
+title: 'Everything is a Worker'
+description: 'Why we renamed modules to workers and how to migrate.'
+---
+
+## Why this change
+
+iii is built on three primitives: **Workers**, **Functions**, and **Triggers**. Every concept in the system maps to one of these:
+
+- **Workers** are long-running processes that connect to the engine and provide capabilities -- HTTP serving, queue processing, cron scheduling, state management, stream handling, and anything else the engine needs to operate.
+- **Functions** are units of work. A worker registers functions, and any other worker (or the engine itself) can invoke them by ID.
+- **Triggers** are the rules that cause functions to fire -- an HTTP request, a cron schedule, a queue message, a state change.
+
+Before this release, the engine-internal components that provide capabilities (HTTP, queues, cron, etc.) were called "modules". Meanwhile, the remote SDK processes that register functions were called "workers". This created a confusing split: two different names for things that serve the same role -- connecting to the engine and doing work.
+
+Modules **are** workers. They run alongside the engine, register functions, react to triggers, and provide capabilities to the rest of the system. The only difference was the name.
+
+By renaming modules to workers, every component in the system uses the same vocabulary. The engine config lists `workers:`. The SDK connects `workers`. The docs describe `workers`. There is one concept, one name, and one mental model.
+
+The previous `Worker` struct (representing a connected SDK runtime over WebSocket) has been renamed to `WorkerConnection` to avoid collision.
+
+---
+
+## What changed
+
+### Config YAML
+
+The top-level `modules:` key is now `workers:`, and each entry uses `name:` with a short identifier instead of `class:` with a Rust-style path.
+
+```yaml
+# Before
+modules:
+ - class: modules::stream::StreamModule
+ config:
+ port: 3112
+
+# After
+workers:
+ - name: iii-stream
+ config:
+ port: 3112
+```
+
+The `modules:` key is still accepted for backward compatibility, but `workers:` is the canonical form going forward.
+
+#### Worker name mapping
+
+| Old `class:` value | New `name:` value |
+|---|---|
+| `modules::worker::WorkerModule` | `iii-worker-manager` |
+| `modules::stream::StreamModule` | `iii-stream` |
+| `modules::state::StateModule` | `iii-state` |
+| `modules::api::RestApiModule` | `iii-http` |
+| `modules::pubsub::PubSubModule` | `iii-pubsub` |
+| `modules::observability::OtelModule` | `iii-observability` |
+| `modules::queue::QueueModule` | `iii-queue` |
+| `modules::http_functions::HttpFunctionsModule` | `iii-http-functions` |
+| `modules::cron::CronModule` | `iii-cron` |
+| `modules::shell::ExecModule` | `iii-exec` |
+| `modules::bridge_client::BridgeClientModule` | `iii-bridge` |
+
+#### Adapter name mapping
+
+Adapter entries inside `config.adapter` also switched from `class:` to `name:` with short identifiers:
+
+| Old adapter `class:` | New adapter `name:` |
+|---|---|
+| `modules::stream::adapters::KvStore` | `kv` |
+| `modules::stream::adapters::RedisAdapter` | `redis` |
+| `modules::stream::adapters::Bridge` | `bridge` |
+| `modules::state::adapters::KvStore` | `kv` |
+| `modules::state::adapters::RedisAdapter` | `redis` |
+| `modules::state::adapters::Bridge` | `bridge` |
+| `modules::pubsub::LocalAdapter` | `local` |
+| `modules::pubsub::RedisAdapter` | `redis` |
+| `modules::queue::BuiltinQueueAdapter` | `builtin` |
+| `modules::queue::RedisAdapter` | `redis` |
+| `modules::queue::RabbitMQAdapter` | `rabbitmq` |
+| `modules::queue::adapters::Bridge` | `bridge` |
+| `modules::cron::KvCronAdapter` | `kv` |
+| `modules::cron::RedisCronAdapter` | `redis` |
+
+#### Full config example
+
+
+
+```yaml
+modules:
+ - class: modules::stream::StreamModule
+ config:
+ port: 3112
+ adapter:
+ class: modules::stream::adapters::KvStore
+ config:
+ store_method: file_based
+ file_path: ./data/stream_store
+
+ - class: modules::state::StateModule
+ config:
+ adapter:
+ class: modules::state::adapters::KvStore
+ config:
+ store_method: file_based
+ file_path: ./data/state_store.db
+
+ - class: modules::api::RestApiModule
+ config:
+ port: 3111
+
+ - class: modules::observability::OtelModule
+ config:
+ enabled: true
+
+ - class: modules::queue::QueueModule
+ config:
+ adapter:
+ class: modules::queue::BuiltinQueueAdapter
+
+ - class: modules::pubsub::PubSubModule
+ config:
+ adapter:
+ class: modules::pubsub::LocalAdapter
+
+ - class: modules::cron::CronModule
+ config:
+ adapter:
+ class: modules::cron::KvCronAdapter
+```
+
+
+```yaml
+workers:
+ - name: iii-stream
+ config:
+ port: 3112
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: ./data/stream_store
+
+ - name: iii-state
+ config:
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: ./data/state_store.db
+
+ - name: iii-http
+ config:
+ port: 3111
+
+ - name: iii-observability
+ config:
+ enabled: true
+
+ - name: iii-queue
+ config:
+ adapter:
+ name: builtin
+
+ - name: iii-pubsub
+ config:
+ adapter:
+ name: local
+
+ - name: iii-cron
+ config:
+ adapter:
+ name: kv
+```
+
+
+
+---
+
+### Rust API
+
+#### Crate module path
+
+The `modules` Rust module has been renamed to `workers`, and the trait definition file moved from `module.rs` to `traits.rs`.
+
+```rust
+// Before
+use iii::modules::config::EngineBuilder;
+use iii::modules::module::Module;
+use iii::modules::registry::ModuleRegistration;
+
+// After
+use iii::workers::config::EngineBuilder; // also re-exported as iii::EngineBuilder
+use iii::workers::traits::Worker;
+use iii::workers::registry::WorkerRegistration;
+```
+
+#### Trait renames
+
+| Before | After |
+|---|---|
+| `trait Module` | `trait Worker` |
+| `trait ConfigurableModule` | `trait ConfigurableWorker` |
+| `Module::make_module()` | `Worker::make_worker()` |
+| `Module::create() -> Box` | `Worker::create() -> Box` |
+| `ConfigurableModule::DEFAULT_ADAPTER_CLASS` | `ConfigurableWorker::DEFAULT_ADAPTER_NAME` |
+| `ConfigurableModule::default_adapter_class()` | `ConfigurableWorker::default_adapter_name()` |
+| `ConfigurableModule::adapter_class_from_config()` | `ConfigurableWorker::adapter_name_from_config()` |
+
+#### Type alias and struct renames
+
+| Before | After |
+|---|---|
+| `ModuleFuture` | `WorkerFuture` |
+| `ModuleFactory` | `WorkerFactory` |
+| `ModuleInfo` | `WorkerInfo` |
+| `ModuleRegistry` | `WorkerRegistry` |
+| `ModuleEntry` | `WorkerEntry` |
+| `ModuleRegistration` | `WorkerRegistration` |
+
+#### Macro renames
+
+| Before | After |
+|---|---|
+| `register_module!(class, Type, ...)` | `register_worker!(name, Type, ...)` |
+
+#### EngineBuilder API
+
+| Before | After |
+|---|---|
+| `EngineBuilder::add_module(class, config)` | `EngineBuilder::add_worker(name, config)` |
+| `EngineBuilder::register_module::(class)` | `EngineBuilder::register_worker::(name)` |
+
+#### Implementation struct renames
+
+| Before | After |
+|---|---|
+| `WorkerModule` | `WorkerManager` |
+| `StreamCoreModule` | `StreamWorker` |
+| `StateCoreModule` | `StateWorker` |
+| `RestApiCoreModule` | `HttpWorker` |
+| `PubSubCoreModule` | `PubSubWorker` |
+| `OtelModule` | `ObservabilityWorker` |
+| `QueueCoreModule` | `QueueWorker` |
+| `HttpFunctionsModule` | `HttpFunctionsWorker` |
+| `CronCoreModule` | `CronWorker` |
+| `ExecCoreModule` | `ExecWorker` |
+| `BridgeClientModule` | `BridgeClientWorker` |
+| `TelemetryModule` | `TelemetryWorker` |
+| `EngineFunctionsModule` | `EngineFunctionsWorker` |
+| `ExternalModule` | `ExternalWorker` |
+
+#### WorkerConnection (was Worker)
+
+The `Worker` struct that represented a connected remote SDK runtime has been renamed to `WorkerConnection` and moved to a separate module.
+
+| Before | After |
+|---|---|
+| `crate::workers::Worker` | `crate::worker_connections::WorkerConnection` |
+| `crate::workers::WorkerRegistry` | `crate::worker_connections::WorkerConnectionRegistry` |
+| `crate::workers::WorkerStatus` | `crate::worker_connections::WorkerConnectionStatus` |
+| `crate::workers::WorkerTelemetryMeta` | `crate::worker_connections::WorkerConnectionTelemetryMeta` |
+
+---
+
+## Migration examples
+
+### Custom worker
+
+
+
+```rust
+use iii::modules::module::Module;
+
+pub struct MyModule { /* ... */ }
+
+#[async_trait]
+impl Module for MyModule {
+ fn name(&self) -> &'static str { "MyModule" }
+ async fn create(engine: Arc, config: Option) -> anyhow::Result> {
+ Ok(Box::new(MyModule { /* ... */ }))
+ }
+ async fn initialize(&self) -> anyhow::Result<()> { Ok(()) }
+}
+
+iii::register_module!("my::custom::Module", MyModule, enabled_by_default = true);
+```
+
+
+```rust
+use iii::workers::traits::Worker;
+
+pub struct MyWorker { /* ... */ }
+
+#[async_trait]
+impl Worker for MyWorker {
+ fn name(&self) -> &'static str { "MyWorker" }
+ async fn create(engine: Arc, config: Option) -> anyhow::Result> {
+ Ok(Box::new(MyWorker { /* ... */ }))
+ }
+ async fn initialize(&self) -> anyhow::Result<()> { Ok(()) }
+}
+
+iii::register_worker!("iii-my-custom", MyWorker, enabled_by_default = true);
+```
+
+
+
+### Custom adapter
+
+
+
+```rust
+iii::register_adapter!(
+
+ "modules::my_worker::MyAdapter",
+ make_adapter
+);
+```
+
+
+```rust
+iii::register_adapter!(
+
+ name: "my-adapter",
+ make_adapter
+);
+```
+
+
+
+---
+
+## Migration checklist
+
+- [ ] Rename `modules:` to `workers:` in all config YAML files
+- [ ] Replace `class:` with `name:` using the new short identifiers (see tables above)
+- [ ] Replace adapter `class:` with `name:` using short identifiers (`kv`, `redis`, `builtin`, etc.)
+- [ ] Update Rust imports from `iii::modules::` to `iii::workers::`
+- [ ] Update `iii::modules::module::Module` to `iii::workers::traits::Worker`
+- [ ] Replace `register_module!` with `register_worker!`
+- [ ] Replace `add_module()` / `register_module()` on `EngineBuilder` with `add_worker()` / `register_worker()`
+- [ ] Rename any `*Module` / `*CoreModule` struct references to the new `*Worker` / `*Manager` names
+- [ ] Update `crate::workers::Worker` references to `crate::worker_connections::WorkerConnection`
diff --git a/docs/0-13-0/changelog/0-11-0/migrated-examples.mdx b/docs/0-13-0/changelog/0-11-0/migrated-examples.mdx
new file mode 100644
index 000000000..002ac5dea
--- /dev/null
+++ b/docs/0-13-0/changelog/0-11-0/migrated-examples.mdx
@@ -0,0 +1,71 @@
+---
+title: 'Examples migrated from Motia'
+description: 'Side-by-side links to the original Motia source and the iii-sdk port for every example we migrated as part of 0.11.0.'
+---
+
+These examples were ported from the Motia framework to **`iii-sdk@0.11.0`** following the [Motia → Node](/changelog/0-11-0/migrating-from-motia-js) and [Motia → Python](/changelog/0-11-0/migrating-from-motia-py) migration guides.
+
+Each row links the **original Motia source** alongside the **iii version** so you can read the same workflow in both shapes — every wrapper stripped, every primitive (`registerFunction`, `registerTrigger`, `iii.trigger`) made explicit.
+
+## Examples
+
+
+
+ Order approval workflow that pauses for a human and resumes via webhook. Demonstrates HTTP, durable subscribers, and state-driven workflow control.
+
+ **Before:** [`MotiaDev/motia-examples/.../human-in-the-loop`](https://github.com/MotiaDev/motia-examples/tree/main/examples/foundational/workflow-patterns/human-in-the-loop)
+ **After:** [`iii-hq/examples/human-in-the-loop`](https://github.com/iii-hq/examples/tree/main/human-in-the-loop)
+
+
+
+ Todo REST API with real-time stream mirror, durable topics for notifications/analytics/achievements, and cron jobs for cleanup and stats.
+
+ **Before:** [`MotiaDev/motia-examples/.../todo-app`](https://github.com/MotiaDev/motia-examples/tree/main/examples/foundational/api-patterns/todo-app)
+ **After:** [`iii-hq/examples/todo-app`](https://github.com/iii-hq/examples/tree/main/todo-app)
+
+
+
+ AI chat agent with conversation memory, web search tools, and live streaming responses through `stream::set` updates.
+
+ **Before:** [`MotiaDev/motia-examples/.../ai-chat-agent`](https://github.com/MotiaDev/motia-examples/tree/main/examples/foundational/ai-chat-agent)
+ **After:** [`iii-hq/examples/ai-chat-agent`](https://github.com/iii-hq/examples/tree/main/ai-chat-agent)
+
+
+
+ Multi-step property search agent in Python (`iii-sdk` async) — scraping, enrichment, neighbourhood analysis, and market trends as separate workers.
+
+ **Before:** [`MotiaDev/motia-examples/.../property-search-agent`](https://github.com/MotiaDev/motia-examples/tree/main/examples/agentic/property-search-agent)
+ **After:** [`iii-hq/examples/property-search-agent`](https://github.com/iii-hq/examples/tree/main/property-search-agent)
+
+
+
+## What stayed the same
+
+The business logic, topic names, function IDs, and trigger semantics carry over almost verbatim. If you can read the Motia version you can read the iii version.
+
+## What changed
+
+| Concern | Motia | iii-sdk@0.11.0 |
+| --- | --- | --- |
+| Setup | `motia` framework + auto file scan | `registerWorker(...)` + `import './handlers/...'` side effects in `src/main.ts` |
+| HTTP responses | `{ status, body }` | `{ status_code, body }` |
+| Path / query | `params`, `query` | `path_params`, `query_params` (values are `string \| string[]`) |
+| State | `stateManager.get/set/list` | `iii.trigger({ function_id: 'state::get' \| 'state::set' \| 'state::list' \| 'state::delete', ... })` |
+| Streams | `Stream` class | `iii.trigger({ function_id: 'stream::set' \| 'stream::delete' \| 'stream::list' \| ..., ... })` |
+| Queue publish | `emit({ topic, data })` | `iii.trigger({ function_id: 'iii::durable::publish', payload: { topic, data } })` |
+| Queue subscriber | `EventConfig.subscribes` | `registerTrigger({ type: 'durable:subscriber', config: { topic } })` |
+| Cron expression | 5-field `cron` | 7-field `expression` (sec min hour dom month dow year) |
+| HTTP path style | `/orders/validate` | `/orders/validate` (leading `/` is **required** in iii) |
+
+## Try them locally
+
+Each folder ships with its own `iii-config.yaml`, `package.json`/`pyproject.toml`, and a smoke-test script (`test-*.sh`) so you can run the engine + worker and exercise the full flow in one command.
+
+```bash
+git clone https://github.com/iii-hq/examples
+cd examples/todo-app
+pnpm install --ignore-workspace
+iii --config ./iii-config.yaml
+# then in another terminal
+bash test-todo-flow.sh
+```
diff --git a/docs/0-13-0/changelog/0-11-0/migrating-from-motia-js.mdx b/docs/0-13-0/changelog/0-11-0/migrating-from-motia-js.mdx
new file mode 100644
index 000000000..dca196573
--- /dev/null
+++ b/docs/0-13-0/changelog/0-11-0/migrating-from-motia-js.mdx
@@ -0,0 +1,648 @@
+---
+title: 'Migrating from Motia (Node.js)'
+description: 'How to move from the Motia framework to iii-sdk directly in Node.js.'
+---
+
+Motia was a higher-level framework built on top of `iii-sdk`. It handled file scanning, middleware wiring, trigger registration, and production bundling automatically. These conveniences came at a cost: they hid iii's three core primitives — **Workers**, **Functions**, and **Triggers** — behind opaque abstractions that limited what you could build.
+
+By moving to `iii-sdk` directly, you unlock the full power of the engine:
+
+- **Add new workers** that register their own functions and triggers, enabling multi-worker orchestration across services, languages, and runtimes.
+- **Connect from the browser** using `iii-browser-sdk` with [Worker RBAC](/0-11-0/how-to/worker-rbac) for secure, real-time frontends — no REST layer needed. See [Use iii in the browser](/0-11-0/how-to/use-iii-in-the-browser).
+- **Treat Motia as one worker among many** instead of a standalone monolith. Your existing Motia code becomes just another worker in a larger iii deployment.
+- **Understand the primitives directly.** Working with `registerFunction`, `registerTrigger`, and `registerWorker` builds a mental model that transfers across all iii SDKs and documentation.
+
+Before diving into this migration, we recommend reading the [iii documentation](/0-11-0/) to understand Workers, Functions, and Triggers. The [quickstart](/0-11-0/quickstart) and [Everything is a Worker](/changelog/0-11-0/everything-is-a-worker) pages are good starting points.
+
+---
+
+## Step 1 — Update dependencies
+
+Remove `motia` and add `iii-sdk`. If you need a production bundler, add `esbuild` as a dev dependency.
+
+```bash
+pnpm remove motia
+pnpm add iii-sdk@0.11.0
+pnpm add -D esbuild
+```
+
+| Before (Motia) | After (iii-sdk) |
+|---|---|
+| `motia` | `iii-sdk@0.11.0` |
+| `motia build` | `esbuild` via `bun run esbuild.config.ts` |
+| `motia dev && bun run dist/index.js` | `bun run --watch src/main.ts` |
+
+---
+
+## Step 2 — Initialize the SDK
+
+Create `src/lib/iii.ts`. This replaces the implicit connection that Motia managed for you.
+
+```typescript title="@/lib/iii"
+import { Logger, registerWorker } from 'iii-sdk'
+
+export const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134', {
+ workerName: 'api-worker',
+})
+
+export const logger = new Logger()
+```
+
+Every `import { logger } from 'motia'` in your codebase changes to `import { logger } from '@/lib/iii'`.
+
+---
+
+## Step 3 — Migrate handlers
+
+Motia auto-registered functions and triggers from exported `config` objects. With iii-sdk you call `registerFunction` and `registerTrigger` directly.
+
+### HTTP
+
+
+
+```typescript
+import { type Handlers, http, type StepConfig } from 'motia'
+import { z } from 'zod'
+
+export const config = {
+ name: 'Health',
+ triggers: [http('GET', '/health', { responseSchema: { 200: z.object({ ok: z.boolean() }) } })],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (_input, _ctx) => {
+ return { status: 200, body: { ok: true } }
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('health', async () => {
+ return { status_code: 200, body: { ok: true } }
+})
+
+iii.registerTrigger({
+ type: 'http',
+ function_id: ref.id,
+ config: { api_path: '/health', http_method: 'GET' },
+})
+```
+
+
+
+### HTTP with middleware
+
+
+
+```typescript
+import { type Handlers, http, type StepConfig } from 'motia'
+
+export const config = {
+ name: 'List Tags',
+ triggers: [http('GET', '/tag', { middleware: [requireAuth] })],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (input, ctx) => {
+ const { sub: userId, orgId } = input.tokenInfo
+ return { status: 200, body: { tags: allTags } }
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('list-tags', async (input) => {
+ const { sub: userId, orgId } = input.tokenInfo
+ return { status_code: 200, body: { tags: allTags } }
+})
+
+iii.registerTrigger({
+ type: 'http',
+ function_id: ref.id,
+ config: {
+ api_path: '/tag',
+ http_method: 'GET',
+ middleware_function_ids: ['middleware::auth'],
+ },
+})
+```
+
+Middleware functions must be registered separately with `registerFunction`. See [Use HTTP middleware](/0-11-0/how-to/use-http-middleware) for details.
+
+
+
+### Cron
+
+
+
+```typescript
+import { cron, type Handlers, type StepConfig } from 'motia'
+
+export const config = {
+ name: 'Daily Report',
+ triggers: [cron('0 0 9 * * * *')],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (_input, _ctx) => {
+ await generateReport()
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('daily-report', async () => {
+ await generateReport()
+})
+
+iii.registerTrigger({
+ type: 'cron',
+ function_id: ref.id,
+ config: { expression: '0 0 9 * * * *' },
+})
+```
+
+
+
+### Queue (durable subscriber)
+
+
+
+```typescript
+import { type Handlers, queue, type StepConfig } from 'motia'
+
+export const config = {
+ name: 'Process Order',
+ triggers: [queue('order.created')],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (input, _ctx) => {
+ await processOrder(input)
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('process-order', async (input) => {
+ await processOrder(input)
+})
+
+iii.registerTrigger({
+ type: 'durable:subscriber',
+ function_id: ref.id,
+ config: { topic: 'order.created' },
+})
+```
+
+
+
+To publish to a topic, use `iii.trigger` instead of Motia's `enqueue`:
+
+```typescript
+import { iii } from '@/lib/iii'
+
+iii.trigger({
+ function_id: 'iii::durable::publish',
+ payload: { topic: 'order.created', data: orderPayload },
+})
+```
+
+### State
+
+
+
+```typescript
+import { type Handlers, state, type StepConfig } from 'motia'
+
+export const config = {
+ name: 'On State Change',
+ triggers: [state()],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (input, _ctx) => {
+ await handleStateChange(input)
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('on-state-change', async (input) => {
+ await handleStateChange(input)
+})
+
+iii.registerTrigger({
+ type: 'state',
+ function_id: ref.id,
+ config: {},
+})
+```
+
+
+
+### Stream
+
+
+
+```typescript
+import { type Handlers, stream, type StepConfig } from 'motia'
+
+export const config = {
+ name: 'Chat Message',
+ triggers: [stream('chat', { groupId: 'room-1' })],
+ enqueues: [],
+} as const satisfies StepConfig
+
+export const handler: Handlers = async (input, _ctx) => {
+ await handleChatMessage(input)
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const ref = iii.registerFunction('chat-message', async (input) => {
+ await handleChatMessage(input)
+})
+
+iii.registerTrigger({
+ type: 'stream',
+ function_id: ref.id,
+ config: { stream_name: 'chat', group_id: 'room-1' },
+})
+```
+
+
+
+### Multiple triggers on one function
+
+A function can have multiple triggers. This was possible in Motia via the `triggers` array, and maps directly to multiple `registerTrigger` calls sharing the same `function_id`:
+
+```typescript
+const ref = iii.registerFunction('order-handler', async (input) => {
+ await handleOrder(input)
+})
+
+iii.registerTrigger({
+ type: 'http',
+ function_id: ref.id,
+ config: { api_path: '/orders', http_method: 'POST' },
+})
+
+iii.registerTrigger({
+ type: 'durable:subscriber',
+ function_id: ref.id,
+ config: { topic: 'order.retry' },
+})
+```
+
+### Stream and State operations
+
+In Motia, you used the `Stream` class and `stateManager` to read and write data. Under the hood, these called `iii.trigger()` with built-in function IDs (`stream::get`, `state::set`, etc.). With iii-sdk, you call `iii.trigger()` directly. See the [Stream](/0-11-0/workers/iii-stream) and [State](/0-11-0/workers/iii-state) worker docs for the full list of operations.
+
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const todo = await iii.trigger({
+ function_id: 'stream::get',
+ payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1' },
+})
+
+await iii.trigger({
+ function_id: 'stream::set',
+ payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1', data: { title: 'Buy milk' } },
+})
+
+await iii.trigger({
+ function_id: 'stream::delete',
+ payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1' },
+})
+
+const items = await iii.trigger({
+ function_id: 'stream::list',
+ payload: { stream_name: 'todos', group_id: 'user-1' },
+})
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+const theme = await iii.trigger({
+ function_id: 'state::get',
+ payload: { scope: 'settings', key: 'theme' },
+})
+
+await iii.trigger({
+ function_id: 'state::set',
+ payload: { scope: 'settings', key: 'theme', value: 'dark' },
+})
+
+await iii.trigger({
+ function_id: 'state::delete',
+ payload: { scope: 'settings', key: 'theme' },
+})
+
+const allSettings = await iii.trigger({
+ function_id: 'state::list',
+ payload: { scope: 'settings' },
+})
+```
+
+
+
+Additional function IDs: `stream::update`, `stream::list_groups`, `stream::send` for streams; `state::update`, `state::list_groups` for state.
+
+### Stream lifecycle hooks (`onJoin` / `onLeave`)
+
+In Motia, `*.stream.ts` files could define `onJoin` and `onLeave` callbacks inside the `StreamConfig`. The framework registered a single shared function for each hook and dispatched by stream name internally.
+
+With iii-sdk, you register these as regular functions with `stream:join` and `stream:leave` triggers. The handler receives a `StreamJoinLeaveEvent` with `{ stream_name, group_id, id, context }`.
+
+
+
+```typescript
+import { type StreamConfig } from 'motia'
+
+export const config: StreamConfig = {
+ name: 'todo',
+ schema: todoSchema,
+ baseConfig: { storageType: 'default' },
+
+ onJoin: async (subscription, _context, authContext) => {
+ return { unauthorized: false }
+ },
+
+ onLeave: async (subscription, _context, authContext) => {
+ // cleanup logic
+ },
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+iii.registerFunction('stream::on-join', async (event) => {
+ const { stream_name, group_id, id, context } = event
+ return { unauthorized: false }
+})
+
+iii.registerTrigger({
+ type: 'stream:join',
+ function_id: 'stream::on-join',
+ config: {},
+})
+
+iii.registerFunction('stream::on-leave', async (event) => {
+ const { stream_name, group_id, id, context } = event
+ // cleanup logic
+})
+
+iii.registerTrigger({
+ type: 'stream:leave',
+ function_id: 'stream::on-leave',
+ config: {},
+})
+```
+
+
+
+If you had multiple streams with different `onJoin` / `onLeave` logic, dispatch by `event.stream_name` inside the handler or register separate function IDs per stream.
+
+### Stream authentication (`authenticateStream`)
+
+In Motia, `motia.config.ts` exported an `authenticateStream` function that ran during WebSocket upgrade. With iii-sdk, you register a function directly and reference its ID in the engine's stream module config.
+
+
+
+```typescript
+// motia.config.ts
+import type { AuthenticateStream } from 'motia'
+
+export const authenticateStream: AuthenticateStream = async (req, context) => {
+ return { context: { userId: 'sergio' } }
+}
+```
+
+
+```typescript
+import { iii } from '@/lib/iii'
+
+iii.registerFunction('stream::authenticate', async (input) => {
+ const { headers, path, query_params, addr } = input
+ return { context: { userId: 'sergio' } }
+})
+```
+
+
+
+The engine calls this function during WebSocket upgrade. Reference the function ID in your `config.yaml`:
+
+```yaml
+workers:
+ - name: iii-stream
+ config:
+ auth_function: stream::authenticate
+```
+
+Key differences:
+
+- Motia used `queryParams` (camelCase); iii-sdk uses `query_params` (snake_case).
+- No trigger registration is needed for auth — it is config-driven via `auth_function`.
+- The `motia.config.ts` file is no longer needed; delete it.
+
+---
+
+## Step 4 — Update request and response shapes
+
+### Request properties
+
+Motia wrapped iii-sdk's HTTP request into friendlier property names. With iii-sdk you receive the raw shape.
+
+| Property | Motia (`input.*`) | iii-sdk (`input.*`) |
+|---|---|---|
+| Route params | `params` | `path_params` |
+| Query string | `query` | `query_params` |
+| JSON body | `body` | `body` |
+| Headers | `headers` | `headers` |
+| HTTP method | `method` | `method` |
+| Request stream | `requestBody` | `request_body` |
+| Response stream | `response` | `response` |
+
+```typescript
+// Before (Motia)
+const { folderId } = input.params || {}
+const parentFolderId = input.query?.parentFolderId
+
+// After (iii-sdk)
+const { folderId } = input.path_params || {}
+const parentFolderId = input.query_params?.parentFolderId
+```
+
+### `query_params` value types
+
+In iii-sdk, `query_params` values are `string | string[]`. Use a helper to safely extract the first value:
+
+```typescript
+function firstQueryParam(
+ queryParams: Record | undefined,
+ name: string,
+): string | undefined {
+ const v = queryParams?.[name]
+ if (v === undefined) return undefined
+ return Array.isArray(v) ? v[0] : v
+}
+```
+
+### Response shape
+
+iii-sdk uses `status_code` instead of `status`. This applies to every handler return and middleware response.
+
+```typescript
+// Before (Motia)
+return { status: 200, body: { tags: allTags } }
+return { status: 404, body: { error: 'Not found' } }
+
+// After (iii-sdk)
+return { status_code: 200, body: { tags: allTags } }
+return { status_code: 404, body: { error: 'Not found' } }
+```
+
+---
+
+## Step 5 — Set up the entry point
+
+Motia discovered `.step.ts` files automatically. With iii-sdk, you create a single entry point that imports all handler files as side effects.
+
+Create `src/main.ts`:
+
+```typescript
+import './lib/iii'
+import './handlers/health'
+import './handlers/auth'
+import './handlers/orders/create-order'
+import './handlers/orders/list-orders'
+import './handlers/reports/daily-report'
+// ... import every handler file
+```
+
+Each import executes the `registerFunction` and `registerTrigger` calls at the module level, registering the function and its triggers with the engine.
+
+### File renaming
+
+Rename all `*.step.ts` files to `*.ts`:
+
+```
+src/steps/health.step.ts → src/handlers/health.ts
+src/steps/auth.step.ts → src/handlers/auth.ts
+```
+
+The directory structure is yours to decide — `handlers/`, `routes/`, `rest/`, or any layout that fits your project.
+
+---
+
+## Step 6 — Development workflow
+
+Replace Motia's dev command with a direct `bun` watcher. In your `config.yaml`, configure the worker under `iii-exec`:
+
+```yaml
+workers:
+ - name: iii-exec
+ config:
+ exec:
+ - bun run --watch src/main.ts
+```
+
+Run the engine, and it will start your worker process with file watching built in.
+
+---
+
+## Step 7 — Production build
+
+Motia had `motia build`. Replace it with a plain esbuild config.
+
+Add to `package.json`:
+
+```json
+{
+ "scripts": {
+ "build": "bun run esbuild.config.ts"
+ }
+}
+```
+
+Create `esbuild.config.ts`:
+
+```typescript
+import * as esbuild from 'esbuild'
+
+esbuild.build({
+ entryPoints: ['src/main.ts'],
+ outfile: 'dist/index-production.js',
+ bundle: true,
+ platform: 'node',
+ target: ['node22'],
+ format: 'esm',
+ minify: true,
+ sourcemap: true,
+ external: ['ws'],
+}).catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
+```
+
+Key decisions:
+
+- **`bundle: true`** — all application code and npm dependencies are bundled into a single file.
+- **`external: ['ws']`** — the `ws` package has native addons and must remain in `node_modules` at runtime.
+- **`sourcemap: true`** — run production with `bun run --enable-source-maps dist/index-production.js` so stack traces map back to TypeScript source.
+- **`platform: 'node'`** — Node.js built-ins (`fs`, `path`, `crypto`, etc.) are treated as external.
+- **`format: 'esm'`** — matches `"type": "module"` in your `package.json`.
+
+Update your production config to run the bundled output:
+
+```yaml
+workers:
+ - name: iii-exec
+ config:
+ exec:
+ - bun run --enable-source-maps dist/index-production.js
+```
+
+---
+
+## Migration checklist
+
+- [ ] Remove `motia` from dependencies, add `iii-sdk@0.11.0`
+- [ ] Add `esbuild` as a dev dependency
+- [ ] Create `src/lib/iii.ts` with `registerWorker` and `Logger`
+- [ ] Rename all `*.step.ts` files to `*.ts`
+- [ ] Replace all `import { ... } from 'motia'` with iii-sdk imports
+- [ ] Convert every `config` + `handler` export to `registerFunction` + `registerTrigger` calls
+- [ ] Replace `status` with `status_code` in every handler return
+- [ ] Replace `params` with `path_params` and `query` with `query_params`
+- [ ] Replace `requestBody` with `request_body` in streaming handlers
+- [ ] Replace `Stream` and `stateManager` usage with `iii.trigger()` calls or custom wrappers
+- [ ] Migrate stream `onJoin` / `onLeave` hooks to `registerFunction` + `registerTrigger` (`stream:join` / `stream:leave`)
+- [ ] Migrate `authenticateStream` from `motia.config.ts` to a registered function and set `auth_function` in `config.yaml`
+- [ ] Delete `motia.config.ts`
+- [ ] Create `src/main.ts` with side-effect imports for every handler
+- [ ] Replace `motia dev` with `bun run --watch src/main.ts` in your dev config
+- [ ] Replace `motia build` with an esbuild config
+- [ ] Test all endpoints, cron jobs, queue subscribers, and stream handlers
diff --git a/docs/0-13-0/changelog/0-11-0/migrating-from-motia-py.mdx b/docs/0-13-0/changelog/0-11-0/migrating-from-motia-py.mdx
new file mode 100644
index 000000000..76cd6e7e4
--- /dev/null
+++ b/docs/0-13-0/changelog/0-11-0/migrating-from-motia-py.mdx
@@ -0,0 +1,667 @@
+---
+title: 'Migrating from Motia (Python)'
+description: 'How to move from the Motia framework to iii-sdk directly in Python.'
+---
+
+Motia was a higher-level framework built on top of `iii-sdk`. It handled file scanning, middleware wiring, trigger registration, and production packaging automatically. These conveniences came at a cost: they hid iii's three core primitives — **Workers**, **Functions**, and **Triggers** — behind opaque abstractions that limited what you could build.
+
+By moving to `iii-sdk` directly, you unlock the full power of the engine:
+
+- **Add new workers** that register their own functions and triggers, enabling multi-worker orchestration across services, languages, and runtimes.
+- **Connect from the browser** using `iii-browser-sdk` with [Worker RBAC](/0-11-0/how-to/worker-rbac) for secure, real-time frontends — no REST layer needed. See [Use iii in the browser](/0-11-0/how-to/use-iii-in-the-browser).
+- **Treat Motia as one worker among many** instead of a standalone monolith. Your existing Motia code becomes just another worker in a larger iii deployment.
+- **Understand the primitives directly.** Working with `register_function`, `register_trigger`, and `register_worker` builds a mental model that transfers across all iii SDKs and documentation.
+
+Before diving into this migration, we recommend reading the [iii documentation](/0-11-0/) to understand Workers, Functions, and Triggers. The [quickstart](/0-11-0/quickstart) and [Everything is a Worker](/changelog/0-11-0/everything-is-a-worker) pages are good starting points.
+
+For the Node / TypeScript migration, see [Migrating from Motia (Node.js)](/changelog/0-11-0/migrating-from-motia-js).
+
+---
+
+## Step 1 — Update dependencies
+
+Remove `motia` and add `iii-sdk`.
+
+```bash
+pip uninstall motia
+pip install iii-sdk==0.11.0
+```
+
+| Before (Motia) | After (iii-sdk) |
+|---|---|
+| `motia` (pip) | `iii-sdk==0.11.0` (pip) |
+| `motia build` | `python -m build` or a container image |
+| `motia dev` | `python -m src.main` with a file watcher |
+
+The Python package is distributed as `iii-sdk` on PyPI but imported as `iii` (e.g., `from iii import register_worker`).
+
+---
+
+## Step 2 — Initialize the SDK
+
+Create `src/lib/iii_client.py`. This replaces the implicit connection that Motia managed for you.
+
+```python
+import os
+from iii import Logger, register_worker, InitOptions
+
+iii = register_worker(
+ address=os.environ.get("III_URL", "ws://localhost:49134"),
+ options=InitOptions(worker_name="api-worker"),
+)
+
+logger = Logger()
+```
+
+Every `from motia import logger` in your codebase changes to `from src.lib.iii_client import logger`.
+
+---
+
+## Step 3 — Register functions and triggers directly
+
+Motia auto-registered functions and triggers from exported `config` objects. With `iii-sdk` Python, you call `iii.register_function` and `iii.register_trigger` directly at module scope — there's no helper to build. Each handler module registers its function and trigger(s) when imported. This mirrors the Motia `config` + `handler` structure without the implicit discovery.
+
+Continue to Step 4 to see the handler patterns.
+
+`iii-sdk` Python supports engine-native HTTP middleware via `middleware_function_ids`. See [Use HTTP middleware](/0-11-0/how-to/use-http-middleware) for the full pattern.
+
+---
+
+## Step 4 — Migrate handlers
+
+### HTTP
+
+
+
+```python
+from motia import http, ApiRequest, ApiResponse
+
+config = {
+ "name": "Health",
+ "description": "Health check",
+ "triggers": [http("GET", "/health")],
+ "enqueues": [],
+}
+
+def handler(_request: ApiRequest) -> ApiResponse:
+ return ApiResponse(status=200, body={"ok": True})
+```
+
+
+```python
+def health(_data):
+ return {"statusCode": 200, "body": {"ok": True}}
+
+iii.register_function("health", health)
+iii.register_trigger({
+ "type": "http",
+ "function_id": "health",
+ "config": {"api_path": "/health", "http_method": "GET"},
+})
+```
+
+
+
+### HTTP with middleware
+
+
+
+```python
+from motia import http, ApiRequest, ApiResponse
+
+def require_auth(request: ApiRequest) -> bool:
+ return request.headers.get("authorization") is not None
+
+config = {
+ "name": "ListTags",
+ "description": "List tags",
+ "triggers": [http("GET", "/tag", middleware=[require_auth])],
+ "enqueues": [],
+}
+
+def handler(request: ApiRequest) -> ApiResponse:
+ org_id = request.headers.get("x-org-id")
+ return ApiResponse(status=200, body={"tags": list_tags_for_org(org_id)})
+```
+
+
+```python
+from src.middleware.auth import require_auth # registers "middleware::require-auth"
+
+def list_tags(data):
+ headers = data.get("headers") or {}
+ org_id = headers.get("x-org-id")
+ return {"statusCode": 200, "body": {"tags": list_tags_for_org(org_id)}}
+
+iii.register_function("list-tags", list_tags)
+iii.register_trigger({
+ "type": "http",
+ "function_id": "list-tags",
+ "config": {
+ "api_path": "/tag",
+ "http_method": "GET",
+ "middleware_function_ids": ["middleware::require-auth"],
+ },
+})
+```
+
+
+
+`iii-sdk` Python uses engine-native middleware registered as regular functions with a `middleware::` prefix. See [Use HTTP middleware](/0-11-0/how-to/use-http-middleware) for the full pattern.
+
+### Cron
+
+
+
+```python
+from motia import cron, logger
+
+config = {
+ "name": "DailyReport",
+ "description": "Generate daily report",
+ "triggers": [cron("0 0 9 * * * *")],
+ "enqueues": [],
+}
+
+def handler(input: None) -> None:
+ generate_report()
+```
+
+
+```python
+def daily_report(_data) -> None:
+ generate_report()
+
+iii.register_function("daily-report", daily_report)
+iii.register_trigger({
+ "type": "cron",
+ "function_id": "daily-report",
+ "config": {"expression": "0 0 9 * * * *"},
+})
+```
+
+
+
+### Queue (durable subscriber)
+
+
+
+```python
+from motia import queue, logger
+from typing import Any
+
+config = {
+ "name": "ProcessOrder",
+ "description": "Process created orders",
+ "triggers": [queue("order.created")],
+ "enqueues": [],
+}
+
+def handler(input: Any) -> None:
+ process_order(input)
+```
+
+
+```python
+def process_order(data) -> None:
+ process_order_impl(data)
+
+iii.register_function("process-order", process_order)
+iii.register_trigger({
+ "type": "durable:subscriber",
+ "function_id": "process-order",
+ "config": {"topic": "order.created"},
+})
+```
+
+
+
+To publish to a topic, use `iii.trigger` instead of Motia's `enqueue`:
+
+```python
+iii.trigger({
+ "function_id": "iii::durable::publish",
+ "payload": {"topic": "order.created", "data": order_payload},
+})
+```
+
+### State
+
+
+
+```python
+from motia import StateTriggerInput, state, logger
+
+config = {
+ "name": "OnStateChange",
+ "description": "React to state changes",
+ "triggers": [state()],
+ "enqueues": [],
+}
+
+def handler(input: StateTriggerInput) -> None:
+ handle_state_change(input)
+```
+
+
+```python
+def on_state_change(data) -> None:
+ handle_state_change(data)
+
+iii.register_function("on-state-change", on_state_change)
+iii.register_trigger({
+ "type": "state",
+ "function_id": "on-state-change",
+ "config": {},
+})
+```
+
+
+
+### Stream
+
+
+
+```python
+from motia import StreamTriggerInput, stream, logger
+
+config = {
+ "name": "ChatMessage",
+ "description": "Handle chat messages",
+ "triggers": [stream("chat", group_id="room-1")],
+ "enqueues": [],
+}
+
+def handler(input: StreamTriggerInput) -> None:
+ handle_chat_message(input)
+```
+
+
+```python
+def chat_message(data) -> None:
+ handle_chat_message(data)
+
+iii.register_function("chat-message", chat_message)
+iii.register_trigger({
+ "type": "stream",
+ "function_id": "chat-message",
+ "config": {"stream_name": "chat", "group_id": "room-1"},
+})
+```
+
+
+
+### Multiple triggers on one function
+
+A function can be driven by multiple triggers. This was possible in Motia via the `triggers` array, and maps directly to multiple `register_trigger` calls for the same `function_id`:
+
+```python
+def order_handler(data) -> None:
+ handle_order(data)
+
+iii.register_function("order-handler", order_handler)
+iii.register_trigger({
+ "type": "http",
+ "function_id": "order-handler",
+ "config": {"api_path": "/orders", "http_method": "POST"},
+})
+iii.register_trigger({
+ "type": "durable:subscriber",
+ "function_id": "order-handler",
+ "config": {"topic": "order.retry"},
+})
+```
+
+### Stream and State operations
+
+In Motia, you used the `Stream` class and `stateManager` singleton to read and write data. Under the hood, these called `iii.trigger()` with built-in function IDs (`stream::get`, `state::set`, etc.). With iii-sdk, you call `iii.trigger()` directly. See the [Stream](/0-11-0/workers/iii-stream) and [State](/0-11-0/workers/iii-state) worker docs for the full list of operations.
+
+
+
+```python
+from src.lib.iii_client import iii
+
+todo = iii.trigger({
+ "function_id": "stream::get",
+ "payload": {"stream_name": "todos", "group_id": "user-1", "item_id": "todo-1"},
+})
+
+iii.trigger({
+ "function_id": "stream::set",
+ "payload": {"stream_name": "todos", "group_id": "user-1", "item_id": "todo-1", "data": {"title": "Buy milk"}},
+})
+
+iii.trigger({
+ "function_id": "stream::delete",
+ "payload": {"stream_name": "todos", "group_id": "user-1", "item_id": "todo-1"},
+})
+
+items = iii.trigger({
+ "function_id": "stream::list",
+ "payload": {"stream_name": "todos", "group_id": "user-1"},
+})
+```
+
+
+```python
+from src.lib.iii_client import iii
+
+theme = iii.trigger({
+ "function_id": "state::get",
+ "payload": {"scope": "settings", "key": "theme"},
+})
+
+iii.trigger({
+ "function_id": "state::set",
+ "payload": {"scope": "settings", "key": "theme", "value": "dark"},
+})
+
+iii.trigger({
+ "function_id": "state::delete",
+ "payload": {"scope": "settings", "key": "theme"},
+})
+
+all_settings = iii.trigger({
+ "function_id": "state::list",
+ "payload": {"scope": "settings"},
+})
+```
+
+
+
+Additional function IDs: `stream::update`, `stream::list_groups`, `stream::send` for streams; `state::update`, `state::list_groups` for state.
+
+### Stream lifecycle hooks (`onJoin` / `onLeave`)
+
+In Motia, stream files could define `onJoin` and `onLeave` callbacks inside the stream config. The framework registered a single shared function for each hook and dispatched by stream name internally.
+
+With iii-sdk, you register these as regular functions with `stream:join` and `stream:leave` triggers. The handler receives a dict with `stream_name`, `group_id`, `id`, and `context`.
+
+
+
+```python
+config = {
+ "name": "todo",
+ "schema": todo_schema,
+ "base_config": {"storage_type": "default"},
+}
+
+def on_join(subscription, context, auth_context):
+ return {"unauthorized": False}
+
+def on_leave(subscription, context, auth_context):
+ pass # cleanup logic
+```
+
+
+```python
+from src.lib.iii_client import iii
+
+def stream_on_join(event):
+ stream_name = event.get("stream_name")
+ group_id = event.get("group_id")
+ item_id = event.get("id")
+ context = event.get("context")
+ return {"unauthorized": False}
+
+iii.register_function("stream::on-join", stream_on_join)
+iii.register_trigger({
+ "type": "stream:join",
+ "function_id": "stream::on-join",
+ "config": {},
+})
+
+def stream_on_leave(event):
+ stream_name = event.get("stream_name")
+ group_id = event.get("group_id")
+ item_id = event.get("id")
+ context = event.get("context")
+ # cleanup logic
+
+iii.register_function("stream::on-leave", stream_on_leave)
+iii.register_trigger({
+ "type": "stream:leave",
+ "function_id": "stream::on-leave",
+ "config": {},
+})
+```
+
+
+
+If you had multiple streams with different `onJoin` / `onLeave` logic, dispatch by `event["stream_name"]` inside the handler or register separate function IDs per stream.
+
+### Stream authentication (`authenticateStream`)
+
+In Motia, `motia_config.py` (or `motia.config.ts` for JS projects) exported an authentication function that ran during WebSocket upgrade. With iii-sdk, you register a function directly and reference its ID in the engine's stream module config.
+
+
+
+```python
+# motia_config.py
+def authenticate_stream(req, context):
+ return {"context": {"userId": "sergio"}}
+```
+
+
+```python
+from src.lib.iii_client import iii
+
+def stream_authenticate(data):
+ headers = data.get("headers")
+ path = data.get("path")
+ query_params = data.get("query_params")
+ addr = data.get("addr")
+ return {"context": {"userId": "sergio"}}
+
+iii.register_function("stream::authenticate", stream_authenticate)
+```
+
+
+
+The engine calls this function during WebSocket upgrade. Reference the function ID in your `config.yaml`:
+
+```yaml
+workers:
+ - name: iii-stream
+ config:
+ auth_function: stream::authenticate
+```
+
+Key differences:
+
+- Motia used `queryParams` (camelCase); iii-sdk uses `query_params` (snake_case).
+- No trigger registration is needed for auth — it is config-driven via `auth_function`.
+- The `motia_config.py` file is no longer needed; delete it.
+
+---
+
+## Step 5 — Update request and response shapes
+
+### Request properties
+
+Motia wrapped `iii-sdk`'s HTTP request into friendlier property names. With `iii-sdk` you receive the raw shape.
+
+| Property | Motia (`input.*`) | iii-sdk (`input.*`) |
+|---|---|---|
+| Route params | `params` | `path_params` |
+| Query string | `query` | `query_params` |
+| JSON body | `body` | `body` |
+| Headers | `headers` | `headers` |
+| HTTP method | `method` | `method` |
+| Request stream | `requestBody` | `request_body` |
+| Response stream | `response` | `response` |
+
+Motia Python's `ApiRequest` already exposed `path_params`, `query_params`, `body`, `headers`, `method`. When switching to `iii-sdk`, handlers receive those fields in a **plain dict** instead of a Pydantic model, so attribute access (`request.path_params`) becomes dict-key access (`data["path_params"]`).
+
+```python
+# Before (Motia — Pydantic model, attribute access)
+folder_id = request.path_params.get("folderId")
+parent_folder_id = request.query_params.get("parentFolderId")
+
+# After (iii-sdk — raw dict, key access)
+folder_id = (data.get("path_params") or {}).get("folderId")
+parent_folder_id = (data.get("query_params") or {}).get("parentFolderId")
+```
+
+### `query_params` value types
+
+In `iii-sdk`, `query_params` values are `str | list[str]`. Use a helper to safely extract the first value:
+
+```python
+def first_query_param(data: dict, name: str) -> str | None:
+ value = (data.get("query_params") or {}).get(name)
+ if value is None:
+ return None
+ return value[0] if isinstance(value, list) else value
+```
+
+### Response shape
+
+`iii-sdk` uses `statusCode` instead of `status`. This applies to every handler return and middleware response.
+
+```python
+# Before (Motia)
+return ApiResponse(status=200, body={"tags": all_tags})
+return ApiResponse(status=404, body={"error": "Not found"})
+
+# After (iii-sdk)
+return {"statusCode": 200, "body": {"tags": all_tags}}
+return {"statusCode": 404, "body": {"error": "Not found"}}
+```
+
+---
+
+## Step 6 — Set up the entry point
+
+Motia discovered `*_step.py` files automatically. With `iii-sdk` Python, create `src/main.py` that imports every handler module as a side effect.
+
+```python
+# src/main.py
+import signal
+import time
+
+from src.lib.iii_client import iii # noqa: F401 — connects on import
+
+# Import every handler module; each registers its function + trigger at import time.
+from src.handlers import health # noqa: F401
+from src.handlers import auth # noqa: F401
+from src.handlers.orders import create_order # noqa: F401
+from src.handlers.orders import list_orders # noqa: F401
+from src.handlers.reports import daily_report # noqa: F401
+# ... import every handler module
+
+
+def _shutdown(*_args) -> None:
+ iii.shutdown()
+ raise SystemExit(0)
+
+
+if __name__ == "__main__":
+ signal.signal(signal.SIGINT, _shutdown)
+ signal.signal(signal.SIGTERM, _shutdown)
+ while True:
+ time.sleep(1)
+```
+
+Each import executes the `register_function` and `register_trigger` calls at the module level, registering the function and its triggers with the engine.
+
+### File renaming
+
+Rename all `*_step.py` files to remove the `_step` suffix:
+
+```
+src/steps/health_step.py → src/handlers/health.py
+src/steps/auth_step.py → src/handlers/auth.py
+```
+
+The directory structure is yours to decide — `handlers/`, `routes/`, `rest/`, or any layout that fits your project.
+
+---
+
+## Step 7 — Development workflow
+
+The `iii-exec` worker has a built-in file watcher. In your `config.yaml`, declare `watch` globs alongside the `exec` command — no extra dependencies, no wrapper process:
+
+```yaml
+workers:
+ - name: iii-exec
+ config:
+ watch:
+ - src/**/*.py
+ exec:
+ - python
+ - -m
+ - src.main
+```
+
+Any change matching a `watch` glob restarts the `exec` pipeline.
+
+---
+
+## Step 8 — Production build
+
+Python has no direct `esbuild` equivalent. The recommended production path is a virtualenv (or container image) with pinned dependencies.
+
+Add to `pyproject.toml`:
+
+```toml
+[project]
+name = "my-worker"
+version = "0.1.0"
+dependencies = [
+ "iii-sdk==0.11.0",
+]
+```
+
+Build and run:
+
+```bash
+pip install .
+python -m src.main
+```
+
+For containerized deploys, a minimal `Dockerfile`:
+
+```dockerfile
+FROM python:3.11-slim
+WORKDIR /app
+COPY pyproject.toml ./
+RUN pip install --no-cache-dir .
+COPY src ./src
+CMD ["python", "-m", "src.main"]
+```
+
+Update your production config to run the installed module (omit `watch` in production):
+
+```yaml
+workers:
+ - name: iii-exec
+ config:
+ exec:
+ - python
+ - -m
+ - src.main
+```
+
+Unlike `esbuild`, you do not bundle Python sources — the runtime imports modules directly. Ship your source tree (or a wheel) rather than a single file.
+
+---
+
+## Migration checklist
+
+- [ ] Remove `motia` from `requirements.txt` / `pyproject.toml`, add `iii-sdk==0.11.0`
+- [ ] Create `src/lib/iii_client.py` with `register_worker` and `Logger`
+- [ ] Rename all `*_step.py` files to drop the `_step` suffix
+- [ ] Replace all `from motia import ...` with `from iii import ...` (and `from src.lib.iii_client import ...` for `iii` / `logger`)
+- [ ] Convert every `config` dict + `handler` function to `iii.register_function(...)` + `iii.register_trigger(...)` calls
+- [ ] Replace `ApiResponse(status=...)` with plain dicts using `statusCode` (e.g., `{"statusCode": 200, "body": ...}`)
+- [ ] Replace `request.path_params` / `request.query_params` attribute access with dict-key access on the raw payload (`data["path_params"]`, `data.get("query_params")`)
+- [ ] Replace `requestBody` with `request_body` in streaming handlers
+- [ ] Replace `Stream` and `stateManager` usage with `iii.trigger()` calls or custom wrappers
+- [ ] Migrate stream `on_join` / `on_leave` hooks to `register_function` + `register_trigger` (`stream:join` / `stream:leave`)
+- [ ] Migrate `authenticate_stream` from `motia_config.py` to a registered function and set `auth_function` in `config.yaml`
+- [ ] Delete `motia_config.py`
+- [ ] Create `src/main.py` with side-effect imports for every handler module
+- [ ] Replace `motia dev` with an `iii-exec` worker using `watch` + `exec` in your `config.yaml`
+- [ ] Replace `motia build` with `pip install .` (and a `Dockerfile` for containerized deploys)
+- [ ] Test all endpoints, cron jobs, queue subscribers, and stream handlers
diff --git a/docs/0-13-0/changelog/index.mdx b/docs/0-13-0/changelog/index.mdx
new file mode 100644
index 000000000..1819757cc
--- /dev/null
+++ b/docs/0-13-0/changelog/index.mdx
@@ -0,0 +1,198 @@
+---
+title: "Changelog"
+description: "Product updates and announcements for iii."
+owner: "devrel"
+type: "reference"
+---
+
+
+ ## Telemetry re-exports removed from public SDK surface
+
+ **Breaking.** Convenience re-exports of OpenTelemetry accessors were dropped from the Rust, Node, Python, and browser SDKs. Underlying behavior is unchanged — only the public surface is smaller. Users who need a tracer or meter directly should depend on the OpenTelemetry library for their language.
+
+ Removed symbols by language:
+
+ | Symbol | Rust (`iii::*`) | Node (`iii-sdk/telemetry`) | Python (`iii.telemetry` / `iii.logger`) | Browser |
+ |---|---|---|---|---|
+ | `get_tracer` / `getTracer` | dropped (still at `iii::telemetry::get_tracer`) | dropped | renamed `_get_tracer` | already absent (asserted) |
+ | `get_meter` / `getMeter` | dropped (still at `iii::telemetry::get_meter`) | dropped | renamed `_get_meter` | already absent (asserted) |
+ | `is_initialized` | dropped (still at `iii::telemetry::is_initialized`) | n/a | renamed `_is_initialized` | already absent (asserted) |
+ | `SpanKind` | dropped (use `opentelemetry::trace::SpanKind`) | n/a | n/a | already absent (asserted) |
+ | `SpanStatus` / `SpanStatusCode` | dropped (use `opentelemetry::trace::Status`) | dropped | n/a | already absent (asserted) |
+
+ ### Migration
+
+ - For custom spans, prefer `withSpan` / `with_span` / `run_in_span`. These preserve trace context.
+ - To obtain a tracer or meter directly, depend on `@opentelemetry/api` (Node) or the `opentelemetry` crate / Python package and call its accessors. Rust users can also keep using `iii::telemetry::get_tracer` / `iii::telemetry::get_meter`.
+
+ ```typescript
+ // Before (Node)
+ import { getTracer, getMeter, SpanStatusCode } from 'iii-sdk/telemetry'
+
+ // After (Node)
+ import { trace, metrics, SpanStatusCode } from '@opentelemetry/api'
+ const tracer = trace.getTracer('my-service')
+ const meter = metrics.getMeter('my-service')
+ ```
+
+ ```rust
+ // Before (Rust)
+ use iii::{get_tracer, get_meter, SpanKind, SpanStatus};
+
+ // After (Rust)
+ use opentelemetry::global;
+ use opentelemetry::trace::{SpanKind, Status};
+ let meter = global::meter("my-service");
+ ```
+
+ ```python
+ # Before (Python)
+ from iii.telemetry import get_tracer, get_meter, is_initialized
+
+ # After (Python)
+ from opentelemetry import trace, metrics
+ tracer = trace.get_tracer("my-service")
+ meter = metrics.get_meter("my-service")
+ ```
+
+
+
+ ## `iii sandbox` subcommand removed
+
+ **Breaking.** The `iii sandbox` CLI subcommand is gone. Every sandbox operation now goes through `iii trigger`:
+
+ ```bash
+ # before
+ iii sandbox create python --idle-timeout 300
+ iii sandbox exec "$SB" -- python3 -c 'print(2+2)'
+ iii sandbox stop "$SB"
+
+ # after
+ SB=$(iii trigger sandbox::create image=python idle_timeout_secs=300 | jq -r .sandbox_id)
+ iii trigger sandbox::exec sandbox_id="$SB" cmd=python3 args='["-c","print(2+2)"]'
+ iii trigger sandbox::stop sandbox_id="$SB"
+ ```
+
+ Each call also accepts a single `--json ''` payload (e.g. `iii trigger sandbox::exec --json '{"sandbox_id":"…","cmd":"python3","args":["-c","print(2+2)"]}'`), equivalent to the kv form shown above.
+
+ `iii trigger` is request/response only, so the streaming flows the old subcommand offered (`exec` stdout/stderr stream, `upload`, `download`) are no longer available from the terminal. Use the SDK from worker code for those: `sandbox::exec` and `sandbox::fs::write` / `sandbox::fs::read` still expose the streaming channel.
+
+ ## `iii trigger` reshape
+
+ **Breaking.** `iii trigger` no longer accepts `--function-id` and `--payload`. The new form takes the function path as a positional argument and accepts payload fields as `key=value` tokens, an `--json ''` flag, or both:
+
+ ```bash
+ # kv form
+ iii trigger orders::process amount=149.99 currency=USD
+
+ # JSON form
+ iii trigger orders::process --json '{"amount": 149.99, "currency": "USD"}'
+
+ # Combined: --json is the base, kv overrides individual keys
+ iii trigger orders::process --json '{"amount": 100}' amount=149.99
+ ```
+
+ See [Triggers](/using-iii/triggers) for the full reference.
+
+ ## `iii update --list-targets`
+
+ `iii update` now exposes a `--list-targets` flag that prints every target accepted by `iii update ` (e.g. `self`, `console`, `worker`). Passing an unknown target now points users at this flag instead of failing silently. Rollback is not supported; reinstall a prior version manually with `curl -fsSL https://iii.dev/install.sh | sh -s -- --version `.
+
+
+
+ ## Migrating from Motia
+
+ **Breaking.** The Motia framework is deprecated in favor of using `iii-sdk` directly. Moving to the SDK unlocks multi-worker orchestration, browser connectivity via `iii-browser-sdk` with RBAC, and a direct understanding of iii's three primitives — Workers, Functions, and Triggers. Your existing Motia project becomes one worker in a larger iii deployment instead of a standalone monolith.
+
+ [Node / TypeScript migration guide →](/changelog/0-11-0/migrating-from-motia-js) · [Python migration guide →](/changelog/0-11-0/migrating-from-motia-py)
+
+ ## SDK discovery wrappers removed
+
+ **Breaking.** The convenience discovery wrappers were removed from the Node, browser, Rust, and Python SDKs:
+
+ - `listFunctions` / `list_functions` / `list_functions_async`
+ - `listWorkers` / `list_workers` / `list_workers_async`
+ - `listTriggers` / `list_triggers` / `list_triggers_async`
+ - `listTriggerTypes` / `list_trigger_types` / `list_trigger_types_async`
+ - `onFunctionsAvailable` / `on_functions_available`
+
+ Discovery now goes through the core primitives directly: call `trigger()` against the built-in engine functions and register `engine::functions-available` like any other trigger type. This keeps the SDK surfaces aligned with the engine's "use the primitives directly" design.
+
+ ## Worker RBAC
+
+ The **iii-worker-manager** now supports role-based access control. Configure auth functions that validate WebSocket upgrade requests, attach per-session allow/deny lists for functions, control trigger registration, and auto-prefix function IDs for namespace isolation. An optional middleware function lets you intercept every invocation for audit logging, rate limiting, or payload enrichment.
+
+ [Read the Worker RBAC guide →](/0-11-0/how-to/worker-rbac)
+
+ ## Trigger format, validation, and metadata
+
+ Trigger types now accept **`trigger_request_format`** and **`call_request_format`** fields (JSON Schema) so the engine can validate trigger configs and call payloads at registration time. Triggers also support an arbitrary **`metadata`** field for tagging and filtering.
+
+ [Define request/response formats →](/0-11-0/how-to/define-request-response-formats) · [Trigger architecture →](/0-11-0/architecture/trigger-types)
+
+ ## Browser SDK
+
+ Your browser is now a first-class iii worker. The new `iii-browser-sdk` package connects to the engine over a single WebSocket and exposes the same core primitives as the Node SDK — `registerFunction`, `trigger`, `registerTrigger`, and `createChannel` all work identically. Build real-time dashboards, collaborative apps, and bi-directional frontends without REST endpoints or polling.
+
+ [Use iii in the browser →](/0-11-0/how-to/use-iii-in-the-browser)
+
+ ## Sandbox and Container Workers
+
+ Workers can now run as **container workers** or **sandbox workers**. Container workers are OCI images managed through the `iii worker` CLI — add an image, configure it in `config.yaml`, and the engine pulls, extracts, and runs it in an isolated sandbox. For local development, `iii worker add ./my-project` registers a local directory as a first-class managed worker that runs inside a lightweight microVM with auto-detected runtimes, dependency caching, and full lifecycle support (`start`, `stop`, `list`, `remove`) — no Dockerfiles needed. Requires macOS Apple Silicon or Linux with KVM.
+
+ [Managing Container Workers →](/0-11-0/how-to/managing-container-workers) · [Developing Sandbox Workers →](/0-11-0/how-to/developing-sandbox-workers)
+
+ ## `iii worker exec`
+
+ A new `iii worker exec -- ` command runs arbitrary commands inside a running worker's microVM — think `docker exec` for iii workers. stdin/stdout/stderr flow through, exit codes pass back, Ctrl-C delivers SIGINT (twice for SIGKILL). TTY mode auto-detects when both stdin and stdout are terminals, so `iii worker exec my-worker -- sh` in a terminal gives you a real interactive shell with line editing and job control. Pass `--timeout 30s` to bound runaway commands (exit 124 matches coreutils).
+
+ [Exec into a running worker →](/0-11-0/how-to/managing-container-workers#5-exec-into-a-running-worker)
+
+ ## Reproducible worker installs
+
+ Registry-managed workers can now be pinned in `iii.lock`. `iii worker add` writes the resolved worker graph when the registry provides one, binary workers can record artifacts for multiple platform targets, `iii worker verify` checks that `config.yaml` is represented in the lockfile, and `iii worker update [worker]` refreshes locked pins intentionally.
+
+ [Reproduce Worker Installs →](/0-11-0/how-to/reproduce-worker-installs)
+
+ ## Topic-based fan-out queues
+
+ **Breaking.** The topic-based queue API has been renamed. The trigger type changes from `queue` to `durable:subscriber`, and the publish function changes from `enqueue` to `iii::durable::publish`:
+
+ ```typescript
+ // Before
+ registerTrigger({ type: 'queue', function_id: 'my::handler', config: { topic: 'order.created' } })
+ trigger({ function_id: 'enqueue', payload: { topic: 'order.created', data } })
+
+ // After
+ registerTrigger({ type: 'durable:subscriber', function_id: 'my::handler', config: { topic: 'order.created' } })
+ trigger({ function_id: 'iii::durable::publish', payload: { topic: 'order.created', data } })
+ ```
+
+ Messages now fan out to every subscriber, with each function processing its copy independently and retrying on its own schedule. If a function has multiple replicas, they compete on a shared per-function queue. An optional `condition_function_id` lets you filter messages server-side before they reach the handler.
+
+ [Use topic-based queues →](/0-11-0/how-to/use-topic-queues)
+
+ ## Node SDK: `registerFunction` signature change
+
+ **Breaking.** The `registerFunction` API now takes the function ID as a plain string instead of an options object:
+
+ ```typescript
+ // Before
+ registerFunction({ id: 'function-id' }, handler)
+
+ // After
+ registerFunction('function-id', handler, {})
+ ```
+
+ The options object (metadata, request/response formats) moves to an optional third argument.
+
+ ## Everything is a worker
+
+ **Breaking.** We simplified iii down to three primitives: **Workers**, **Functions**, and **Triggers**. Modules were always workers in disguise -- they connect to the engine, register functions, and react to triggers just like SDK workers do. Now the naming reflects that.
+
+ - **Config YAML** — `modules:` top-level key renamed to `workers:`, `class:` field renamed to `name:` with short identifiers.
+ - **Rust API** — `Module` trait → `Worker`, `register_module!` → `register_worker!`, `EngineBuilder::add_module()` → `add_worker()`.
+ - **Adapter IDs** — changed from long Rust-style paths to short names: `kv`, `redis`, `builtin`, `rabbitmq`, `local`, `bridge`.
+
+ [Read the full story and migration guide →](/changelog/0-11-0/everything-is-a-worker)
+
diff --git a/docs/0-13-0/creating-workers/functions.mdx b/docs/0-13-0/creating-workers/functions.mdx
new file mode 100644
index 000000000..9605fb99c
--- /dev/null
+++ b/docs/0-13-0/creating-workers/functions.mdx
@@ -0,0 +1,204 @@
+---
+title: "Functions"
+description: "Author the functions a new worker contributes to an iii system."
+owner: "devrel"
+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.
+
+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.
+
+## 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).
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
+ return { c: payload.a + payload.b };
+ });
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ def add_handler(payload: dict) -> dict:
+ return {"c": payload["a"] + payload["b"]}
+
+ worker.register_function("math::add", add_handler)
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+
+ 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 }))
+ }))?;
+ ```
+
+
+
+## Attach request and response schemas
+
+Attach JSON Schemas to the registration so the request and response shape are documented alongside
+the function. The schemas are stored with the function and surface in the iii console and the
+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.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction(
+ "math::add",
+ async (payload) => ({ c: payload.a + payload.b }),
+ {
+ request_schema: {
+ type: "object",
+ properties: { a: { type: "number" }, b: { type: "number" } },
+ required: ["a", "b"],
+ },
+ response_schema: {
+ type: "object",
+ properties: { c: { type: "number" } },
+ required: ["c"],
+ },
+ },
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ worker.register_function(
+ "math::add",
+ add_handler,
+ request_schema={
+ "type": "object",
+ "properties": {"a": {"type": "number"}, "b": {"type": "number"}},
+ "required": ["a", "b"],
+ },
+ response_schema={
+ "type": "object",
+ "properties": {"c": {"type": "number"}},
+ "required": ["c"],
+ },
+ )
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+ use serde_json::json;
+
+ 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"],
+ })),
+ )?;
+ ```
+
+
+
+The schemas also feed the iii console and the agent-readable skills.
+
+## 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`).
+
+## 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.
+
+
+
+ ```typescript
+ const add = worker.registerFunction("math::add", async (payload) => {
+ return { c: payload.a + payload.b };
+ });
+
+ add.unregister();
+ ```
+
+
+ ```python
+ add = worker.register_function("math::add", add_handler)
+
+ add.unregister()
+ ```
+
+
+ ```rust
+ let add = worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
+ Ok(serde_json::json!({ "c": input.a + input.b }))
+ }));
+
+ 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/0-13-0/creating-workers/functions.mdx.skill.md b/docs/0-13-0/creating-workers/functions.mdx.skill.md
new file mode 100644
index 000000000..f9ff86224
--- /dev/null
+++ b/docs/0-13-0/creating-workers/functions.mdx.skill.md
@@ -0,0 +1,202 @@
+
+
+# Functions
+
+
+## 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.
+
+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.
+
+## 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).
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
+ return { c: payload.a + payload.b };
+ });
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ def add_handler(payload: dict) -> dict:
+ return {"c": payload["a"] + payload["b"]}
+
+ worker.register_function("math::add", add_handler)
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+
+ 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 }))
+ }))?;
+ ```
+
+
+
+## Attach request and response schemas
+
+Attach JSON Schemas to the registration so the request and response shape are documented alongside
+the function. The schemas are stored with the function and surface in the iii console and the
+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.
+
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction(
+ "math::add",
+ async (payload) => ({ c: payload.a + payload.b }),
+ {
+ request_schema: {
+ type: "object",
+ properties: { a: { type: "number" }, b: { type: "number" } },
+ required: ["a", "b"],
+ },
+ response_schema: {
+ type: "object",
+ properties: { c: { type: "number" } },
+ required: ["c"],
+ },
+ },
+ );
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ worker.register_function(
+ "math::add",
+ add_handler,
+ request_schema={
+ "type": "object",
+ "properties": {"a": {"type": "number"}, "b": {"type": "number"}},
+ "required": ["a", "b"],
+ },
+ response_schema={
+ "type": "object",
+ "properties": {"c": {"type": "number"}},
+ "required": ["c"],
+ },
+ )
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+ use serde_json::json;
+
+ 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"],
+ })),
+ )?;
+ ```
+
+
+
+The schemas also feed the iii console and the agent-readable skills.
+
+## 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`).
+
+## 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.
+
+
+
+ ```typescript
+ const add = worker.registerFunction("math::add", async (payload) => {
+ return { c: payload.a + payload.b };
+ });
+
+ add.unregister();
+ ```
+
+
+ ```python
+ add = worker.register_function("math::add", add_handler)
+
+ add.unregister()
+ ```
+
+
+ ```rust
+ let add = worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
+ Ok(serde_json::json!({ "c": input.a + input.b }))
+ }));
+
+ 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/0-13-0/creating-workers/triggers.mdx b/docs/0-13-0/creating-workers/triggers.mdx
new file mode 100644
index 000000000..5f35f384c
--- /dev/null
+++ b/docs/0-13-0/creating-workers/triggers.mdx
@@ -0,0 +1,201 @@
+---
+title: "Triggers"
+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.
+
+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.
+
+
+
+ ```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,
+ );
+ ```
+
+
+ ```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(),
+ )
+ ```
+
+
+ ```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(())
+ }
+ }
+
+ let webhook = worker.register_trigger_type(
+ RegisterTriggerType::new("webhook", "Incoming webhook trigger", WebhookHandler),
+ );
+ ```
+
+
+
+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.
+
+## 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 worker looks up the bindings it stashed in its
+`registerTrigger` callback and invokes each bound function via `worker.trigger(...)`.
+
+
+
+ ```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 },
+ });
+ ```
+
+
+ ```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},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ 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?;
+ ```
+
+
+
+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.
+
+
+
+ ```typescript
+ webhook.unregister();
+ ```
+
+
+ ```python
+ worker.unregister_trigger_type(
+ RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
+ )
+ ```
+
+
+ ```rust
+ worker.unregister_trigger_type("webhook");
+ ```
+
+
+
+## What goes in Worker Docs
+
+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.
diff --git a/docs/0-13-0/creating-workers/triggers.mdx.skill.md b/docs/0-13-0/creating-workers/triggers.mdx.skill.md
new file mode 100644
index 000000000..45a2c31af
--- /dev/null
+++ b/docs/0-13-0/creating-workers/triggers.mdx.skill.md
@@ -0,0 +1,197 @@
+
+
+
+## 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.
+
+
+
+ ```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,
+ );
+ ```
+
+
+ ```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(),
+ )
+ ```
+
+
+ ```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(())
+ }
+ }
+
+ let webhook = worker.register_trigger_type(
+ RegisterTriggerType::new("webhook", "Incoming webhook trigger", WebhookHandler),
+ );
+ ```
+
+
+
+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.
+
+## 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 worker looks up the bindings it stashed in its
+`registerTrigger` callback and invokes each bound function via `worker.trigger(...)`.
+
+
+
+ ```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 },
+ });
+ ```
+
+
+ ```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},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::TriggerRequest;
+ 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?;
+ ```
+
+
+
+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.
+
+
+
+ ```typescript
+ webhook.unregister();
+ ```
+
+
+ ```python
+ worker.unregister_trigger_type(
+ RegisterTriggerTypeInput(id="webhook", description="Incoming webhook trigger"),
+ )
+ ```
+
+
+ ```rust
+ worker.unregister_trigger_type("webhook");
+ ```
+
+
+
+## What goes in Worker Docs
+
+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.
diff --git a/docs/0-13-0/creating-workers/workers-registry.mdx b/docs/0-13-0/creating-workers/workers-registry.mdx
new file mode 100644
index 000000000..302cd6e71
--- /dev/null
+++ b/docs/0-13-0/creating-workers/workers-registry.mdx
@@ -0,0 +1,145 @@
+---
+title: "Registry"
+description: "Publishing your workers to the iii registry."
+owner: "devrel"
+type: "how-to"
+---
+
+{/* Note: move this up */}
+
+The iii registry at [workers.iii.dev](https://workers.iii.dev/) is where published workers live so
+other iii projects can install them with `iii worker add `.
+
+## Publish a worker
+
+Publishing a worker uploads its binary or OCI image to the registry, records its semver version, and
+makes the worker installable by name from any iii project.
+
+{/* TODO: capture the canonical publish command (likely `iii worker publish` or similar), the authentication requirements, and the metadata the registry expects (description, repo URL, supported platforms, etc.). */}
+
+## Version your worker
+
+Workers in the registry follow semver. Patch bumps for bug fixes, minor bumps for additive
+capability, major bumps for breaking changes to function or trigger signatures.
+
+{/* TODO: document how versions are tagged in the worker repo (git tag pattern), how the publish command resolves the version, and how to publish pre-releases. */}
+
+## Build binary artifacts for multiple platforms
+
+Binary workers can publish artifacts for multiple platform targets in a single registry entry (macOS
+arm64/x64, Linux arm64/x64/armv7, Windows arm64/x64/x86). One published version covers every
+supported host without separate publications per platform.
+
+{/* TODO: document the cross-build flow, the supported target triples, how the artifacts are signed/checksummed, and where they're uploaded. */}
+
+## Update or remove a published worker
+
+{/* TODO: cover how to publish a new version (semver bump + republish), how to deprecate a worker, and whether/how a published version can be retracted (yanked). */}
+
+## Bundle workers (tar.gz archives)
+
+Bundle workers are a third artifact kind alongside `binary` and `image`. The registry serves a
+single `tar.gz` archive that contains the worker's bundled source plus an `iii.worker.yaml`
+manifest at the archive root. `iii worker add ` downloads, verifies a SHA-256 checksum,
+extracts the archive into `~/.iii/workers-bundle//`, and runs it through the existing
+libkrun rails (the same sandbox path used by local-path workers, minus the host-side source
+watcher).
+
+Use a bundle when:
+
+- You ship a pre-built JavaScript bundle (`esbuild`, `tsdown`, `bun build`) or a packaged Python
+ worker and don't want to publish a Docker image.
+- You want artifacts measured in KB, not MB. Only the bundled source travels in the archive;
+ the runtime ships with the engine-allowlisted base image (`docker.io/iiidev/node:latest` or
+ `docker.io/iiidev/python:latest`).
+- You want install to look identical to other registry workers from the user's perspective
+ (`iii worker add my-worker`, same as binary and OCI).
+
+### Registry response shape
+
+```json
+{
+ "type": "bundle",
+ "name": "my-worker",
+ "version": "1.2.0",
+ "archive_url": "https://cdn.workers.iii.dev/my-worker/1.2.0/bundle.tar.gz",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+}
+```
+
+The engine GETs `archive_url`, streams the bytes through a SHA-256 hasher, and compares against
+`sha256`. Mismatches abort the install and delete the downloaded blob immediately.
+
+### Archive layout
+
+The archive root MUST contain `iii.worker.yaml`. Anything else sits at runtime-discoverable
+paths from the bundle's perspective.
+
+```text
+my-worker-1.2.0.tar.gz
+├── iii.worker.yaml
+├── bundle.js
+└── assets/
+ └── ...
+```
+
+### Manifest contract (`iii.worker.yaml`)
+
+Bundle manifests use a strict subset of the local-worker manifest. Three fields are explicitly
+**rejected**:
+
+- `scripts.setup`: would execute publisher-supplied shell during install (a supply-chain
+ smuggling vector).
+- `scripts.install`: same reason. Vendor dependencies into the bundle instead.
+- `runtime.base_image`: would let a bundle pull an arbitrary OCI image as its rootfs. Bundles
+ use the engine-allowlisted base image instead.
+
+Required fields:
+
+- `name`: must equal the install target (the value passed to `iii worker add`).
+- `scripts.start`: a non-empty shell string. The engine `exec`s this inside the sandbox VM.
+ Example: `node bundle.js`, `python -m worker`, `bun run bundle.js`.
+
+Optional fields (clamped against engine caps, with a `W182 BundleResourceClamped` warning when
+the request exceeds the cap):
+
+- `resources.cpus`: defaults to `2`, clamped to `4`.
+- `resources.memory`: defaults to `2048` MiB, clamped to `4096` MiB.
+
+```yaml
+name: my-worker
+version: 1.2.0
+scripts:
+ start: node bundle.js
+resources:
+ cpus: 2
+ memory: 2048
+```
+
+### Archive safety policy
+
+Bundle archives are extracted with tighter limits than OCI layers:
+
+| Limit | Value |
+|-----------------------------|--------------------|
+| Total uncompressed size | 64 MiB |
+| Largest single file | 32 MiB |
+| Maximum entry count | 1024 |
+| Maximum directory depth | 16 |
+| Allowed tar entry types | Regular, Directory |
+
+Archives containing symlinks, hard links, character devices, FIFOs, or paths with `..`
+components are rejected with `W181 BundleArchiveUnsafe`.
+
+### Error codes
+
+| Code | Failure |
+|--------|----------------------------------------------------------------------------------------------------|
+| `W142` | Archive download failed (HTTP error, unexpected content-type, size cap, sha256 mismatch). |
+| `W180` | Manifest rejected (forbidden field like `scripts.setup` or `runtime.base_image`). |
+| `W181` | Archive contains unsafe entries (symlink, hardlink, traversal, oversized, too many entries). |
+| `W182` | Resource request exceeded engine cap; install proceeded with clamped values (warn, not fail). |
+| `W183` | Dependency graph too wide or too deep (max depth 5, max transitive count 32). |
+
+{/* TODO: document the publish flow (`iii worker publish bundle.tar.gz`?), the registry's
+storage layout, and the recommended bundler configurations for Node/Bun/Python. */}
diff --git a/docs/0-13-0/creating-workers/workers-registry.mdx.skill.md b/docs/0-13-0/creating-workers/workers-registry.mdx.skill.md
new file mode 100644
index 000000000..35c90fd49
--- /dev/null
+++ b/docs/0-13-0/creating-workers/workers-registry.mdx.skill.md
@@ -0,0 +1,143 @@
+
+
+# Registry
+
+
+{/* Note: move this up */}
+
+The iii registry at [workers.iii.dev](https://workers.iii.dev/) is where published workers live so
+other iii projects can install them with `iii worker add `.
+
+## Publish a worker
+
+Publishing a worker uploads its binary or OCI image to the registry, records its semver version, and
+makes the worker installable by name from any iii project.
+
+{/* TODO: capture the canonical publish command (likely `iii worker publish` or similar), the authentication requirements, and the metadata the registry expects (description, repo URL, supported platforms, etc.). */}
+
+## Version your worker
+
+Workers in the registry follow semver. Patch bumps for bug fixes, minor bumps for additive
+capability, major bumps for breaking changes to function or trigger signatures.
+
+{/* TODO: document how versions are tagged in the worker repo (git tag pattern), how the publish command resolves the version, and how to publish pre-releases. */}
+
+## Build binary artifacts for multiple platforms
+
+Binary workers can publish artifacts for multiple platform targets in a single registry entry (macOS
+arm64/x64, Linux arm64/x64/armv7, Windows arm64/x64/x86). One published version covers every
+supported host without separate publications per platform.
+
+{/* TODO: document the cross-build flow, the supported target triples, how the artifacts are signed/checksummed, and where they're uploaded. */}
+
+## Update or remove a published worker
+
+{/* TODO: cover how to publish a new version (semver bump + republish), how to deprecate a worker, and whether/how a published version can be retracted (yanked). */}
+
+## Bundle workers (tar.gz archives)
+
+Bundle workers are a third artifact kind alongside `binary` and `image`. The registry serves a
+single `tar.gz` archive that contains the worker's bundled source plus an `iii.worker.yaml`
+manifest at the archive root. `iii worker add ` downloads, verifies a SHA-256 checksum,
+extracts the archive into `~/.iii/workers-bundle//`, and runs it through the existing
+libkrun rails (the same sandbox path used by local-path workers, minus the host-side source
+watcher).
+
+Use a bundle when:
+
+- You ship a pre-built JavaScript bundle (`esbuild`, `tsdown`, `bun build`) or a packaged Python
+ worker and don't want to publish a Docker image.
+- You want artifacts measured in KB, not MB. Only the bundled source travels in the archive;
+ the runtime ships with the engine-allowlisted base image (`docker.io/iiidev/node:latest` or
+ `docker.io/iiidev/python:latest`).
+- You want install to look identical to other registry workers from the user's perspective
+ (`iii worker add my-worker`, same as binary and OCI).
+
+### Registry response shape
+
+```json
+{
+ "type": "bundle",
+ "name": "my-worker",
+ "version": "1.2.0",
+ "archive_url": "https://cdn.workers.iii.dev/my-worker/1.2.0/bundle.tar.gz",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+}
+```
+
+The engine GETs `archive_url`, streams the bytes through a SHA-256 hasher, and compares against
+`sha256`. Mismatches abort the install and delete the downloaded blob immediately.
+
+### Archive layout
+
+The archive root MUST contain `iii.worker.yaml`. Anything else sits at runtime-discoverable
+paths from the bundle's perspective.
+
+```text
+my-worker-1.2.0.tar.gz
+├── iii.worker.yaml
+├── bundle.js
+└── assets/
+ └── ...
+```
+
+### Manifest contract (`iii.worker.yaml`)
+
+Bundle manifests use a strict subset of the local-worker manifest. Three fields are explicitly
+**rejected**:
+
+- `scripts.setup`: would execute publisher-supplied shell during install (a supply-chain
+ smuggling vector).
+- `scripts.install`: same reason. Vendor dependencies into the bundle instead.
+- `runtime.base_image`: would let a bundle pull an arbitrary OCI image as its rootfs. Bundles
+ use the engine-allowlisted base image instead.
+
+Required fields:
+
+- `name`: must equal the install target (the value passed to `iii worker add`).
+- `scripts.start`: a non-empty shell string. The engine `exec`s this inside the sandbox VM.
+ Example: `node bundle.js`, `python -m worker`, `bun run bundle.js`.
+
+Optional fields (clamped against engine caps, with a `W182 BundleResourceClamped` warning when
+the request exceeds the cap):
+
+- `resources.cpus`: defaults to `2`, clamped to `4`.
+- `resources.memory`: defaults to `2048` MiB, clamped to `4096` MiB.
+
+```yaml
+name: my-worker
+version: 1.2.0
+scripts:
+ start: node bundle.js
+resources:
+ cpus: 2
+ memory: 2048
+```
+
+### Archive safety policy
+
+Bundle archives are extracted with tighter limits than OCI layers:
+
+| Limit | Value |
+|-----------------------------|--------------------|
+| Total uncompressed size | 64 MiB |
+| Largest single file | 32 MiB |
+| Maximum entry count | 1024 |
+| Maximum directory depth | 16 |
+| Allowed tar entry types | Regular, Directory |
+
+Archives containing symlinks, hard links, character devices, FIFOs, or paths with `..`
+components are rejected with `W181 BundleArchiveUnsafe`.
+
+### Error codes
+
+| Code | Failure |
+|--------|----------------------------------------------------------------------------------------------------|
+| `W142` | Archive download failed (HTTP error, unexpected content-type, size cap, sha256 mismatch). |
+| `W180` | Manifest rejected (forbidden field like `scripts.setup` or `runtime.base_image`). |
+| `W181` | Archive contains unsafe entries (symlink, hardlink, traversal, oversized, too many entries). |
+| `W182` | Resource request exceeded engine cap; install proceeded with clamped values (warn, not fail). |
+| `W183` | Dependency graph too wide or too deep (max depth 5, max transitive count 32). |
+
+{/* TODO: document the publish flow (`iii worker publish bundle.tar.gz`?), the registry's
+storage layout, and the recommended bundler configurations for Node/Bun/Python. */}
diff --git a/docs/0-13-0/creating-workers/workers.mdx b/docs/0-13-0/creating-workers/workers.mdx
new file mode 100644
index 000000000..72305cb34
--- /dev/null
+++ b/docs/0-13-0/creating-workers/workers.mdx
@@ -0,0 +1,139 @@
+---
+title: "Workers"
+description: "Deploying and integrating workers into an iii project."
+owner: "devrel"
+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.
+
+## 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.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="my-worker"),
+ )
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, register_worker};
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+ ```
+
+
+
+
+## Worker lifecycle states
+
+Workers transition through a small set of states after connecting:
+`connecting → connected → available / busy → disconnected`. `connecting` is the WebSocket handshake.
+`connected` means the Worker has joined the Engine's registry. `available` and `busy` describe
+whether the Worker is currently handling invocations. `disconnected` is the terminal state when the
+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
+
+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).
+
+
+## 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.
+
+```yaml
+name: math-worker
+runtime:
+ kind: python
+ package_manager: pip
+ entry: math_worker.py
+scripts:
+ install: "pip install -r requirements.txt"
+ 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.
+
+For the full manifest field schema, see [Using iii / Workers](/using-iii/workers).
+
+## What a worker contributes
+
+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)).
+
+The SDK calls a worker uses to register these are language-specific and documented in each
+language's Worker Docs authoring guide.
+
+## Run an ephemeral worker
+
+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.
+
+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.
diff --git a/docs/0-13-0/creating-workers/workers.mdx.skill.md b/docs/0-13-0/creating-workers/workers.mdx.skill.md
new file mode 100644
index 000000000..33cc03769
--- /dev/null
+++ b/docs/0-13-0/creating-workers/workers.mdx.skill.md
@@ -0,0 +1,137 @@
+
+
+# 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.
+
+## 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.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="my-worker"),
+ )
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, register_worker};
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+ ```
+
+
+
+
+## Worker lifecycle states
+
+Workers transition through a small set of states after connecting:
+`connecting → connected → available / busy → disconnected`. `connecting` is the WebSocket handshake.
+`connected` means the Worker has joined the Engine's registry. `available` and `busy` describe
+whether the Worker is currently handling invocations. `disconnected` is the terminal state when the
+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
+
+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).
+
+
+## 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.
+
+```yaml
+name: math-worker
+runtime:
+ kind: python
+ package_manager: pip
+ entry: math_worker.py
+scripts:
+ install: "pip install -r requirements.txt"
+ 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.
+
+For the full manifest field schema, see [Using iii / Workers](/using-iii/workers).
+
+## What a worker contributes
+
+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)).
+
+The SDK calls a worker uses to register these are language-specific and documented in each
+language's Worker Docs authoring guide.
+
+## Run an ephemeral worker
+
+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.
+
+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.
diff --git a/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx b/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx
new file mode 100644
index 000000000..a28fb1b40
--- /dev/null
+++ b/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx
@@ -0,0 +1,53 @@
+---
+title: "Build a real-time todo app"
+description: "End-to-end reactive todo app over a single browser-to-engine WebSocket."
+owner: "devrel"
+type: "how-to"
+---
+
+A full-stack reactive todo app over a single browser-to-engine WebSocket. Function calls and live
+stream change events flow over the same connection.
+
+## Engine configuration
+
+The `config.yaml` workers needed to support a browser-driven reactive app.
+
+## Backend
+
+The worker, auth, stream wrapper, and CRUD functions that power the app server-side.
+
+### Worker setup
+
+Connect a worker, register the functions the app needs, and bind triggers.
+
+### Auth function (RBAC)
+
+An auth function runs on each new browser WebSocket connection, issues a session ID, and namespaces
+the session's registered functions to prevent collisions.
+
+### Stream wrapper
+
+A typed wrapper class over `stream::get` / `stream::set` / `stream::list` / `stream::delete` for one
+named stream.
+
+### Functions
+
+The CRUD functions the browser calls (create, toggle, delete, list) wired to write through the
+stream wrapper.
+
+## Frontend
+
+The browser-side WebSocket connection and reactive hook that render live updates.
+
+### Connection
+
+Connect the browser to the engine over a single WebSocket using `iii-browser-sdk`.
+
+### Real-time hook
+
+A hook that subscribes to the stream and re-renders the UI on each change event the engine pushes.
+
+## Key concepts
+
+The reactive primitives this app relies on: single-WebSocket transport, per-session RBAC
+namespacing, and stream-driven UI updates.
diff --git a/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx.skill.md b/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx.skill.md
new file mode 100644
index 000000000..331a1f962
--- /dev/null
+++ b/docs/0-13-0/how-to/build-a-realtime-todo-app.mdx.skill.md
@@ -0,0 +1,51 @@
+
+
+# Build a real-time todo app
+
+
+A full-stack reactive todo app over a single browser-to-engine WebSocket. Function calls and live
+stream change events flow over the same connection.
+
+## Engine configuration
+
+The `config.yaml` workers needed to support a browser-driven reactive app.
+
+## Backend
+
+The worker, auth, stream wrapper, and CRUD functions that power the app server-side.
+
+### Worker setup
+
+Connect a worker, register the functions the app needs, and bind triggers.
+
+### Auth function (RBAC)
+
+An auth function runs on each new browser WebSocket connection, issues a session ID, and namespaces
+the session's registered functions to prevent collisions.
+
+### Stream wrapper
+
+A typed wrapper class over `stream::get` / `stream::set` / `stream::list` / `stream::delete` for one
+named stream.
+
+### Functions
+
+The CRUD functions the browser calls (create, toggle, delete, list) wired to write through the
+stream wrapper.
+
+## Frontend
+
+The browser-side WebSocket connection and reactive hook that render live updates.
+
+### Connection
+
+Connect the browser to the engine over a single WebSocket using `iii-browser-sdk`.
+
+### Real-time hook
+
+A hook that subscribes to the stream and re-renders the UI on each change event the engine pushes.
+
+## Key concepts
+
+The reactive primitives this app relies on: single-WebSocket transport, per-session RBAC
+namespacing, and stream-driven UI updates.
diff --git a/docs/0-13-0/index.mdx b/docs/0-13-0/index.mdx
new file mode 100644
index 000000000..848be1f0d
--- /dev/null
+++ b/docs/0-13-0/index.mdx
@@ -0,0 +1,99 @@
+---
+title: "Welcome to iii"
+description:
+ "A next-generation software system that makes it possible to effortlessly compose, extend, and
+ observe every service in real-time for the first time ever."
+owner: "devrel"
+type: "explanation"
+---
+
+{/* The tabs above split the site into Docs (start here), Tutorials, How-tos, Patterns, SDK & Engine Reference, and Changelog. Read this page and the Docs tab in order first. The other tabs make more sense once you have the model. */}
+
+## The Problem
+
+Each service in a modern system arrives with its own internals, its own lifecycle, its own
+integration story, and its own failure modes. Four services means six possible integration edges.
+Twenty means 190.
+
+What we mean by edges is that every new integration brings with it cross-system interactions. For
+example the addition of an agentic harness includes not just a single integration point with your
+current system but the numerous downstream consumers of it.
+
+Every new capability quadratically compounds the coordination cost of everything already in your
+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.
+
+## 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.
+
+## Have a Need? Add a Worker
+
+Need a queue? Add a worker. Need real-time streaming, scheduling, sandboxing, observability, an
+agent, a CRM integration, a browser tab participating as a worker? Add a worker. Some workers are
+deterministic code. Some are stochastic agents.
+
+`iii worker add` is the npm moment for systems. What installs is not a library. It is a complete
+running service. A queue worker. A sandbox worker. A classifier. One command, complete capability,
+immediately available to every other worker in the system.
+
+## Same Contract, Both Sides
+
+Application teams register functions and declare triggers, focused entirely on business logic.
+Platform teams publish workers, focused entirely on the capabilities they provide. Both sides
+fulfill the same contract. No bespoke SDKs, no internal client libraries, no per-service API
+contracts. The work that used to live between teams disappears.
+
+## Any Language, Any Runtime
+
+A worker in Docker, on Kubernetes, on the edge, in a browser tab, on a Raspberry Pi, or inside a
+hardware-isolated microVM is the same kind of worker. Moving a workload is a redeploy, not a
+rewrite. The engine handles serialization and routing.
+
+## Built for Agents
+
+Most agent harnesses solve one slice of the problem: a chat loop, a tool-calling sandbox, a chained
+workflow. iii is not a harness for agents. Instead it functions better than a harness because it is
+the same runtime the whole system already runs on. This makes iii inherently agentic. In iii an
+agent is a worker and can (with permission) act on every part of the system, not inside a runtime
+created separately for agents.
+
+Agents are workers. An agent's tools are functions. Its memory is state. Its orchestration is
+triggers. The agent does not call out to a separate "agent runtime" to do work. The runtime is the
+rest of the system. An agent that hits a task outside its current capabilities can register a worker
+at runtime, expose new functions, and extend the system it operates inside. That is what makes iii
+different.
+
+Humans and agents share one mental model. A new engineer is productive on day one because the mental
+model never changes from one capability to the next. AI agents can reliably reason about an entire
+system in a single context window because there is one set of primitives to learn and one
+always-accurate source of truth for what exists.
+
+As agents do more of the work of building and operating software, the size of that primitive set is
+what compounds: easier to onboard, cheaper to prompt, faster to extend, simpler to maintain.
+
+## Getting Started
+
+The best way to understand iii is to try it. Install the engine and follow the Quickstart to create
+your first iii-powered project.
+
+
+
+ Install the iii engine.
+
+
+ Follow the Quickstart and explore a live iii application.
+
+
diff --git a/docs/0-13-0/index.mdx.skill.md b/docs/0-13-0/index.mdx.skill.md
new file mode 100644
index 000000000..cc83bafca
--- /dev/null
+++ b/docs/0-13-0/index.mdx.skill.md
@@ -0,0 +1,95 @@
+
+
+# Welcome to iii
+
+
+{/* The tabs above split the site into Docs (start here), Tutorials, How-tos, Patterns, SDK & Engine Reference, and Changelog. Read this page and the Docs tab in order first. The other tabs make more sense once you have the model. */}
+
+## The Problem
+
+Each service in a modern system arrives with its own internals, its own lifecycle, its own
+integration story, and its own failure modes. Four services means six possible integration edges.
+Twenty means 190.
+
+What we mean by edges is that every new integration brings with it cross-system interactions. For
+example the addition of an agentic harness includes not just a single integration point with your
+current system but the numerous downstream consumers of it.
+
+Every new capability quadratically compounds the coordination cost of everything already in your
+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.
+
+## 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.
+
+## Have a Need? Add a Worker
+
+Need a queue? Add a worker. Need real-time streaming, scheduling, sandboxing, observability, an
+agent, a CRM integration, a browser tab participating as a worker? Add a worker. Some workers are
+deterministic code. Some are stochastic agents.
+
+`iii worker add` is the npm moment for systems. What installs is not a library. It is a complete
+running service. A queue worker. A sandbox worker. A classifier. One command, complete capability,
+immediately available to every other worker in the system.
+
+## Same Contract, Both Sides
+
+Application teams register functions and declare triggers, focused entirely on business logic.
+Platform teams publish workers, focused entirely on the capabilities they provide. Both sides
+fulfill the same contract. No bespoke SDKs, no internal client libraries, no per-service API
+contracts. The work that used to live between teams disappears.
+
+## Any Language, Any Runtime
+
+A worker in Docker, on Kubernetes, on the edge, in a browser tab, on a Raspberry Pi, or inside a
+hardware-isolated microVM is the same kind of worker. Moving a workload is a redeploy, not a
+rewrite. The engine handles serialization and routing.
+
+## Built for Agents
+
+Most agent harnesses solve one slice of the problem: a chat loop, a tool-calling sandbox, a chained
+workflow. iii is not a harness for agents. Instead it functions better than a harness because it is
+the same runtime the whole system already runs on. This makes iii inherently agentic. In iii an
+agent is a worker and can (with permission) act on every part of the system, not inside a runtime
+created separately for agents.
+
+Agents are workers. An agent's tools are functions. Its memory is state. Its orchestration is
+triggers. The agent does not call out to a separate "agent runtime" to do work. The runtime is the
+rest of the system. An agent that hits a task outside its current capabilities can register a worker
+at runtime, expose new functions, and extend the system it operates inside. That is what makes iii
+different.
+
+Humans and agents share one mental model. A new engineer is productive on day one because the mental
+model never changes from one capability to the next. AI agents can reliably reason about an entire
+system in a single context window because there is one set of primitives to learn and one
+always-accurate source of truth for what exists.
+
+As agents do more of the work of building and operating software, the size of that primitive set is
+what compounds: easier to onboard, cheaper to prompt, faster to extend, simpler to maintain.
+
+## Getting Started
+
+The best way to understand iii is to try it. Install the engine and follow the Quickstart to create
+your first iii-powered project.
+
+
+
+ Install the iii engine.
+
+
+ Follow the Quickstart and explore a live iii application.
+
+
diff --git a/docs/0-13-0/install.mdx b/docs/0-13-0/install.mdx
new file mode 100644
index 000000000..9b040dc6a
--- /dev/null
+++ b/docs/0-13-0/install.mdx
@@ -0,0 +1,46 @@
+---
+title: "Install"
+description: "Install the iii engine and set up your development environment."
+owner: "devrel"
+type: "how-to"
+---
+
+{/* TODO (community feedback): Add a direct binary-download path alongside the curl|sh installer so users who do not want to verify a remote script with a bot, or are on Windows, have an alternative. Link to the GitHub Releases page for the engine and include PATH setup instructions per OS (especially Windows). */}
+
+## 1. Install iii
+
+Install the iii engine:
+
+```bash
+curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
+```
+
+## 2. Verify installation
+
+Check that iii has installed correctly with the following command. It should return a version
+number.
+
+```bash
+iii --version
+```
+
+
+ The engine and SDK packages can have different patch versions within the same minor line. Keep the
+ engine and SDKs on the same minor version, for example `0.11.x`, unless a release note says
+ 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-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). */}
+
+## Next Steps
+
+
+
+ Follow the Quickstart and explore a live iii application.
+
+
+ Learn how to use iii in production.
+
+
diff --git a/docs/0-13-0/install.mdx.skill.md b/docs/0-13-0/install.mdx.skill.md
new file mode 100644
index 000000000..b835b8c90
--- /dev/null
+++ b/docs/0-13-0/install.mdx.skill.md
@@ -0,0 +1,44 @@
+
+
+# Install
+
+
+{/* TODO (community feedback): Add a direct binary-download path alongside the curl|sh installer so users who do not want to verify a remote script with a bot, or are on Windows, have an alternative. Link to the GitHub Releases page for the engine and include PATH setup instructions per OS (especially Windows). */}
+
+## 1. Install iii
+
+Install the iii engine:
+
+```bash
+curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
+```
+
+## 2. Verify installation
+
+Check that iii has installed correctly with the following command. It should return a version
+number.
+
+```bash
+iii --version
+```
+
+
+ The engine and SDK packages can have different patch versions within the same minor line. Keep the
+ engine and SDKs on the same minor version, for example `0.11.x`, unless a release note says
+ 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-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). */}
+
+## Next Steps
+
+
+
+ Follow the Quickstart and explore a live iii application.
+
+
+ Learn how to use iii in production.
+
+
diff --git a/docs/0-13-0/patterns/adapter-pattern.mdx b/docs/0-13-0/patterns/adapter-pattern.mdx
new file mode 100644
index 000000000..574475be6
--- /dev/null
+++ b/docs/0-13-0/patterns/adapter-pattern.mdx
@@ -0,0 +1,28 @@
+---
+title: "Adapter Pattern"
+description: "Wrap an existing service as an iii worker so it joins the system like any other."
+owner: "devrel"
+type: "how-to"
+---
+
+When you want to bring an existing service (a third-party API, a library, an in-house microservice)
+into an iii system without rewriting it, wrap it in a thin worker that exposes the service's
+capabilities as iii functions. The worker translates between the existing service's interface and
+the iii function-call shape.
+
+## When to use this pattern
+
+When you have an existing service you want to use from iii without rewriting it, and you want
+callers to address it the way they address every other worker (by function ID, not by HTTP or
+library-specific call shapes).
+
+## Structure
+
+A thin iii worker that:
+
+1. Connects to the engine.
+2. Registers one function per operation you want to expose.
+3. Inside each handler, calls the existing service and returns the result as the function's
+ response.
+
+{/* TODO: full worked example wrapping a real third-party API (e.g. Stripe, OpenAI, internal REST service) as an iii worker. Include the function ID conventions, error mapping from the wrapped service to iii function errors, and how to handle authentication / secrets. */}
diff --git a/docs/0-13-0/patterns/adapter-pattern.mdx.skill.md b/docs/0-13-0/patterns/adapter-pattern.mdx.skill.md
new file mode 100644
index 000000000..8841ea776
--- /dev/null
+++ b/docs/0-13-0/patterns/adapter-pattern.mdx.skill.md
@@ -0,0 +1,26 @@
+
+
+# Adapter Pattern
+
+
+When you want to bring an existing service (a third-party API, a library, an in-house microservice)
+into an iii system without rewriting it, wrap it in a thin worker that exposes the service's
+capabilities as iii functions. The worker translates between the existing service's interface and
+the iii function-call shape.
+
+## When to use this pattern
+
+When you have an existing service you want to use from iii without rewriting it, and you want
+callers to address it the way they address every other worker (by function ID, not by HTTP or
+library-specific call shapes).
+
+## Structure
+
+A thin iii worker that:
+
+1. Connects to the engine.
+2. Registers one function per operation you want to expose.
+3. Inside each handler, calls the existing service and returns the result as the function's
+ response.
+
+{/* TODO: full worked example wrapping a real third-party API (e.g. Stripe, OpenAI, internal REST service) as an iii worker. Include the function ID conventions, error mapping from the wrapped service to iii function errors, and how to handle authentication / secrets. */}
diff --git a/docs/0-13-0/patterns/reactive-state-pattern.mdx b/docs/0-13-0/patterns/reactive-state-pattern.mdx
new file mode 100644
index 000000000..7a6f56427
--- /dev/null
+++ b/docs/0-13-0/patterns/reactive-state-pattern.mdx
@@ -0,0 +1,30 @@
+---
+title: "Reactive State Pattern"
+description: "Drive function execution from state changes instead of explicit invocation."
+owner: "devrel"
+type: "how-to"
+---
+
+When the trigger for a function is a change in some piece of state (not a request or a schedule),
+bind the function to a state-change trigger advertised by a state worker. The engine fires the
+function whenever the watched key or scope changes. The function code is the same as any other;
+only the trigger registration differs.
+
+## When to use this pattern
+
+When the action is conceptually "whenever this data changes, do this", instead of "on schedule" or
+"on demand". Examples: rebuild a derived index when its source state changes, fan out a notification
+when a user record updates, propagate a calculated value into another scope when its inputs move.
+
+## Structure
+
+1. A state worker holds the source-of-truth state.
+2. A function in some worker handles the change (compute a derived value, send a notification,
+ etc.).
+3. A trigger advertised by the state worker binds that function to the relevant key or scope so the
+ engine fires it on every change.
+
+The trigger type, its config shape, and the state worker's read/write surface are documented in
+that worker's Worker Docs.
+
+{/* TODO: full worked example with a state set/write and a derived function. Cover the trigger config shape, scope vs key matching, ordering guarantees, and how to handle bursts of changes. */}
diff --git a/docs/0-13-0/patterns/reactive-state-pattern.mdx.skill.md b/docs/0-13-0/patterns/reactive-state-pattern.mdx.skill.md
new file mode 100644
index 000000000..727eb6331
--- /dev/null
+++ b/docs/0-13-0/patterns/reactive-state-pattern.mdx.skill.md
@@ -0,0 +1,28 @@
+
+
+# Reactive State Pattern
+
+
+When the trigger for a function is a change in some piece of state (not a request or a schedule),
+bind the function to a state-change trigger advertised by a state worker. The engine fires the
+function whenever the watched key or scope changes. The function code is the same as any other;
+only the trigger registration differs.
+
+## When to use this pattern
+
+When the action is conceptually "whenever this data changes, do this", instead of "on schedule" or
+"on demand". Examples: rebuild a derived index when its source state changes, fan out a notification
+when a user record updates, propagate a calculated value into another scope when its inputs move.
+
+## Structure
+
+1. A state worker holds the source-of-truth state.
+2. A function in some worker handles the change (compute a derived value, send a notification,
+ etc.).
+3. A trigger advertised by the state worker binds that function to the relevant key or scope so the
+ engine fires it on every change.
+
+The trigger type, its config shape, and the state worker's read/write surface are documented in
+that worker's Worker Docs.
+
+{/* TODO: full worked example with a state set/write and a derived function. Cover the trigger config shape, scope vs key matching, ordering guarantees, and how to handle bursts of changes. */}
diff --git a/docs/0-13-0/quickstart.mdx b/docs/0-13-0/quickstart.mdx
new file mode 100644
index 000000000..c7d4389a4
--- /dev/null
+++ b/docs/0-13-0/quickstart.mdx
@@ -0,0 +1,249 @@
+---
+title: "Quickstart"
+description:
+ "Scaffold a cross-language project, compose Python and TypeScript workers, and incrementally add
+ functionality to a live system with zero downtime."
+owner: "devrel"
+type: "tutorial"
+---
+
+In this tutorial you will learn how iii makes it unreasonably simple to build and extend systems.
+
+
+ Make sure you have installed iii before proceeding. If you haven't then visit the
+ [Install](/install) guide first.
+
+
+## 1. Create the project
+
+```bash
+iii project init quickstart --template quickstart
+cd quickstart
+```
+
+This creates the two workers that you'll run: a Python worker that adds two numbers and stores the
+sum in state, and a TypeScript worker that exposes an http endpoint and calls the Python worker
+through the iii engine.
+
+```
+workers/
+ math-worker/
+ math_worker.py # Python worker
+ caller-worker/
+ src/worker.ts # TypeScript worker
+```
+
+## 2. Start the engine
+
+```bash
+iii --config config.yaml
+```
+
+The engine is now listening on `ws://localhost:49134`. Keep this terminal open and open a second
+terminal in the `quickstart` directory for the remaining commands.
+
+## 3. Start the Python worker
+
+
+ Workers only need a WebSocket connection to the iii engine. They can run locally, in the cloud,
+ replicated in kubernetes, or anywhere else.
+
+
+```bash
+iii worker add ./workers/math-worker
+```
+
+You should see:
+
+```
+✓ Worker math-worker added to config.yaml
+Path /Users/tony/iii/projects/testing/quickstart/workers/math-worker
+✓ Using cached deps (use --force to reinstall)
+✓ math-worker started (pid: 12345)
+✓ Worker auto-started
+```
+
+This worker registered the function `math::add` with the engine. You could call this function right
+now using the command below.
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+However this is not much different than running an equivalent script on its own. The utility of iii
+comes from being able to place any functionality into a worker and then compose that worker with
+other workers through the engine, regardless of where each one runs or what language it's written
+in.
+
+
+ Workers need a moment to install their runtime dependencies after being added. If you see
+ `"message": "Function math::add not found"`, wait a few seconds and try again.
+
+
+## 4. Start the TypeScript worker
+
+```bash
+iii worker add ./workers/caller-worker
+```
+
+You should see:
+
+```
+✓ Worker caller-worker added to config.yaml
+Path /Users/tony/iii/projects/testing/quickstart/workers/caller-worker
+✓ Using cached deps (use --force to reinstall)
+✓ caller-worker started (pid: 23456)
+✓ Worker auto-started
+```
+
+This worker registered the function `math::add_two_numbers` with the engine.
+
+## 5. Call across languages
+
+Call the TypeScript worker. It will call the Python worker through the engine and return the result:
+
+```bash
+iii trigger math::add_two_numbers a=10 b=20
+```
+
+```json
+{ "c": 30 }
+```
+
+## 6. Add state
+
+The `iii worker add` command incrementally adds workers from the registry to your running system.
+Start by adding the state worker, which gives every function access to a persistent key-value store.
+
+From the folder containing iii's `config.yaml` run:
+
+```bash
+iii worker add iii-state
+```
+
+Now open `workers/math-worker/math_worker.py` in your code editor and uncomment the state block so
+the handler looks like this:
+
+```python
+def add_handler(payload: dict) -> dict:
+ a = payload.get("a", 0)
+ b = payload.get("b", 0)
+ logger.info(f"math::add called in Python with a={a}, b={b}")
+ result = {"c": a + b}
+
+ running_total = worker.trigger(
+ {
+ "function_id": "state::get",
+ "payload": {"scope": "math", "key": "running_total"},
+ }
+ )
+ new_total = (running_total or 0) + result["c"]
+ worker.trigger(
+ {
+ "function_id": "state::set",
+ "payload": {"scope": "math", "key": "running_total", "value": new_total},
+ }
+ )
+ result["running_total"] = new_total
+
+ return result
+```
+
+Save the file and call the function a few times:
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+```json
+{ "c": 5, "running_total": 5 }
+```
+
+```bash
+iii trigger math::add a=10 b=20
+```
+
+```json
+{ "c": 30, "running_total": 35 }
+```
+
+The running total persists across every call, including calls that arrive through
+`math::add_two_numbers`.
+
+## 7. Add HTTP endpoints
+
+Now let's add an HTTP worker to expose your functions as REST endpoints.
+
+From the folder containing iii's `config.yaml` run:
+
+```bash
+iii worker add iii-http
+```
+
+Open `workers/caller-worker/src/worker.ts` and uncomment the HTTP block at the bottom of the file:
+
+```typescript
+worker.registerFunction(
+ "http::add_two_numbers",
+ async (payload: { body: { a: number; b: number } }) => {
+ const result = await worker.trigger({
+ function_id: "math::add_two_numbers",
+ payload: payload.body,
+ });
+ return {
+ status_code: 200,
+ body: { c: result.c, running_total: result.running_total },
+ headers: { "Content-Type": "application/json" },
+ };
+ },
+);
+
+worker.registerTrigger({
+ type: "http",
+ function_id: "http::add_two_numbers",
+ config: { api_path: "/math/add-two-numbers", http_method: "POST" },
+});
+```
+
+Save the file, then call the new endpoint with curl:
+
+```bash
+curl -X POST http://localhost:3111/math/add-two-numbers \
+ -H 'Content-Type: application/json' \
+ -d '{"a": 100, "b": 200}'
+```
+
+```json
+{ "c": 300, "running_total": 335 }
+```
+
+The same functions that respond to `iii trigger` now also respond to HTTP requests with no code
+changes to the handlers themselves.
+
+## How it works
+
+For a walkthrough of how the engine, workers, functions, and triggers in this scaffold fit together,
+see [Understanding iii](/understanding-iii). It uses this project as the worked example.
+
+
+ Open the iii Console with `iii console` in a new terminal: an interactive UI for workers,
+ functions, triggers, logs, traces, and state. See the full [Console
+ documentation](/using-iii/console) for details.
+
+
+{/* TODO: re-add the "Give your coding agent context" Tip with `npx skills add iii-hq/iii/skills` once the iii skills worker (owned by Sergio) ships. */}
+
+## Next Steps
+
+You scaffolded a project, started two workers in different languages, called functions across them,
+added persistent state, and exposed everything over HTTP, all by incrementally adding workers to a
+running system.
+
+
+
+ Learn how to use iii in production.
+
+
+ Understand functions, triggers, and workers from a conceptual point of view.
+
+
diff --git a/docs/0-13-0/quickstart.mdx.skill.md b/docs/0-13-0/quickstart.mdx.skill.md
new file mode 100644
index 000000000..e2143b871
--- /dev/null
+++ b/docs/0-13-0/quickstart.mdx.skill.md
@@ -0,0 +1,245 @@
+
+
+# Quickstart
+
+
+In this tutorial you will learn how iii makes it unreasonably simple to build and extend systems.
+
+
+ Make sure you have installed iii before proceeding. If you haven't then visit the
+ [Install](/install) guide first.
+
+
+## 1. Create the project
+
+```bash
+iii project init quickstart --template quickstart
+cd quickstart
+```
+
+This creates the two workers that you'll run: a Python worker that adds two numbers and stores the
+sum in state, and a TypeScript worker that exposes an http endpoint and calls the Python worker
+through the iii engine.
+
+```
+workers/
+ math-worker/
+ math_worker.py # Python worker
+ caller-worker/
+ src/worker.ts # TypeScript worker
+```
+
+## 2. Start the engine
+
+```bash
+iii --config config.yaml
+```
+
+The engine is now listening on `ws://localhost:49134`. Keep this terminal open and open a second
+terminal in the `quickstart` directory for the remaining commands.
+
+## 3. Start the Python worker
+
+
+ Workers only need a WebSocket connection to the iii engine. They can run locally, in the cloud,
+ replicated in kubernetes, or anywhere else.
+
+
+```bash
+iii worker add ./workers/math-worker
+```
+
+You should see:
+
+```
+✓ Worker math-worker added to config.yaml
+Path /Users/tony/iii/projects/testing/quickstart/workers/math-worker
+✓ Using cached deps (use --force to reinstall)
+✓ math-worker started (pid: 12345)
+✓ Worker auto-started
+```
+
+This worker registered the function `math::add` with the engine. You could call this function right
+now using the command below.
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+However this is not much different than running an equivalent script on its own. The utility of iii
+comes from being able to place any functionality into a worker and then compose that worker with
+other workers through the engine, regardless of where each one runs or what language it's written
+in.
+
+
+ Workers need a moment to install their runtime dependencies after being added. If you see
+ `"message": "Function math::add not found"`, wait a few seconds and try again.
+
+
+## 4. Start the TypeScript worker
+
+```bash
+iii worker add ./workers/caller-worker
+```
+
+You should see:
+
+```
+✓ Worker caller-worker added to config.yaml
+Path /Users/tony/iii/projects/testing/quickstart/workers/caller-worker
+✓ Using cached deps (use --force to reinstall)
+✓ caller-worker started (pid: 23456)
+✓ Worker auto-started
+```
+
+This worker registered the function `math::add_two_numbers` with the engine.
+
+## 5. Call across languages
+
+Call the TypeScript worker. It will call the Python worker through the engine and return the result:
+
+```bash
+iii trigger math::add_two_numbers a=10 b=20
+```
+
+```json
+{ "c": 30 }
+```
+
+## 6. Add state
+
+The `iii worker add` command incrementally adds workers from the registry to your running system.
+Start by adding the state worker, which gives every function access to a persistent key-value store.
+
+From the folder containing iii's `config.yaml` run:
+
+```bash
+iii worker add iii-state
+```
+
+Now open `workers/math-worker/math_worker.py` in your code editor and uncomment the state block so
+the handler looks like this:
+
+```python
+def add_handler(payload: dict) -> dict:
+ a = payload.get("a", 0)
+ b = payload.get("b", 0)
+ logger.info(f"math::add called in Python with a={a}, b={b}")
+ result = {"c": a + b}
+
+ running_total = worker.trigger(
+ {
+ "function_id": "state::get",
+ "payload": {"scope": "math", "key": "running_total"},
+ }
+ )
+ new_total = (running_total or 0) + result["c"]
+ worker.trigger(
+ {
+ "function_id": "state::set",
+ "payload": {"scope": "math", "key": "running_total", "value": new_total},
+ }
+ )
+ result["running_total"] = new_total
+
+ return result
+```
+
+Save the file and call the function a few times:
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+```json
+{ "c": 5, "running_total": 5 }
+```
+
+```bash
+iii trigger math::add a=10 b=20
+```
+
+```json
+{ "c": 30, "running_total": 35 }
+```
+
+The running total persists across every call, including calls that arrive through
+`math::add_two_numbers`.
+
+## 7. Add HTTP endpoints
+
+Now let's add an HTTP worker to expose your functions as REST endpoints.
+
+From the folder containing iii's `config.yaml` run:
+
+```bash
+iii worker add iii-http
+```
+
+Open `workers/caller-worker/src/worker.ts` and uncomment the HTTP block at the bottom of the file:
+
+```typescript
+worker.registerFunction(
+ "http::add_two_numbers",
+ async (payload: { body: { a: number; b: number } }) => {
+ const result = await worker.trigger({
+ function_id: "math::add_two_numbers",
+ payload: payload.body,
+ });
+ return {
+ status_code: 200,
+ body: { c: result.c, running_total: result.running_total },
+ headers: { "Content-Type": "application/json" },
+ };
+ },
+);
+
+worker.registerTrigger({
+ type: "http",
+ function_id: "http::add_two_numbers",
+ config: { api_path: "/math/add-two-numbers", http_method: "POST" },
+});
+```
+
+Save the file, then call the new endpoint with curl:
+
+```bash
+curl -X POST http://localhost:3111/math/add-two-numbers \
+ -H 'Content-Type: application/json' \
+ -d '{"a": 100, "b": 200}'
+```
+
+```json
+{ "c": 300, "running_total": 335 }
+```
+
+The same functions that respond to `iii trigger` now also respond to HTTP requests with no code
+changes to the handlers themselves.
+
+## How it works
+
+For a walkthrough of how the engine, workers, functions, and triggers in this scaffold fit together,
+see [Understanding iii](/understanding-iii). It uses this project as the worked example.
+
+
+ Open the iii Console with `iii console` in a new terminal: an interactive UI for workers,
+ functions, triggers, logs, traces, and state. See the full [Console
+ documentation](/using-iii/console) for details.
+
+
+{/* TODO: re-add the "Give your coding agent context" Tip with `npx skills add iii-hq/iii/skills` once the iii skills worker (owned by Sergio) ships. */}
+
+## Next Steps
+
+You scaffolded a project, started two workers in different languages, called functions across them,
+added persistent state, and exposed everything over HTTP, all by incrementally adding workers to a
+running system.
+
+
+
+ Learn how to use iii in production.
+
+
+ Understand functions, triggers, and workers from a conceptual point of view.
+
+
diff --git a/docs/0-13-0/sdk-reference/browser-sdk.mdx b/docs/0-13-0/sdk-reference/browser-sdk.mdx
new file mode 100644
index 000000000..b9fd14b90
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/browser-sdk.mdx
@@ -0,0 +1,150 @@
+---
+title: "Browser SDK"
+description: "Public surface of the iii Browser SDK (`iii-browser-sdk`)."
+owner: "engineering"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source. The Browser SDK is a standalone implementation alongside the
+ Node SDK, not a build of it.
+
+
+## Installation
+
+```bash
+npm install iii-browser-sdk
+```
+
+## Common methods
+
+### `registerWorker`
+
+Connect a browser tab to a running iii engine and return its handle.
+
+```typescript
+function registerWorker(address: string, options?: InitOptions): ISdk;
+```
+
+`address` is the engine's SDK WebSocket URL. The returned `ISdk` carries every method below.
+
+### `registerFunction`
+
+Register a callable function in this tab.
+
+```typescript
+worker.registerFunction(
+ functionId: string,
+ handler: RemoteFunctionHandler,
+ options?: RegisterFunctionOptions,
+): FunctionRef;
+```
+
+### `registerTrigger`
+
+Bind a registered function to a configured trigger instance.
+
+```typescript
+worker.registerTrigger(trigger: RegisterTriggerInput): Trigger;
+```
+
+Drop the trigger with `Trigger.unregister()` on the returned handle. There is no top-level
+`unregisterTrigger`.
+
+### `registerTriggerType`
+
+Declare a new trigger type that this tab advertises.
+
+```typescript
+worker.registerTriggerType(
+ triggerType: RegisterTriggerTypeInput,
+ handler: TriggerHandler,
+): TriggerTypeRef;
+```
+
+### `unregisterTriggerType`
+
+Remove a previously registered trigger type.
+
+```typescript
+worker.unregisterTriggerType(triggerType: RegisterTriggerTypeInput): void;
+```
+
+### `trigger`
+
+Invoke a registered function.
+
+```typescript
+worker.trigger(request: TriggerRequest): Promise;
+```
+
+Resolves with the function's return value for synchronous calls, with an `EnqueueResult` for
+`TriggerAction.Enqueue` actions, and with `undefined` for `TriggerAction.Void`.
+
+### `shutdown`
+
+Disconnect from the engine and release resources.
+
+```typescript
+worker.shutdown(): Promise;
+```
+
+## Trigger actions
+
+`TriggerAction` is a runtime const that produces the value passed to `trigger`'s `action` field.
+
+```typescript
+TriggerAction.Void(); // fire-and-forget
+TriggerAction.Enqueue({ queue: "math" }); // route through iii-queue
+```
+
+The underlying type is `{ type: "enqueue"; queue: string } | { type: "void" }`.
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` are runtime classes wrapping the engine's stream WebSockets
+using the browser's native WebSocket API. `StreamChannelRef` is the type passed between SDK calls
+to identify a channel:
+
+```typescript
+type StreamChannelRef = {
+ channel_id: string;
+ access_key: string;
+ direction: "read" | "write";
+};
+```
+
+`ChannelReader` exposes `onMessage`, `onBinary`, `readAll`, and `close`. `ChannelWriter` exposes
+`sendMessage(msg: string)`, `sendBinary(data: Uint8Array)`, and `close()`.
+
+## Connection state
+
+`IIIConnectionState` is the literal-type alias `"disconnected" | "connecting" | "connected" |
+"reconnecting" | "failed"`, exported from the package root.
+
+## Info types
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+
+`WorkerInfo` and `WorkerMetadata` are not part of this SDK. Use the engine introspection
+functions ([`engine::workers::list`](/sdk-reference/engine-sdk#engine-discovery-functions)) when a
+browser client needs metadata about other workers.
+
+## `MessageType`
+
+A runtime enum naming every wire frame the SDK exchanges with the engine. Rarely needed by
+callers.
+
+## Surfaces not in this SDK
+
+- **`Logger`.** The Browser SDK does not ship a structured logger; use the browser's built-in
+ `console` and rely on the OpenTelemetry surfaces provided by the
+ iii-observability worker for export.
+- **Error class.** There is no `IIIError` / `IIIInvocationError` class. Failures are surfaced as
+ rejected promises carrying `Error` instances; inspect the message and any attached `code` for
+ the engine error code.
diff --git a/docs/0-13-0/sdk-reference/browser-sdk.mdx.skill.md b/docs/0-13-0/sdk-reference/browser-sdk.mdx.skill.md
new file mode 100644
index 000000000..9d444f49a
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/browser-sdk.mdx.skill.md
@@ -0,0 +1,148 @@
+
+
+# Browser SDK
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source. The Browser SDK is a standalone implementation alongside the
+ Node SDK, not a build of it.
+
+
+## Installation
+
+```bash
+npm install iii-browser-sdk
+```
+
+## Common methods
+
+### `registerWorker`
+
+Connect a browser tab to a running iii engine and return its handle.
+
+```typescript
+function registerWorker(address: string, options?: InitOptions): ISdk;
+```
+
+`address` is the engine's SDK WebSocket URL. The returned `ISdk` carries every method below.
+
+### `registerFunction`
+
+Register a callable function in this tab.
+
+```typescript
+worker.registerFunction(
+ functionId: string,
+ handler: RemoteFunctionHandler,
+ options?: RegisterFunctionOptions,
+): FunctionRef;
+```
+
+### `registerTrigger`
+
+Bind a registered function to a configured trigger instance.
+
+```typescript
+worker.registerTrigger(trigger: RegisterTriggerInput): Trigger;
+```
+
+Drop the trigger with `Trigger.unregister()` on the returned handle. There is no top-level
+`unregisterTrigger`.
+
+### `registerTriggerType`
+
+Declare a new trigger type that this tab advertises.
+
+```typescript
+worker.registerTriggerType(
+ triggerType: RegisterTriggerTypeInput,
+ handler: TriggerHandler,
+): TriggerTypeRef;
+```
+
+### `unregisterTriggerType`
+
+Remove a previously registered trigger type.
+
+```typescript
+worker.unregisterTriggerType(triggerType: RegisterTriggerTypeInput): void;
+```
+
+### `trigger`
+
+Invoke a registered function.
+
+```typescript
+worker.trigger(request: TriggerRequest): Promise;
+```
+
+Resolves with the function's return value for synchronous calls, with an `EnqueueResult` for
+`TriggerAction.Enqueue` actions, and with `undefined` for `TriggerAction.Void`.
+
+### `shutdown`
+
+Disconnect from the engine and release resources.
+
+```typescript
+worker.shutdown(): Promise;
+```
+
+## Trigger actions
+
+`TriggerAction` is a runtime const that produces the value passed to `trigger`'s `action` field.
+
+```typescript
+TriggerAction.Void(); // fire-and-forget
+TriggerAction.Enqueue({ queue: "math" }); // route through iii-queue
+```
+
+The underlying type is `{ type: "enqueue"; queue: string } | { type: "void" }`.
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` are runtime classes wrapping the engine's stream WebSockets
+using the browser's native WebSocket API. `StreamChannelRef` is the type passed between SDK calls
+to identify a channel:
+
+```typescript
+type StreamChannelRef = {
+ channel_id: string;
+ access_key: string;
+ direction: "read" | "write";
+};
+```
+
+`ChannelReader` exposes `onMessage`, `onBinary`, `readAll`, and `close`. `ChannelWriter` exposes
+`sendMessage(msg: string)`, `sendBinary(data: Uint8Array)`, and `close()`.
+
+## Connection state
+
+`IIIConnectionState` is the literal-type alias `"disconnected" | "connecting" | "connected" |
+"reconnecting" | "failed"`, exported from the package root.
+
+## Info types
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+
+`WorkerInfo` and `WorkerMetadata` are not part of this SDK. Use the engine introspection
+functions ([`engine::workers::list`](/sdk-reference/engine-sdk#engine-discovery-functions)) when a
+browser client needs metadata about other workers.
+
+## `MessageType`
+
+A runtime enum naming every wire frame the SDK exchanges with the engine. Rarely needed by
+callers.
+
+## Surfaces not in this SDK
+
+- **`Logger`.** The Browser SDK does not ship a structured logger; use the browser's built-in
+ `console` and rely on the OpenTelemetry surfaces provided by the
+ iii-observability worker for export.
+- **Error class.** There is no `IIIError` / `IIIInvocationError` class. Failures are surfaced as
+ rejected promises carrying `Error` instances; inspect the message and any attached `code` for
+ the engine error code.
diff --git a/docs/0-13-0/sdk-reference/engine-sdk.mdx b/docs/0-13-0/sdk-reference/engine-sdk.mdx
new file mode 100644
index 000000000..6ddd82fd9
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/engine-sdk.mdx
@@ -0,0 +1,245 @@
+---
+title: "Engine protocol"
+description: "The raw WebSocket protocol that SDK workers and the engine speak."
+owner: "engineering"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page documents the wire-level protocol the engine and SDK workers exchange. Most projects use
+ a language SDK ([Node](/sdk-reference/node-sdk), [Python](/sdk-reference/python-sdk),
+ [Rust](/sdk-reference/rust-sdk), [Browser](/sdk-reference/browser-sdk)) and never touch the
+ protocol directly. The shapes below are the source of truth those SDKs serialize to.
+
+
+
+ Observability introspection (traces, logs, metrics, sampling rules, alerts, rollups) is owned
+ end-to-end by the iii-observability worker.
+
+
+## Connection ports
+
+The engine binds three ports of its own and runs alongside one more from the observability worker:
+
+| Port | Bound by | Surface |
+| ------- | ------------------ | ------------------------------------------------------------- |
+| `3111` | engine | REST API. |
+| `3112` | engine | Stream API (WebSocket; consumer-side stream subscriptions). |
+| `49134` | engine | SDK WebSocket; this is what `iii_sdk::register_worker` opens. |
+| `9464` | `iii-observability` worker | Prometheus metrics endpoint (typically exposed from the same container as the engine). |
+
+The console UI runs on `3113` and is launched separately by `iii console`.
+
+## Connection flow
+
+A worker opens the SDK WebSocket (default `ws://127.0.0.1:49134`) and sends the registrations it
+holds in memory: each `RegisterFunction`, `RegisterTrigger`, and `RegisterTriggerType` it intends to
+expose. The worker then calls `engine::workers::register` to publish its own metadata (runtime,
+version, OS, PID, isolation), and the engine answers with a `WorkerRegistered { worker_id }` frame
+carrying the assigned UUID.
+
+The connection is bidirectional from that point on: the engine pushes `InvokeFunction` frames at the
+worker, and the worker pushes `InvocationResult`, additional registrations, or unregistrations back.
+
+## Message types
+
+Every frame is a JSON object discriminated by `message_type`. The full set, defined on `Message` in
+[`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs):
+
+| Frame | Direction | Purpose |
+| --------------------------- | ---------------- | ------------------------------------------------------ |
+| `RegisterFunction` | worker -> engine | Make a function callable by `function_id`. |
+| `UnregisterFunction` | worker -> engine | Drop a previously registered function. |
+| `RegisterTrigger` | worker -> engine | Bind a function to a trigger instance. |
+| `UnregisterTrigger` | worker -> engine | Drop a trigger binding. |
+| `TriggerRegistrationResult` | engine -> worker | Ack / error for a `RegisterTrigger`. |
+| `RegisterTriggerType` | worker -> engine | Declare a new trigger type the worker advertises. |
+| `RegisterService` | worker -> engine | Group related functions under a service id. |
+| `InvokeFunction` | engine -> worker | Call a registered function with a payload. |
+| `InvocationResult` | worker -> engine | Carry the function's result or error back. |
+| `WorkerRegistered` | engine -> worker | Acknowledge the worker, with the assigned `worker_id`. |
+| `Ping` / `Pong` | bidirectional | Liveness; keeps idle connections from timing out. |
+
+## `RegisterFunction`
+
+```json
+{
+ "message_type": "register_function",
+ "id": "math::add",
+ "description": "Add two numbers.",
+ "request_format": {
+ "type": "object",
+ "properties": { "a": { "type": "number" }, "b": { "type": "number" } }
+ },
+ "response_format": { "type": "object", "properties": { "c": { "type": "number" } } },
+ "metadata": { "owner": "math-team" },
+ "invocation": null
+}
+```
+
+`id` is required. `description`, `request_format`, `response_format`, and `metadata` are optional
+and feed the iii console and the agent-readable skills. `invocation` is reserved for external HTTP
+functions (`HttpInvocationRef`); leave it `null` for in-process handlers.
+
+## `RegisterTrigger`
+
+```json
+{
+ "message_type": "register_trigger",
+ "id": "math::add@http",
+ "trigger_type": "http",
+ "function_id": "math::add",
+ "config": { "api_path": "/math/add", "http_method": "POST" },
+ "metadata": null
+}
+```
+
+`config` is the per-trigger-type configuration; the shape is defined by whatever worker advertised
+that `trigger_type` (e.g. `iii-http` for `http` triggers). The engine responds with a
+`TriggerRegistrationResult` carrying an optional `error: ErrorBody`.
+
+## `RegisterTriggerType`
+
+```json
+{
+ "message_type": "register_trigger_type",
+ "id": "webhook",
+ "description": "HTTP webhook trigger",
+ "trigger_request_format": { "type": "object", ... },
+ "call_request_format": { "type": "object", ... }
+}
+```
+
+`trigger_request_format` is the JSON Schema for the trigger's per-binding `config`.
+`call_request_format` is the JSON Schema for the payload delivered to bound functions when the
+trigger fires.
+
+## `InvokeFunction`
+
+```json
+{
+ "message_type": "invoke_function",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "data": { "a": 2, "b": 3 },
+ "traceparent": "00-…",
+ "baggage": "k=v,…",
+ "action": { "type": "void" }
+}
+```
+
+`invocation_id` is omitted on `Void` invocations (the worker has no result channel to reply on).
+`traceparent` and `baggage` carry W3C trace context. `action` is the routing flag (see
+[Trigger actions](#trigger-actions) below); absent / `null` means synchronous.
+
+## `InvocationResult`
+
+Success:
+
+```json
+{
+ "message_type": "invocation_result",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "result": { "c": 5 },
+ "error": null,
+ "traceparent": "00-…",
+ "baggage": "k=v,…"
+}
+```
+
+Failure:
+
+```json
+{
+ "message_type": "invocation_result",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "result": null,
+ "error": {
+ "code": "invocation_failed",
+ "message": "boom",
+ "stacktrace": "TraceError: …"
+ }
+}
+```
+
+`ErrorBody.code` values the engine emits today include `invocation_failed` (handler threw),
+`invocation_stopped` (the owning worker disconnected mid-flight, so the engine cancels the in-flight
+call and surfaces this code to the caller), `function_not_found`, `function_not_invokable`,
+`TIMEOUT` (client-side timeout), `FORBIDDEN` (RBAC denial).
+
+## Trigger actions
+
+`InvokeFunction.action` is tagged by `type` and lowercase-encoded on the wire:
+
+| Wire shape | Meaning |
+| ---------------------------------------- | -------------------------------------------------------- |
+| omitted / `null` | Synchronous; the worker replies with `InvocationResult`. |
+| `{ "type": "void" }` | Fire-and-forget; no `invocation_id`, no reply. |
+| `{ "type": "enqueue", "queue": "math" }` | Route through the named queue (provided by `iii-queue`). |
+
+## Invocation lifecycle
+
+For synchronous calls the engine assigns an `invocation_id`, forwards the `InvokeFunction` to the
+owning worker, and waits for the matching `InvocationResult`. For `Void` actions the engine forwards
+without an `invocation_id` and never expects a reply. For `Enqueue` the engine hands the invocation
+to the queue worker, which persists it and re-invokes the target function on a subscriber according
+to the queue's retry policy.
+
+## Engine discovery functions
+
+The engine registers a built-in set of functions under the `engine::*` namespace for introspection
+and worker lifecycle. Defined in
+[`engine/src/workers/engine_fn/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/engine_fn/mod.rs):
+
+| Function | Purpose |
+| ----------------------------- | ----------------------------------------------------------------------------- |
+| `engine::channels::create` | Create a streaming-channel reader / writer pair. |
+| `engine::functions::list` | List every registered function (filterable by `include_internal`). |
+| `engine::workers::list` | List every connected worker with metrics. |
+| `engine::triggers::list` | List every registered trigger (filterable by `include_internal`). |
+| `engine::trigger-types::list` | List every registered trigger type with its config and call request schemas. |
+| `engine::workers::register` | Publish the calling worker's metadata (runtime, version, OS, PID, isolation). |
+
+## Engine discovery triggers
+
+| Trigger | Fires when |
+| ----------------------------- | ----------------------------------------- |
+| `engine::functions-available` | A function is registered or unregistered. |
+| `engine::workers-available` | A worker connects or disconnects. |
+
+## Engine-collected metrics
+
+These metrics are emitted by the engine regardless of which language SDK a worker uses. Names and
+units come from
+[`engine/src/workers/observability/metrics.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/observability/metrics.rs).
+
+### Invocations
+
+| Metric | Instrument | Unit |
+| ----------------------------- | ---------- | ----------- |
+| `iii.invocations.total` | counter | invocations |
+| `iii.invocation.duration` | histogram | seconds |
+| `iii.invocation.errors.total` | counter | errors |
+
+### Workers
+
+| Metric | Instrument | Unit |
+| -------------------------- | ---------- | ------- |
+| `iii.workers.active` | gauge | workers |
+| `iii.workers.spawns.total` | counter | workers |
+| `iii.workers.deaths.total` | counter | workers |
+| `iii.workers.by_status` | gauge | workers |
+
+### Per-worker
+
+| Metric | Instrument | Unit |
+| ------------------------------ | ---------- | ----- |
+| `iii.worker.memory.heap.bytes` | gauge | bytes |
+| `iii.worker.memory.rss.bytes` | gauge | bytes |
+| `iii.worker.cpu.percent` | gauge | % |
+| `iii.worker.event_loop.lag.ms` | gauge | ms |
+| `iii.worker.uptime.seconds` | gauge | s |
diff --git a/docs/0-13-0/sdk-reference/engine-sdk.mdx.skill.md b/docs/0-13-0/sdk-reference/engine-sdk.mdx.skill.md
new file mode 100644
index 000000000..27d426a8d
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/engine-sdk.mdx.skill.md
@@ -0,0 +1,243 @@
+
+
+# Engine protocol
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page documents the wire-level protocol the engine and SDK workers exchange. Most projects use
+ a language SDK ([Node](/sdk-reference/node-sdk), [Python](/sdk-reference/python-sdk),
+ [Rust](/sdk-reference/rust-sdk), [Browser](/sdk-reference/browser-sdk)) and never touch the
+ protocol directly. The shapes below are the source of truth those SDKs serialize to.
+
+
+
+ Observability introspection (traces, logs, metrics, sampling rules, alerts, rollups) is owned
+ end-to-end by the iii-observability worker.
+
+
+## Connection ports
+
+The engine binds three ports of its own and runs alongside one more from the observability worker:
+
+| Port | Bound by | Surface |
+| ------- | ------------------ | ------------------------------------------------------------- |
+| `3111` | engine | REST API. |
+| `3112` | engine | Stream API (WebSocket; consumer-side stream subscriptions). |
+| `49134` | engine | SDK WebSocket; this is what `iii_sdk::register_worker` opens. |
+| `9464` | `iii-observability` worker | Prometheus metrics endpoint (typically exposed from the same container as the engine). |
+
+The console UI runs on `3113` and is launched separately by `iii console`.
+
+## Connection flow
+
+A worker opens the SDK WebSocket (default `ws://127.0.0.1:49134`) and sends the registrations it
+holds in memory: each `RegisterFunction`, `RegisterTrigger`, and `RegisterTriggerType` it intends to
+expose. The worker then calls `engine::workers::register` to publish its own metadata (runtime,
+version, OS, PID, isolation), and the engine answers with a `WorkerRegistered { worker_id }` frame
+carrying the assigned UUID.
+
+The connection is bidirectional from that point on: the engine pushes `InvokeFunction` frames at the
+worker, and the worker pushes `InvocationResult`, additional registrations, or unregistrations back.
+
+## Message types
+
+Every frame is a JSON object discriminated by `message_type`. The full set, defined on `Message` in
+[`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs):
+
+| Frame | Direction | Purpose |
+| --------------------------- | ---------------- | ------------------------------------------------------ |
+| `RegisterFunction` | worker -> engine | Make a function callable by `function_id`. |
+| `UnregisterFunction` | worker -> engine | Drop a previously registered function. |
+| `RegisterTrigger` | worker -> engine | Bind a function to a trigger instance. |
+| `UnregisterTrigger` | worker -> engine | Drop a trigger binding. |
+| `TriggerRegistrationResult` | engine -> worker | Ack / error for a `RegisterTrigger`. |
+| `RegisterTriggerType` | worker -> engine | Declare a new trigger type the worker advertises. |
+| `RegisterService` | worker -> engine | Group related functions under a service id. |
+| `InvokeFunction` | engine -> worker | Call a registered function with a payload. |
+| `InvocationResult` | worker -> engine | Carry the function's result or error back. |
+| `WorkerRegistered` | engine -> worker | Acknowledge the worker, with the assigned `worker_id`. |
+| `Ping` / `Pong` | bidirectional | Liveness; keeps idle connections from timing out. |
+
+## `RegisterFunction`
+
+```json
+{
+ "message_type": "register_function",
+ "id": "math::add",
+ "description": "Add two numbers.",
+ "request_format": {
+ "type": "object",
+ "properties": { "a": { "type": "number" }, "b": { "type": "number" } }
+ },
+ "response_format": { "type": "object", "properties": { "c": { "type": "number" } } },
+ "metadata": { "owner": "math-team" },
+ "invocation": null
+}
+```
+
+`id` is required. `description`, `request_format`, `response_format`, and `metadata` are optional
+and feed the iii console and the agent-readable skills. `invocation` is reserved for external HTTP
+functions (`HttpInvocationRef`); leave it `null` for in-process handlers.
+
+## `RegisterTrigger`
+
+```json
+{
+ "message_type": "register_trigger",
+ "id": "math::add@http",
+ "trigger_type": "http",
+ "function_id": "math::add",
+ "config": { "api_path": "/math/add", "http_method": "POST" },
+ "metadata": null
+}
+```
+
+`config` is the per-trigger-type configuration; the shape is defined by whatever worker advertised
+that `trigger_type` (e.g. `iii-http` for `http` triggers). The engine responds with a
+`TriggerRegistrationResult` carrying an optional `error: ErrorBody`.
+
+## `RegisterTriggerType`
+
+```json
+{
+ "message_type": "register_trigger_type",
+ "id": "webhook",
+ "description": "HTTP webhook trigger",
+ "trigger_request_format": { "type": "object", ... },
+ "call_request_format": { "type": "object", ... }
+}
+```
+
+`trigger_request_format` is the JSON Schema for the trigger's per-binding `config`.
+`call_request_format` is the JSON Schema for the payload delivered to bound functions when the
+trigger fires.
+
+## `InvokeFunction`
+
+```json
+{
+ "message_type": "invoke_function",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "data": { "a": 2, "b": 3 },
+ "traceparent": "00-…",
+ "baggage": "k=v,…",
+ "action": { "type": "void" }
+}
+```
+
+`invocation_id` is omitted on `Void` invocations (the worker has no result channel to reply on).
+`traceparent` and `baggage` carry W3C trace context. `action` is the routing flag (see
+[Trigger actions](#trigger-actions) below); absent / `null` means synchronous.
+
+## `InvocationResult`
+
+Success:
+
+```json
+{
+ "message_type": "invocation_result",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "result": { "c": 5 },
+ "error": null,
+ "traceparent": "00-…",
+ "baggage": "k=v,…"
+}
+```
+
+Failure:
+
+```json
+{
+ "message_type": "invocation_result",
+ "invocation_id": "9f3c…",
+ "function_id": "math::add",
+ "result": null,
+ "error": {
+ "code": "invocation_failed",
+ "message": "boom",
+ "stacktrace": "TraceError: …"
+ }
+}
+```
+
+`ErrorBody.code` values the engine emits today include `invocation_failed` (handler threw),
+`invocation_stopped` (the owning worker disconnected mid-flight, so the engine cancels the in-flight
+call and surfaces this code to the caller), `function_not_found`, `function_not_invokable`,
+`TIMEOUT` (client-side timeout), `FORBIDDEN` (RBAC denial).
+
+## Trigger actions
+
+`InvokeFunction.action` is tagged by `type` and lowercase-encoded on the wire:
+
+| Wire shape | Meaning |
+| ---------------------------------------- | -------------------------------------------------------- |
+| omitted / `null` | Synchronous; the worker replies with `InvocationResult`. |
+| `{ "type": "void" }` | Fire-and-forget; no `invocation_id`, no reply. |
+| `{ "type": "enqueue", "queue": "math" }` | Route through the named queue (provided by `iii-queue`). |
+
+## Invocation lifecycle
+
+For synchronous calls the engine assigns an `invocation_id`, forwards the `InvokeFunction` to the
+owning worker, and waits for the matching `InvocationResult`. For `Void` actions the engine forwards
+without an `invocation_id` and never expects a reply. For `Enqueue` the engine hands the invocation
+to the queue worker, which persists it and re-invokes the target function on a subscriber according
+to the queue's retry policy.
+
+## Engine discovery functions
+
+The engine registers a built-in set of functions under the `engine::*` namespace for introspection
+and worker lifecycle. Defined in
+[`engine/src/workers/engine_fn/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/engine_fn/mod.rs):
+
+| Function | Purpose |
+| ----------------------------- | ----------------------------------------------------------------------------- |
+| `engine::channels::create` | Create a streaming-channel reader / writer pair. |
+| `engine::functions::list` | List every registered function (filterable by `include_internal`). |
+| `engine::workers::list` | List every connected worker with metrics. |
+| `engine::triggers::list` | List every registered trigger (filterable by `include_internal`). |
+| `engine::trigger-types::list` | List every registered trigger type with its config and call request schemas. |
+| `engine::workers::register` | Publish the calling worker's metadata (runtime, version, OS, PID, isolation). |
+
+## Engine discovery triggers
+
+| Trigger | Fires when |
+| ----------------------------- | ----------------------------------------- |
+| `engine::functions-available` | A function is registered or unregistered. |
+| `engine::workers-available` | A worker connects or disconnects. |
+
+## Engine-collected metrics
+
+These metrics are emitted by the engine regardless of which language SDK a worker uses. Names and
+units come from
+[`engine/src/workers/observability/metrics.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/observability/metrics.rs).
+
+### Invocations
+
+| Metric | Instrument | Unit |
+| ----------------------------- | ---------- | ----------- |
+| `iii.invocations.total` | counter | invocations |
+| `iii.invocation.duration` | histogram | seconds |
+| `iii.invocation.errors.total` | counter | errors |
+
+### Workers
+
+| Metric | Instrument | Unit |
+| -------------------------- | ---------- | ------- |
+| `iii.workers.active` | gauge | workers |
+| `iii.workers.spawns.total` | counter | workers |
+| `iii.workers.deaths.total` | counter | workers |
+| `iii.workers.by_status` | gauge | workers |
+
+### Per-worker
+
+| Metric | Instrument | Unit |
+| ------------------------------ | ---------- | ----- |
+| `iii.worker.memory.heap.bytes` | gauge | bytes |
+| `iii.worker.memory.rss.bytes` | gauge | bytes |
+| `iii.worker.cpu.percent` | gauge | % |
+| `iii.worker.event_loop.lag.ms` | gauge | ms |
+| `iii.worker.uptime.seconds` | gauge | s |
diff --git a/docs/0-13-0/sdk-reference/node-sdk.mdx b/docs/0-13-0/sdk-reference/node-sdk.mdx
new file mode 100644
index 000000000..72f91368c
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/node-sdk.mdx
@@ -0,0 +1,177 @@
+---
+title: "Node.js SDK"
+description: "Public surface of the iii Node.js SDK (`iii-sdk`)."
+owner: "engineering"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+npm install iii-sdk
+```
+
+## Common methods
+
+### `registerWorker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```typescript
+function registerWorker(address: string, options?: InitOptions): ISdk;
+```
+
+Pass the engine's SDK WebSocket URL (e.g. `process.env.III_URL`) as `address`. `options` configures
+worker identity, timeouts, reconnection, and OpenTelemetry. The returned `ISdk` carries every
+method below.
+
+### `registerFunction`
+
+Register a callable function on this worker.
+
+```typescript
+worker.registerFunction(
+ functionId: string,
+ handlerOrInvocation: RemoteFunctionHandler | HttpInvocationConfig,
+ options?: RegisterFunctionOptions,
+): FunctionRef;
+```
+
+`options` accepts `description`, `metadata`, and optional `request_format` / `response_format`
+JSON Schemas (stored alongside the function for the iii console and agent-readable skills).
+
+### `registerTrigger`
+
+Bind a registered function to a configured trigger instance.
+
+```typescript
+worker.registerTrigger(trigger: RegisterTriggerInput): Trigger;
+```
+
+The returned `Trigger` carries the runtime handle. Drop the trigger with `Trigger.unregister()`;
+there is no top-level `unregisterTrigger` function.
+
+### `registerTriggerType`
+
+Declare a new trigger type that this worker advertises so other workers can bind their functions to
+it.
+
+```typescript
+worker.registerTriggerType(
+ triggerType: RegisterTriggerTypeInput,
+ handler: TriggerHandler,
+): TriggerTypeRef;
+```
+
+### `unregisterTriggerType`
+
+Remove a previously registered trigger type.
+
+```typescript
+worker.unregisterTriggerType(triggerType: RegisterTriggerTypeInput): void;
+```
+
+### `trigger`
+
+Invoke a registered function. Resolves with the function's return value for synchronous calls,
+with an `EnqueueResult` for `TriggerAction.Enqueue` actions, and with `undefined` for
+`TriggerAction.Void`.
+
+```typescript
+worker.trigger(request: TriggerRequest): Promise;
+```
+
+### `shutdown`
+
+Disconnect from the engine and release resources, flushing any pending observability data.
+
+```typescript
+worker.shutdown(): Promise;
+```
+
+## Trigger actions
+
+`TriggerAction` is a runtime const that produces the value passed to `trigger`'s `action` field.
+
+```typescript
+TriggerAction.Void(); // fire-and-forget
+TriggerAction.Enqueue({ queue: "math" }); // route through iii-queue
+```
+
+The underlying type is the discriminated union `{ type: "enqueue"; queue: string } | { type: "void" }`,
+exported as `TriggerActionType`.
+
+## Error type
+
+`IIIInvocationError extends Error`. Every failure that crosses the SDK boundary is thrown as an
+instance of this class with a `code: string`, a `message: string`, an optional `function_id`, and
+an optional `stacktrace`.
+
+```typescript
+class IIIInvocationError extends Error {
+ code: string;
+ message: string;
+ function_id?: string;
+ stacktrace?: string;
+}
+```
+
+Common `code` values come from the engine: `invocation_failed` (handler threw), `invocation_stopped`
+(engine timeout), `function_not_found`, `function_not_invokable`, `TIMEOUT` (client-side timeout),
+`FORBIDDEN` (RBAC denial).
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` are runtime classes wrapping the engine's stream WebSockets.
+`StreamChannelRef` is the type passed between SDK calls to identify a channel:
+
+```typescript
+type StreamChannelRef = {
+ channel_id: string;
+ access_key: string;
+ direction: "read" | "write";
+};
+```
+
+Construct each with the engine's WS base URL and a `StreamChannelRef`. `ChannelReader` exposes a
+Node `Readable` plus `.sendMessage()` and `.onMessage()`; `ChannelWriter` exposes a `Writable` plus
+`.sendMessage()`, `.sendChunked()`, and `.close()`.
+
+## Logger
+
+`Logger` is a runtime class with `info`, `warn`, `error`, and `debug` methods, each
+`(message: string, data?: unknown) => void`. The output integrates with the SDK's OpenTelemetry
+setup; see iii-observability for the export
+side.
+
+## Info types
+
+The SDK re-exports the structured types the engine returns when listing system state:
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config`, optional `metadata`.
+- `WorkerInfo`. `id`, `name`, runtime/version/OS fields, IP, `status`, `connected_at_ms`,
+ `function_count`, registered `functions`, `active_invocations`, optional `isolation`.
+
+`WorkerMetadata` is not part of this SDK; use `WorkerInfo` for worker-side metadata.
+
+## MessageType
+
+`MessageType` is a runtime enum naming every wire frame the SDK exchanges with the engine
+(`RegisterFunction`, `InvokeFunction`, `InvocationResult`, `RegisterTrigger`, `WorkerRegistered`,
+and the rest). Callers rarely use it directly; it surfaces in middleware hooks and protocol-level
+custom code.
+
+## Connection state
+
+The connection-state literal union (`"disconnected" | "connecting" | "connected" | "reconnecting"
+| "failed"`) is internal in the current build. Treat the connection as established once
+`registerWorker` returns; failures raise on the first SDK call.
diff --git a/docs/0-13-0/sdk-reference/node-sdk.mdx.skill.md b/docs/0-13-0/sdk-reference/node-sdk.mdx.skill.md
new file mode 100644
index 000000000..bc12bd831
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/node-sdk.mdx.skill.md
@@ -0,0 +1,175 @@
+
+
+# Node.js SDK
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+npm install iii-sdk
+```
+
+## Common methods
+
+### `registerWorker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```typescript
+function registerWorker(address: string, options?: InitOptions): ISdk;
+```
+
+Pass the engine's SDK WebSocket URL (e.g. `process.env.III_URL`) as `address`. `options` configures
+worker identity, timeouts, reconnection, and OpenTelemetry. The returned `ISdk` carries every
+method below.
+
+### `registerFunction`
+
+Register a callable function on this worker.
+
+```typescript
+worker.registerFunction(
+ functionId: string,
+ handlerOrInvocation: RemoteFunctionHandler | HttpInvocationConfig,
+ options?: RegisterFunctionOptions,
+): FunctionRef;
+```
+
+`options` accepts `description`, `metadata`, and optional `request_format` / `response_format`
+JSON Schemas (stored alongside the function for the iii console and agent-readable skills).
+
+### `registerTrigger`
+
+Bind a registered function to a configured trigger instance.
+
+```typescript
+worker.registerTrigger(trigger: RegisterTriggerInput): Trigger;
+```
+
+The returned `Trigger` carries the runtime handle. Drop the trigger with `Trigger.unregister()`;
+there is no top-level `unregisterTrigger` function.
+
+### `registerTriggerType`
+
+Declare a new trigger type that this worker advertises so other workers can bind their functions to
+it.
+
+```typescript
+worker.registerTriggerType(
+ triggerType: RegisterTriggerTypeInput,
+ handler: TriggerHandler,
+): TriggerTypeRef;
+```
+
+### `unregisterTriggerType`
+
+Remove a previously registered trigger type.
+
+```typescript
+worker.unregisterTriggerType(triggerType: RegisterTriggerTypeInput): void;
+```
+
+### `trigger`
+
+Invoke a registered function. Resolves with the function's return value for synchronous calls,
+with an `EnqueueResult` for `TriggerAction.Enqueue` actions, and with `undefined` for
+`TriggerAction.Void`.
+
+```typescript
+worker.trigger(request: TriggerRequest): Promise;
+```
+
+### `shutdown`
+
+Disconnect from the engine and release resources, flushing any pending observability data.
+
+```typescript
+worker.shutdown(): Promise;
+```
+
+## Trigger actions
+
+`TriggerAction` is a runtime const that produces the value passed to `trigger`'s `action` field.
+
+```typescript
+TriggerAction.Void(); // fire-and-forget
+TriggerAction.Enqueue({ queue: "math" }); // route through iii-queue
+```
+
+The underlying type is the discriminated union `{ type: "enqueue"; queue: string } | { type: "void" }`,
+exported as `TriggerActionType`.
+
+## Error type
+
+`IIIInvocationError extends Error`. Every failure that crosses the SDK boundary is thrown as an
+instance of this class with a `code: string`, a `message: string`, an optional `function_id`, and
+an optional `stacktrace`.
+
+```typescript
+class IIIInvocationError extends Error {
+ code: string;
+ message: string;
+ function_id?: string;
+ stacktrace?: string;
+}
+```
+
+Common `code` values come from the engine: `invocation_failed` (handler threw), `invocation_stopped`
+(engine timeout), `function_not_found`, `function_not_invokable`, `TIMEOUT` (client-side timeout),
+`FORBIDDEN` (RBAC denial).
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` are runtime classes wrapping the engine's stream WebSockets.
+`StreamChannelRef` is the type passed between SDK calls to identify a channel:
+
+```typescript
+type StreamChannelRef = {
+ channel_id: string;
+ access_key: string;
+ direction: "read" | "write";
+};
+```
+
+Construct each with the engine's WS base URL and a `StreamChannelRef`. `ChannelReader` exposes a
+Node `Readable` plus `.sendMessage()` and `.onMessage()`; `ChannelWriter` exposes a `Writable` plus
+`.sendMessage()`, `.sendChunked()`, and `.close()`.
+
+## Logger
+
+`Logger` is a runtime class with `info`, `warn`, `error`, and `debug` methods, each
+`(message: string, data?: unknown) => void`. The output integrates with the SDK's OpenTelemetry
+setup; see iii-observability for the export
+side.
+
+## Info types
+
+The SDK re-exports the structured types the engine returns when listing system state:
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config`, optional `metadata`.
+- `WorkerInfo`. `id`, `name`, runtime/version/OS fields, IP, `status`, `connected_at_ms`,
+ `function_count`, registered `functions`, `active_invocations`, optional `isolation`.
+
+`WorkerMetadata` is not part of this SDK; use `WorkerInfo` for worker-side metadata.
+
+## MessageType
+
+`MessageType` is a runtime enum naming every wire frame the SDK exchanges with the engine
+(`RegisterFunction`, `InvokeFunction`, `InvocationResult`, `RegisterTrigger`, `WorkerRegistered`,
+and the rest). Callers rarely use it directly; it surfaces in middleware hooks and protocol-level
+custom code.
+
+## Connection state
+
+The connection-state literal union (`"disconnected" | "connecting" | "connected" | "reconnecting"
+| "failed"`) is internal in the current build. Treat the connection as established once
+`registerWorker` returns; failures raise on the first SDK call.
diff --git a/docs/0-13-0/sdk-reference/python-sdk.mdx b/docs/0-13-0/sdk-reference/python-sdk.mdx
new file mode 100644
index 000000000..8240f037d
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/python-sdk.mdx
@@ -0,0 +1,186 @@
+---
+title: "Python SDK"
+description: "Public surface of the iii Python SDK (`iii-sdk`, imported as `iii`)."
+owner: "engineering"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+pip install iii-sdk
+```
+
+Imported as `iii`.
+
+## Common methods
+
+### `register_worker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```python
+def register_worker(address: str, options: InitOptions | None = None) -> III: ...
+```
+
+`address` is the engine's SDK WebSocket URL. `options` configures worker identity and
+reconnection. The returned `III` instance carries every method below.
+
+
+ The SDK's OpenTelemetry hookup is wired through `options` as well; the export and rollup side is
+ owned by iii-observability.
+
+
+### `register_function`
+
+Register a callable function on this worker.
+
+```python
+def register_function(
+ self,
+ function_id: str,
+ handler_or_invocation: RemoteFunctionHandler | HttpInvocationConfig,
+ *,
+ description: str | None = None,
+ metadata: dict[str, Any] | None = None,
+ request_format: RegisterFunctionFormat | dict[str, Any] | None = None,
+ response_format: RegisterFunctionFormat | dict[str, Any] | None = None,
+) -> FunctionRef: ...
+```
+
+`request_format` / `response_format` accept either a JSON Schema dict or a `RegisterFunctionFormat`
+helper. They are stored alongside the function for the iii console and the agent-readable skills.
+
+### `register_trigger`
+
+Bind a registered function to a configured trigger instance.
+
+```python
+def register_trigger(self, trigger: RegisterTriggerInput | dict[str, Any]) -> Trigger: ...
+```
+
+Drop the trigger with `trigger.unregister()` on the returned handle. There is no top-level
+`unregister_trigger` method.
+
+### `register_trigger_type`
+
+Declare a new trigger type that this worker advertises.
+
+```python
+def register_trigger_type(
+ self,
+ trigger_type: RegisterTriggerTypeInput | dict[str, Any],
+ handler: TriggerHandler[Any],
+) -> TriggerTypeRef[Any, Any]: ...
+```
+
+### `unregister_trigger_type`
+
+Remove a previously registered trigger type.
+
+```python
+def unregister_trigger_type(
+ self,
+ trigger_type: RegisterTriggerTypeInput | dict[str, Any],
+) -> None: ...
+```
+
+### `trigger` / `trigger_async`
+
+Invoke a registered function. `trigger` is the synchronous entry point (runs the async machinery
+on the SDK's internal loop); `trigger_async` is the awaitable form for callers inside `asyncio`.
+
+```python
+def trigger(self, request: dict[str, Any] | TriggerRequest) -> Any: ...
+async def trigger_async(self, request: dict[str, Any] | TriggerRequest) -> Any: ...
+```
+
+Both return the function's value for synchronous invocations, an `EnqueueResult` for
+`TriggerAction.Enqueue` actions, and `None` for `TriggerAction.Void`.
+
+### `shutdown` / `shutdown_async`
+
+Disconnect from the engine and release resources. Use `shutdown_async` from `asyncio` contexts.
+
+```python
+def shutdown(self) -> None: ...
+async def shutdown_async(self) -> None: ...
+```
+
+## Trigger actions
+
+`TriggerAction` is a factory class with two static helpers; `TriggerActionEnqueue` and
+`TriggerActionVoid` are the concrete return shapes.
+
+```python
+TriggerAction.Void() # fire-and-forget; returns TriggerActionVoid()
+TriggerAction.Enqueue(queue="math") # route through iii-queue; returns TriggerActionEnqueue(...)
+```
+
+`queue` is a keyword-only argument on `Enqueue`.
+
+## Error types
+
+The base class is `IIIInvocationError`. The SDK's wire-error decoder maps engine error codes to
+two known subclasses; everything else stays on the base class:
+
+| Class | When raised |
+| -------------------- | --------------------------------- |
+| `IIIInvocationError` | Any engine-side invocation error. |
+| `IIIForbiddenError` | `code == "FORBIDDEN"` (RBAC). |
+| `IIITimeoutError` | `code == "TIMEOUT"`. |
+
+All three are exported. The base class has `code`, `message`, `function_id`, and `stacktrace`
+attributes.
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` wrap the engine's stream WebSockets. `StreamChannelRef`
+identifies a channel:
+
+```python
+class StreamChannelRef(BaseModel):
+ channel_id: str
+ access_key: str
+ direction: Literal["read", "write"]
+```
+
+Both classes are constructed with the engine's WS base URL and a `StreamChannelRef`.
+
+## Logger
+
+`Logger` exposes `info`, `warn`, `error`, and `debug`, each accepting a message and an optional
+data dict. The output integrates with the SDK's OpenTelemetry setup; see
+iii-observability for the export side.
+
+## Info types
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+
+`WorkerInfo` exists in `iii_types` but isn't currently re-exported from the package root; import
+it from `iii.iii_types` when needed. `WorkerMetadata` is not part of this SDK.
+
+## `MessageType`
+
+A runtime enum naming every wire frame the SDK exchanges with the engine. Used internally by
+middleware; rarely needed by callers.
+
+## `RegisterFunctionFormat`
+
+The Python-only helper for declaring a function's request or response schema in a structured way.
+Accepts either a JSON Schema dict directly or constructed values; both forms reach `register_function`.
+
+## Connection state
+
+`IIIConnectionState` is the literal-type alias `"disconnected" | "connecting" | "connected" |
+"reconnecting" | "failed"`. It is defined in `iii.iii_constants` but not re-exported from the
+package root; treat the connection as established once `register_worker` returns.
diff --git a/docs/0-13-0/sdk-reference/python-sdk.mdx.skill.md b/docs/0-13-0/sdk-reference/python-sdk.mdx.skill.md
new file mode 100644
index 000000000..0bb76a9d7
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/python-sdk.mdx.skill.md
@@ -0,0 +1,184 @@
+
+
+# Python SDK
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+pip install iii-sdk
+```
+
+Imported as `iii`.
+
+## Common methods
+
+### `register_worker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```python
+def register_worker(address: str, options: InitOptions | None = None) -> III: ...
+```
+
+`address` is the engine's SDK WebSocket URL. `options` configures worker identity and
+reconnection. The returned `III` instance carries every method below.
+
+
+ The SDK's OpenTelemetry hookup is wired through `options` as well; the export and rollup side is
+ owned by iii-observability.
+
+
+### `register_function`
+
+Register a callable function on this worker.
+
+```python
+def register_function(
+ self,
+ function_id: str,
+ handler_or_invocation: RemoteFunctionHandler | HttpInvocationConfig,
+ *,
+ description: str | None = None,
+ metadata: dict[str, Any] | None = None,
+ request_format: RegisterFunctionFormat | dict[str, Any] | None = None,
+ response_format: RegisterFunctionFormat | dict[str, Any] | None = None,
+) -> FunctionRef: ...
+```
+
+`request_format` / `response_format` accept either a JSON Schema dict or a `RegisterFunctionFormat`
+helper. They are stored alongside the function for the iii console and the agent-readable skills.
+
+### `register_trigger`
+
+Bind a registered function to a configured trigger instance.
+
+```python
+def register_trigger(self, trigger: RegisterTriggerInput | dict[str, Any]) -> Trigger: ...
+```
+
+Drop the trigger with `trigger.unregister()` on the returned handle. There is no top-level
+`unregister_trigger` method.
+
+### `register_trigger_type`
+
+Declare a new trigger type that this worker advertises.
+
+```python
+def register_trigger_type(
+ self,
+ trigger_type: RegisterTriggerTypeInput | dict[str, Any],
+ handler: TriggerHandler[Any],
+) -> TriggerTypeRef[Any, Any]: ...
+```
+
+### `unregister_trigger_type`
+
+Remove a previously registered trigger type.
+
+```python
+def unregister_trigger_type(
+ self,
+ trigger_type: RegisterTriggerTypeInput | dict[str, Any],
+) -> None: ...
+```
+
+### `trigger` / `trigger_async`
+
+Invoke a registered function. `trigger` is the synchronous entry point (runs the async machinery
+on the SDK's internal loop); `trigger_async` is the awaitable form for callers inside `asyncio`.
+
+```python
+def trigger(self, request: dict[str, Any] | TriggerRequest) -> Any: ...
+async def trigger_async(self, request: dict[str, Any] | TriggerRequest) -> Any: ...
+```
+
+Both return the function's value for synchronous invocations, an `EnqueueResult` for
+`TriggerAction.Enqueue` actions, and `None` for `TriggerAction.Void`.
+
+### `shutdown` / `shutdown_async`
+
+Disconnect from the engine and release resources. Use `shutdown_async` from `asyncio` contexts.
+
+```python
+def shutdown(self) -> None: ...
+async def shutdown_async(self) -> None: ...
+```
+
+## Trigger actions
+
+`TriggerAction` is a factory class with two static helpers; `TriggerActionEnqueue` and
+`TriggerActionVoid` are the concrete return shapes.
+
+```python
+TriggerAction.Void() # fire-and-forget; returns TriggerActionVoid()
+TriggerAction.Enqueue(queue="math") # route through iii-queue; returns TriggerActionEnqueue(...)
+```
+
+`queue` is a keyword-only argument on `Enqueue`.
+
+## Error types
+
+The base class is `IIIInvocationError`. The SDK's wire-error decoder maps engine error codes to
+two known subclasses; everything else stays on the base class:
+
+| Class | When raised |
+| -------------------- | --------------------------------- |
+| `IIIInvocationError` | Any engine-side invocation error. |
+| `IIIForbiddenError` | `code == "FORBIDDEN"` (RBAC). |
+| `IIITimeoutError` | `code == "TIMEOUT"`. |
+
+All three are exported. The base class has `code`, `message`, `function_id`, and `stacktrace`
+attributes.
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` wrap the engine's stream WebSockets. `StreamChannelRef`
+identifies a channel:
+
+```python
+class StreamChannelRef(BaseModel):
+ channel_id: str
+ access_key: str
+ direction: Literal["read", "write"]
+```
+
+Both classes are constructed with the engine's WS base URL and a `StreamChannelRef`.
+
+## Logger
+
+`Logger` exposes `info`, `warn`, `error`, and `debug`, each accepting a message and an optional
+data dict. The output integrates with the SDK's OpenTelemetry setup; see
+iii-observability for the export side.
+
+## Info types
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+
+`WorkerInfo` exists in `iii_types` but isn't currently re-exported from the package root; import
+it from `iii.iii_types` when needed. `WorkerMetadata` is not part of this SDK.
+
+## `MessageType`
+
+A runtime enum naming every wire frame the SDK exchanges with the engine. Used internally by
+middleware; rarely needed by callers.
+
+## `RegisterFunctionFormat`
+
+The Python-only helper for declaring a function's request or response schema in a structured way.
+Accepts either a JSON Schema dict directly or constructed values; both forms reach `register_function`.
+
+## Connection state
+
+`IIIConnectionState` is the literal-type alias `"disconnected" | "connecting" | "connected" |
+"reconnecting" | "failed"`. It is defined in `iii.iii_constants` but not re-exported from the
+package root; treat the connection as established once `register_worker` returns.
diff --git a/docs/0-13-0/sdk-reference/rust-sdk.mdx b/docs/0-13-0/sdk-reference/rust-sdk.mdx
new file mode 100644
index 000000000..ff8389078
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/rust-sdk.mdx
@@ -0,0 +1,202 @@
+---
+title: "Rust SDK"
+description: "Public surface of the iii Rust SDK (`iii-sdk`, imported as `iii_sdk`)."
+owner: "engineering"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+cargo add iii-sdk
+```
+
+Imported as `iii_sdk`.
+
+## Common methods
+
+### `register_worker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```rust
+pub fn register_worker(address: &str, options: InitOptions) -> III;
+```
+
+The returned `III` carries every method below. Spawned async tasks are driven on the SDK's
+internal tokio runtime.
+
+### `register_function`
+
+Register a callable function on this worker. Request and response schemas are derived from the
+handler's input and output types via the `schemars::JsonSchema` derive; the call site doesn't
+restate them.
+
+```rust
+pub fn register_function(
+ &self,
+ registration: R,
+) -> FunctionRef;
+```
+
+Build the `RegisterFunction` value via `RegisterFunction::new("namespace::name", handler)` and pass
+it to `register_function`. The handler's parameter and return types must implement
+`serde::Deserialize`, `serde::Serialize`, and `schemars::JsonSchema`.
+
+### `register_trigger`
+
+Bind a registered function to a configured trigger instance.
+
+```rust
+pub fn register_trigger(
+ &self,
+ input: RegisterTriggerInput,
+) -> Result;
+```
+
+Drop the trigger with `trigger.unregister()` on the returned handle. There is no free-function
+`unregister_trigger`.
+
+### `register_trigger_type`
+
+Declare a new trigger type that this worker advertises.
+
+```rust
+pub fn register_trigger_type(
+ &self,
+ registration: RegisterTriggerType,
+) -> TriggerTypeRef
+where
+ H: TriggerHandler + 'static;
+```
+
+`C` and `R` are the trigger config and result types, each `schemars::JsonSchema`.
+
+### `unregister_trigger_type`
+
+Remove a previously registered trigger type.
+
+```rust
+pub fn unregister_trigger_type(&self, id: impl Into);
+```
+
+### `trigger`
+
+Invoke a registered function. Always async; await the future to receive the result.
+
+```rust
+pub async fn trigger(
+ &self,
+ request: impl Into,
+) -> Result;
+```
+
+The returned `Value` is the function's return JSON for synchronous calls, an `EnqueueResult`-shaped
+JSON for `TriggerAction::Enqueue` actions, and `Null` for `TriggerAction::Void`.
+
+### `shutdown`
+
+Disconnect from the engine and release resources. Returns immediately; in-flight tasks are aborted.
+
+```rust
+pub fn shutdown(&self);
+```
+
+## Trigger actions
+
+`TriggerAction` is a plain enum.
+
+```rust
+pub enum TriggerAction {
+ Void,
+ Enqueue { queue: String },
+}
+```
+
+Pass it in `TriggerRequest::action` as `Some(TriggerAction::Void)` or
+`Some(TriggerAction::Enqueue { queue: "math".to_string() })`. `None` means synchronous.
+
+## Error type
+
+`IIIError` is the public error enum. Most invocation failures arrive on the `Remote` or `Runtime`
+variants.
+
+```rust
+pub enum IIIError {
+ NotConnected,
+ Timeout,
+ Runtime(String),
+ Remote(ErrorBody),
+ Handler(String),
+ Serde(serde_json::Error),
+ WebSocket(String),
+}
+```
+
+`ErrorBody` carries the engine's `{ code, message, stacktrace? }` payload; match on
+`IIIError::Remote(body) => body.code.as_str()` to branch on engine error codes
+(`invocation_failed`, `invocation_stopped`, `function_not_found`, `FORBIDDEN`, `TIMEOUT`, etc.).
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` wrap the engine's stream WebSockets. `StreamChannelRef`
+identifies a channel:
+
+```rust
+pub struct StreamChannelRef {
+ pub channel_id: String,
+ pub access_key: String,
+ pub direction: ChannelDirection,
+}
+```
+
+`ChannelReader::new(engine_ws_base, ref)` and `ChannelWriter::new(engine_ws_base, ref)` open the
+underlying WebSocket. Reader methods include `read()`, `on_message()`, and `close()`; writer
+methods include `write()`, `send_message()`, and `close()`.
+
+## Logger
+
+`Logger` is a `Clone + Default` struct that emits structured log records. The output integrates
+with the SDK's OpenTelemetry setup; see
+iii-observability for the export side.
+
+## Connection state
+
+`IIIConnectionState` is the public enum mirroring the wire-level connection lifecycle.
+
+```rust
+pub enum IIIConnectionState {
+ Disconnected,
+ Connecting,
+ Connected,
+ Reconnecting,
+ Failed,
+}
+```
+
+## Info types
+
+The SDK re-exports the engine's structured introspection types:
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+- `WorkerInfo`. `id`, `name`, runtime / version / OS fields, IP, `status`, `connected_at_ms`,
+ `function_count`, registered `functions`, `active_invocations`, optional `isolation`.
+- `WorkerMetadata`. The structured metadata a worker reports about itself: `runtime`, `version`,
+ `name`, `os`, `pid`, `telemetry` (an optional `WorkerTelemetryMeta` carrying language /
+ framework / project labels plus an Amplitude key, used by iii-telemetry for anonymous usage
+ reporting; distinct from the OpenTelemetry observability surfaces owned by iii-observability),
+ `isolation`. Rust is the only SDK that surfaces this as a distinct type today.
+
+## `MessageType`
+
+Not part of this SDK. Wire frames are typed via the protocol module's `Message` enum, which is
+internal to the SDK.
diff --git a/docs/0-13-0/sdk-reference/rust-sdk.mdx.skill.md b/docs/0-13-0/sdk-reference/rust-sdk.mdx.skill.md
new file mode 100644
index 000000000..47845c8f5
--- /dev/null
+++ b/docs/0-13-0/sdk-reference/rust-sdk.mdx.skill.md
@@ -0,0 +1,200 @@
+
+
+# Rust SDK
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+
+ This page is a hand-authored snapshot of the planned public surface. The final reference will be
+ generated from the SDK source.
+
+
+## Installation
+
+```bash
+cargo add iii-sdk
+```
+
+Imported as `iii_sdk`.
+
+## Common methods
+
+### `register_worker`
+
+Connect a worker to a running iii engine and return its handle.
+
+```rust
+pub fn register_worker(address: &str, options: InitOptions) -> III;
+```
+
+The returned `III` carries every method below. Spawned async tasks are driven on the SDK's
+internal tokio runtime.
+
+### `register_function`
+
+Register a callable function on this worker. Request and response schemas are derived from the
+handler's input and output types via the `schemars::JsonSchema` derive; the call site doesn't
+restate them.
+
+```rust
+pub fn register_function(
+ &self,
+ registration: R,
+) -> FunctionRef;
+```
+
+Build the `RegisterFunction` value via `RegisterFunction::new("namespace::name", handler)` and pass
+it to `register_function`. The handler's parameter and return types must implement
+`serde::Deserialize`, `serde::Serialize`, and `schemars::JsonSchema`.
+
+### `register_trigger`
+
+Bind a registered function to a configured trigger instance.
+
+```rust
+pub fn register_trigger(
+ &self,
+ input: RegisterTriggerInput,
+) -> Result;
+```
+
+Drop the trigger with `trigger.unregister()` on the returned handle. There is no free-function
+`unregister_trigger`.
+
+### `register_trigger_type`
+
+Declare a new trigger type that this worker advertises.
+
+```rust
+pub fn register_trigger_type(
+ &self,
+ registration: RegisterTriggerType,
+) -> TriggerTypeRef
+where
+ H: TriggerHandler + 'static;
+```
+
+`C` and `R` are the trigger config and result types, each `schemars::JsonSchema`.
+
+### `unregister_trigger_type`
+
+Remove a previously registered trigger type.
+
+```rust
+pub fn unregister_trigger_type(&self, id: impl Into);
+```
+
+### `trigger`
+
+Invoke a registered function. Always async; await the future to receive the result.
+
+```rust
+pub async fn trigger(
+ &self,
+ request: impl Into,
+) -> Result;
+```
+
+The returned `Value` is the function's return JSON for synchronous calls, an `EnqueueResult`-shaped
+JSON for `TriggerAction::Enqueue` actions, and `Null` for `TriggerAction::Void`.
+
+### `shutdown`
+
+Disconnect from the engine and release resources. Returns immediately; in-flight tasks are aborted.
+
+```rust
+pub fn shutdown(&self);
+```
+
+## Trigger actions
+
+`TriggerAction` is a plain enum.
+
+```rust
+pub enum TriggerAction {
+ Void,
+ Enqueue { queue: String },
+}
+```
+
+Pass it in `TriggerRequest::action` as `Some(TriggerAction::Void)` or
+`Some(TriggerAction::Enqueue { queue: "math".to_string() })`. `None` means synchronous.
+
+## Error type
+
+`IIIError` is the public error enum. Most invocation failures arrive on the `Remote` or `Runtime`
+variants.
+
+```rust
+pub enum IIIError {
+ NotConnected,
+ Timeout,
+ Runtime(String),
+ Remote(ErrorBody),
+ Handler(String),
+ Serde(serde_json::Error),
+ WebSocket(String),
+}
+```
+
+`ErrorBody` carries the engine's `{ code, message, stacktrace? }` payload; match on
+`IIIError::Remote(body) => body.code.as_str()` to branch on engine error codes
+(`invocation_failed`, `invocation_stopped`, `function_not_found`, `FORBIDDEN`, `TIMEOUT`, etc.).
+
+## Channels
+
+`ChannelReader` and `ChannelWriter` wrap the engine's stream WebSockets. `StreamChannelRef`
+identifies a channel:
+
+```rust
+pub struct StreamChannelRef {
+ pub channel_id: String,
+ pub access_key: String,
+ pub direction: ChannelDirection,
+}
+```
+
+`ChannelReader::new(engine_ws_base, ref)` and `ChannelWriter::new(engine_ws_base, ref)` open the
+underlying WebSocket. Reader methods include `read()`, `on_message()`, and `close()`; writer
+methods include `write()`, `send_message()`, and `close()`.
+
+## Logger
+
+`Logger` is a `Clone + Default` struct that emits structured log records. The output integrates
+with the SDK's OpenTelemetry setup; see
+iii-observability for the export side.
+
+## Connection state
+
+`IIIConnectionState` is the public enum mirroring the wire-level connection lifecycle.
+
+```rust
+pub enum IIIConnectionState {
+ Disconnected,
+ Connecting,
+ Connected,
+ Reconnecting,
+ Failed,
+}
+```
+
+## Info types
+
+The SDK re-exports the engine's structured introspection types:
+
+- `FunctionInfo`. `function_id`, optional `description`, optional `request_format` /
+ `response_format`, optional `metadata`.
+- `TriggerInfo`. `id`, `trigger_type`, `function_id`, optional `config` / `metadata`.
+- `WorkerInfo`. `id`, `name`, runtime / version / OS fields, IP, `status`, `connected_at_ms`,
+ `function_count`, registered `functions`, `active_invocations`, optional `isolation`.
+- `WorkerMetadata`. The structured metadata a worker reports about itself: `runtime`, `version`,
+ `name`, `os`, `pid`, `telemetry` (an optional `WorkerTelemetryMeta` carrying language /
+ framework / project labels plus an Amplitude key, used by iii-telemetry for anonymous usage
+ reporting; distinct from the OpenTelemetry observability surfaces owned by iii-observability),
+ `isolation`. Rust is the only SDK that surfaces this as a distinct type today.
+
+## `MessageType`
+
+Not part of this SDK. Wire frames are typed via the protocol module's `Message` enum, which is
+internal to the SDK.
diff --git a/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx b/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx
new file mode 100644
index 000000000..bf26665ab
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx
@@ -0,0 +1,6 @@
+---
+title: "Define tools"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx.skill.md b/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx.skill.md
new file mode 100644
index 000000000..8c7b46dbb
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/define-tools.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Define tools
diff --git a/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx b/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx
new file mode 100644
index 000000000..114c81fc5
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx
@@ -0,0 +1,6 @@
+---
+title: "Memory and state"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx.skill.md b/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx.skill.md
new file mode 100644
index 000000000..5c39936cc
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/memory-and-state.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Memory and state
diff --git a/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx b/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx
new file mode 100644
index 000000000..5c6a949aa
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx
@@ -0,0 +1,6 @@
+---
+title: "Orchestration loop"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx.skill.md b/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx.skill.md
new file mode 100644
index 000000000..e8e9e8059
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/orchestration-loop.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Orchestration loop
diff --git a/docs/0-13-0/tutorials/build-an-agent/overview.mdx b/docs/0-13-0/tutorials/build-an-agent/overview.mdx
new file mode 100644
index 000000000..6791b5dbb
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/overview.mdx
@@ -0,0 +1,22 @@
+---
+title: "Overview"
+description: "Build an agent in iii: tools, the orchestration loop, and persistent memory."
+owner: "devrel"
+type: "tutorial"
+---
+
+This tutorial walks through building an agent in iii in three pieces: the functions the agent calls
+as tools, the loop that drives its think-act-observe cycle, and the memory it carries between turns.
+By the end you'll have a working agent worker you can extend with your own tools.
+
+## Define tools
+
+Register the functions the agent can call as tools.
+
+## Orchestration loop
+
+Drive the agent's think-act-observe cycle until the task is done.
+
+## Memory and state
+
+Persist conversation history and intermediate state between agent turns.
diff --git a/docs/0-13-0/tutorials/build-an-agent/overview.mdx.skill.md b/docs/0-13-0/tutorials/build-an-agent/overview.mdx.skill.md
new file mode 100644
index 000000000..f295a11f6
--- /dev/null
+++ b/docs/0-13-0/tutorials/build-an-agent/overview.mdx.skill.md
@@ -0,0 +1,20 @@
+
+
+# Overview
+
+
+This tutorial walks through building an agent in iii in three pieces: the functions the agent calls
+as tools, the loop that drives its think-act-observe cycle, and the memory it carries between turns.
+By the end you'll have a working agent worker you can extend with your own tools.
+
+## Define tools
+
+Register the functions the agent can call as tools.
+
+## Orchestration loop
+
+Drive the agent's think-act-observe cycle until the task is done.
+
+## Memory and state
+
+Persist conversation history and intermediate state between agent turns.
diff --git a/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx b/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx
new file mode 100644
index 000000000..310a80144
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx
@@ -0,0 +1,6 @@
+---
+title: "Migrate persistence"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx.skill.md b/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx.skill.md
new file mode 100644
index 000000000..3614df411
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/migrate-persistence.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Migrate persistence
diff --git a/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx b/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx
new file mode 100644
index 000000000..1b932f7cf
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx
@@ -0,0 +1,6 @@
+---
+title: "Offload work to a queue"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx.skill.md b/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx.skill.md
new file mode 100644
index 000000000..71f3b1e64
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/offload-to-queue.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Offload work to a queue
diff --git a/docs/0-13-0/tutorials/incremental-adoption/overview.mdx b/docs/0-13-0/tutorials/incremental-adoption/overview.mdx
new file mode 100644
index 000000000..7f3f96acd
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/overview.mdx
@@ -0,0 +1,38 @@
+---
+title: "Overview"
+description: "Bring iii into an existing system incrementally, one slice at a time."
+owner: "devrel"
+type: "tutorial"
+---
+
+## Introduction
+
+In this tutorial, you will learn how to bring iii into an existing system incrementally, without a
+big-bang rewrite. You'll wrap an existing HTTP service as an iii function so other workers can
+address it, move slow work behind an iii queue so callers return immediately and retries are
+handled by the queue, and migrate persistence into iii's state primitives. By the end your system
+runs on iii without ever needing a full cutover.
+
+## Prerequisites
+
+- A running iii engine ([install](/install), then `iii --use-default-config` for a scratch
+ instance; see [Default configuration](/using-iii/engine#default-configuration)).
+- An existing service you want to migrate, reachable over HTTP.
+- The SDK for the language your service is written in (TypeScript, Python, or Rust).
+
+## Steps
+
+1. **[Wrap an existing API](./wrap-existing-api)**. Front the existing HTTP service with an iii
+ function so it becomes addressable by `function_id` from anywhere in the system. No traffic
+ moves yet; you're just adding an iii-shaped entry point.
+2. **[Offload work to a queue](./offload-to-queue)**. Move long-running calls behind a queue
+ worker so the wrapper returns immediately and retries are handled for you.
+3. **[Migrate persistence](./migrate-persistence)**. Migrate one slice of state at a time into a
+ state worker, leaving the rest of your system untouched until you're ready to move the next
+ slice.
+
+## Conclusion
+
+Working through the three sub-tutorials in order moves the system onto iii a slice at a time. Each
+slice is independently reversible, so you can pause or roll back at any boundary without taking the
+rest of the system down.
diff --git a/docs/0-13-0/tutorials/incremental-adoption/overview.mdx.skill.md b/docs/0-13-0/tutorials/incremental-adoption/overview.mdx.skill.md
new file mode 100644
index 000000000..c01c80953
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/overview.mdx.skill.md
@@ -0,0 +1,36 @@
+
+
+# Overview
+
+
+## Introduction
+
+In this tutorial, you will learn how to bring iii into an existing system incrementally, without a
+big-bang rewrite. You'll wrap an existing HTTP service as an iii function so other workers can
+address it, move slow work behind an iii queue so callers return immediately and retries are
+handled by the queue, and migrate persistence into iii's state primitives. By the end your system
+runs on iii without ever needing a full cutover.
+
+## Prerequisites
+
+- A running iii engine ([install](/install), then `iii --use-default-config` for a scratch
+ instance; see [Default configuration](/using-iii/engine#default-configuration)).
+- An existing service you want to migrate, reachable over HTTP.
+- The SDK for the language your service is written in (TypeScript, Python, or Rust).
+
+## Steps
+
+1. **[Wrap an existing API](./wrap-existing-api)**. Front the existing HTTP service with an iii
+ function so it becomes addressable by `function_id` from anywhere in the system. No traffic
+ moves yet; you're just adding an iii-shaped entry point.
+2. **[Offload work to a queue](./offload-to-queue)**. Move long-running calls behind a queue
+ worker so the wrapper returns immediately and retries are handled for you.
+3. **[Migrate persistence](./migrate-persistence)**. Migrate one slice of state at a time into a
+ state worker, leaving the rest of your system untouched until you're ready to move the next
+ slice.
+
+## Conclusion
+
+Working through the three sub-tutorials in order moves the system onto iii a slice at a time. Each
+slice is independently reversible, so you can pause or roll back at any boundary without taking the
+rest of the system down.
diff --git a/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx b/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx
new file mode 100644
index 000000000..b5f162b60
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx
@@ -0,0 +1,6 @@
+---
+title: "Wrap an existing API"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx.skill.md b/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx.skill.md
new file mode 100644
index 000000000..c235f1724
--- /dev/null
+++ b/docs/0-13-0/tutorials/incremental-adoption/wrap-existing-api.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Wrap an existing API
diff --git a/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx b/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx
new file mode 100644
index 000000000..a2540eff7
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx
@@ -0,0 +1,6 @@
+---
+title: "CRUD endpoints"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx.skill.md b/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx.skill.md
new file mode 100644
index 000000000..955b56176
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/crud-endpoints.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# CRUD endpoints
diff --git a/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx b/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx
new file mode 100644
index 000000000..f07983f9d
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx
@@ -0,0 +1,6 @@
+---
+title: "Model and store"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx.skill.md b/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx.skill.md
new file mode 100644
index 000000000..c1b9bbd2c
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/model-and-store.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Model and store
diff --git a/docs/0-13-0/tutorials/reactive-crud/overview.mdx b/docs/0-13-0/tutorials/reactive-crud/overview.mdx
new file mode 100644
index 000000000..2b3ae6452
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/overview.mdx
@@ -0,0 +1,23 @@
+---
+title: "Overview"
+description: "Build a CRUD service on iii that streams live updates to clients."
+owner: "devrel"
+type: "tutorial"
+---
+
+This tutorial builds a real-time CRUD service with iii. You'll model your data on the iii state
+store, expose create/read/update/delete operations over HTTP triggers, and stream live changes to
+connected clients. By the end you'll have a service that streams state changes to clients in real
+time without writing pub/sub code.
+
+## Model and store
+
+Define the data model and back it with an iii state store.
+
+## CRUD endpoints
+
+Expose create, read, update, and delete functions over HTTP triggers.
+
+## Realtime subscriptions
+
+Stream live changes from the store to connected clients.
diff --git a/docs/0-13-0/tutorials/reactive-crud/overview.mdx.skill.md b/docs/0-13-0/tutorials/reactive-crud/overview.mdx.skill.md
new file mode 100644
index 000000000..95fe37f2b
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/overview.mdx.skill.md
@@ -0,0 +1,21 @@
+
+
+# Overview
+
+
+This tutorial builds a real-time CRUD service with iii. You'll model your data on the iii state
+store, expose create/read/update/delete operations over HTTP triggers, and stream live changes to
+connected clients. By the end you'll have a service that streams state changes to clients in real
+time without writing pub/sub code.
+
+## Model and store
+
+Define the data model and back it with an iii state store.
+
+## CRUD endpoints
+
+Expose create, read, update, and delete functions over HTTP triggers.
+
+## Realtime subscriptions
+
+Stream live changes from the store to connected clients.
diff --git a/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx b/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx
new file mode 100644
index 000000000..9d222933d
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx
@@ -0,0 +1,6 @@
+---
+title: "Realtime subscriptions"
+description: "Placeholder."
+owner: "devrel"
+type: "tutorial"
+---
diff --git a/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx.skill.md b/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx.skill.md
new file mode 100644
index 000000000..a3d451000
--- /dev/null
+++ b/docs/0-13-0/tutorials/reactive-crud/realtime-subscriptions.mdx.skill.md
@@ -0,0 +1,3 @@
+
+
+# Realtime subscriptions
diff --git a/docs/0-13-0/understanding-iii/channels.mdx b/docs/0-13-0/understanding-iii/channels.mdx
new file mode 100644
index 000000000..9050d7b59
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/channels.mdx
@@ -0,0 +1,92 @@
+---
+title: "Channels"
+description: "How iii streams data between workers without JSON payload limits."
+owner: "devrel"
+type: "explanation"
+---
+
+## What channels are
+
+Channels are stream pipes between iii workers. They let one function write bytes while another
+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. |
+
+The key idea is that refs travel through regular function calls, but the data itself travels over
+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.
+
+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.
+
+## Runtime flow
+
+```mermaid
+sequenceDiagram
+ participant A as Producer function
+ participant Engine
+ participant B as Consumer function
+
+ A->>Engine: createChannel()
+ Engine-->>A: writer, reader, writerRef, readerRef
+ A->>Engine: trigger B with readerRef
+ Engine->>B: invoke B with reader ref
+ A->>Engine: writer sends chunks
+ Engine->>B: reader receives chunks
+ A->>Engine: writer closes
+ Engine->>B: reader ends
+```
+
+The producer creates a channel and passes `readerRef` to the consumer. Node and Python materialize
+that ref into a live reader before the handler runs. Rust receives the ref as JSON and constructs a
+`ChannelReader` from it. The producer writes chunks into its local writer, and the consumer reads
+those chunks from its local reader.
+
+## Backpressure and lifecycle
+
+Channel streams connect lazily. Creating a channel allocates refs, but the WebSocket stream connects
+when one side starts reading or writing. Backpressure is handled by the SDK stream implementation so
+writers can pause when readers cannot keep up.
+
+When the writer closes, the reader receives the stream end. When a worker disconnects, its channel
+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/0-13-0/understanding-iii/channels.mdx.skill.md b/docs/0-13-0/understanding-iii/channels.mdx.skill.md
new file mode 100644
index 000000000..620623fdf
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/channels.mdx.skill.md
@@ -0,0 +1,90 @@
+
+
+# Channels
+
+
+## What channels are
+
+Channels are stream pipes between iii workers. They let one function write bytes while another
+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. |
+
+The key idea is that refs travel through regular function calls, but the data itself travels over
+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.
+
+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.
+
+## Runtime flow
+
+```mermaid
+sequenceDiagram
+ participant A as Producer function
+ participant Engine
+ participant B as Consumer function
+
+ A->>Engine: createChannel()
+ Engine-->>A: writer, reader, writerRef, readerRef
+ A->>Engine: trigger B with readerRef
+ Engine->>B: invoke B with reader ref
+ A->>Engine: writer sends chunks
+ Engine->>B: reader receives chunks
+ A->>Engine: writer closes
+ Engine->>B: reader ends
+```
+
+The producer creates a channel and passes `readerRef` to the consumer. Node and Python materialize
+that ref into a live reader before the handler runs. Rust receives the ref as JSON and constructs a
+`ChannelReader` from it. The producer writes chunks into its local writer, and the consumer reads
+those chunks from its local reader.
+
+## Backpressure and lifecycle
+
+Channel streams connect lazily. Creating a channel allocates refs, but the WebSocket stream connects
+when one side starts reading or writing. Backpressure is handled by the SDK stream implementation so
+writers can pause when readers cannot keep up.
+
+When the writer closes, the reader receives the stream end. When a worker disconnects, its channel
+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/0-13-0/understanding-iii/engine.mdx b/docs/0-13-0/understanding-iii/engine.mdx
new file mode 100644
index 000000000..7c6817c94
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/engine.mdx
@@ -0,0 +1,69 @@
+---
+title: "Engine"
+description:
+ "The iii engine is the thin layer that allows the benefits of Workers, Triggers, and Functions to
+ happen."
+owner: "devrel"
+type: "explanation"
+---
+
+## Startup flow
+
+When the Engine starts it does a few things:
+
+1. It parses its command-line arguments.
+1. Loads its configuration file (typically `config.yaml`).
+1. Applies the worker declarations from the config (starting each declared worker process).
+1. Begins serving connections.
+
+After this sequence the Engine is ready to accept WebSocket connections from Workers and route
+invocations between them.
+
+## Engine responsibilities
+
+The Engine's responsibilities cover three concerns at runtime. First, it accepts WebSocket
+connections from Workers and maintains the live registry of which Workers are currently connected.
+Second, it tracks the Functions and Triggers each connected Worker has registered, exposing them as
+a unified system-wide surface. Third, it routes invocations: when a Trigger fires or a Function is
+called, the Engine finds a Worker that provides the target Function and dispatches the call.
+
+## Worker disconnect cleanup
+
+When a Worker disconnects, the Engine cleans up the Worker's footprint in the live registry: its
+Functions and Triggers are removed, in-flight invocations of those Functions are cancelled, and the
+rest of the system keeps serving.
+
+
+ See [Creating Workers / Workers / Handling Worker
+ disconnects](/creating-workers/workers#handling-worker-disconnects) for the cancellation error code
+ and the discovery events that fire (with their consistency semantics).
+
+
+## Config hot-reload
+
+`config.yaml` is watched at runtime. When the file changes, the Engine parses, diffs, validates, and
+commits the new config. Workers that did not change in the diff stay running through the reload, so
+only added, removed, or changed Workers are restarted. An invalid config (parse error or validation
+failure) causes the Engine to exit rather than enter an indeterminate state.
+
+## Architecture-agnostic routing
+
+Routing is independent of language, runtime, and location. The Engine applies the same routing path
+whether a Function is hosted by a Python Agent on a laptop, a TypeScript Worker in a browser tab, a
+Rust binary in a microVM, or an OCI image on Kubernetes. This is what
+makes "any language, any runtime" a concrete property of iii rather than an aspiration.
+
+## Discovery and the live registry
+
+The Engine maintains a registry of every connected Worker, the Functions each Worker has registered,
+and the Triggers bound to those Functions. Other Workers and tooling can read the registry on demand
+or subscribe to changes as it evolves.
+
+
+ See [Creating Workers / Workers / Inspecting the live
+ registry](/creating-workers/workers#inspecting-the-live-registry) for the concrete `engine::*::list`
+ snapshot calls and the `engine::workers-available` / `engine::functions-available` subscription
+ events.
+
+
+Querying traces, logs, and metrics is documented with the iii-observability Worker.
diff --git a/docs/0-13-0/understanding-iii/engine.mdx.skill.md b/docs/0-13-0/understanding-iii/engine.mdx.skill.md
new file mode 100644
index 000000000..d7e03dcac
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/engine.mdx.skill.md
@@ -0,0 +1,65 @@
+
+
+# Engine
+
+
+## Startup flow
+
+When the Engine starts it does a few things:
+
+1. It parses its command-line arguments.
+1. Loads its configuration file (typically `config.yaml`).
+1. Applies the worker declarations from the config (starting each declared worker process).
+1. Begins serving connections.
+
+After this sequence the Engine is ready to accept WebSocket connections from Workers and route
+invocations between them.
+
+## Engine responsibilities
+
+The Engine's responsibilities cover three concerns at runtime. First, it accepts WebSocket
+connections from Workers and maintains the live registry of which Workers are currently connected.
+Second, it tracks the Functions and Triggers each connected Worker has registered, exposing them as
+a unified system-wide surface. Third, it routes invocations: when a Trigger fires or a Function is
+called, the Engine finds a Worker that provides the target Function and dispatches the call.
+
+## Worker disconnect cleanup
+
+When a Worker disconnects, the Engine cleans up the Worker's footprint in the live registry: its
+Functions and Triggers are removed, in-flight invocations of those Functions are cancelled, and the
+rest of the system keeps serving.
+
+
+ See [Creating Workers / Workers / Handling Worker
+ disconnects](/creating-workers/workers#handling-worker-disconnects) for the cancellation error code
+ and the discovery events that fire (with their consistency semantics).
+
+
+## Config hot-reload
+
+`config.yaml` is watched at runtime. When the file changes, the Engine parses, diffs, validates, and
+commits the new config. Workers that did not change in the diff stay running through the reload, so
+only added, removed, or changed Workers are restarted. An invalid config (parse error or validation
+failure) causes the Engine to exit rather than enter an indeterminate state.
+
+## Architecture-agnostic routing
+
+Routing is independent of language, runtime, and location. The Engine applies the same routing path
+whether a Function is hosted by a Python Agent on a laptop, a TypeScript Worker in a browser tab, a
+Rust binary in a microVM, or an OCI image on Kubernetes. This is what
+makes "any language, any runtime" a concrete property of iii rather than an aspiration.
+
+## Discovery and the live registry
+
+The Engine maintains a registry of every connected Worker, the Functions each Worker has registered,
+and the Triggers bound to those Functions. Other Workers and tooling can read the registry on demand
+or subscribe to changes as it evolves.
+
+
+ See [Creating Workers / Workers / Inspecting the live
+ registry](/creating-workers/workers#inspecting-the-live-registry) for the concrete `engine::*::list`
+ snapshot calls and the `engine::workers-available` / `engine::functions-available` subscription
+ events.
+
+
+Querying traces, logs, and metrics is documented with the iii-observability Worker.
diff --git a/docs/0-13-0/understanding-iii/functions.mdx b/docs/0-13-0/understanding-iii/functions.mdx
new file mode 100644
index 000000000..e56060c88
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/functions.mdx
@@ -0,0 +1,46 @@
+---
+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/0-13-0/understanding-iii/functions.mdx.skill.md b/docs/0-13-0/understanding-iii/functions.mdx.skill.md
new file mode 100644
index 000000000..e7111ce1c
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/functions.mdx.skill.md
@@ -0,0 +1,44 @@
+
+
+# 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/0-13-0/understanding-iii/index.mdx b/docs/0-13-0/understanding-iii/index.mdx
new file mode 100644
index 000000000..452b42103
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/index.mdx
@@ -0,0 +1,167 @@
+---
+title: "Overview"
+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."
+owner: "devrel"
+type: "explanation"
+---
+
+Unix gave processes a single interface. React gave components a single interface. iii gives every
+category of software (queues, schedulers, agents, frontends, sandboxes, business logic, etc.) a
+single interface: **Workers** host work, **Functions** are the work, **Triggers** are what causes
+the work to run, and the **Engine** routes between them. Once you have a mental model for those four
+pieces, everything else in iii is a variation on a theme.
+
+This page uses the [Quickstart tutorial](/quickstart) as an example.
+
+## 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.
+
+### Worker
+
+A Worker is anything that connects to the Engine and registers Triggers and Functions with it.
+Workers can run anywhere (on a laptop, in a container, in a browser tab, on a microVM) and in any
+language as long as they can open a WebSocket to the Engine.
+
+### Trigger
+
+A Trigger is what causes a Function to run. A Trigger has a type (HTTP, cron, queue message, state
+change, another Function calling `trigger`), a configuration (which path, which schedule, which
+queue), and the function ID it invokes.
+
+### Function
+
+A Function is a named handler inside a Worker. It takes a payload and returns a result. Function
+identifiers follow a `service::name` convention so they remain stable across worker restarts and
+language boundaries.
+
+### Engine
+
+The Engine is the coordinator. It accepts worker connections, maintains a live registry of available
+Functions and Triggers, and routes invocations to whichever Worker currently provides the requested
+Function.
+
+## The Quickstart
+
+The Quickstart tutorial produces a running system with two Workers connected to the same Engine:
+
+1. `math-worker` is a Python Worker that registers `math::add`.
+1. `caller-worker` is a TypeScript Worker that registers `math::add_two_numbers`, which calls
+ `math::add` through the Engine.
+
+By the end of the Quickstart, the system also includes the `iii-state` and `iii-http` Workers, an
+HTTP Trigger that exposes `math::add_two_numbers` at `POST /math/add-two-numbers`, and a key-value
+scope named `math` holding a `running_total`.
+
+The runtime topology looks like this:
+
+```mermaid
+graph TD
+ CLI["iii trigger (CLI)"] <-->|WS| Engine["iii engine :49134"]
+ HTTP["curl / HTTP"] <-->|HTTP :3111| HttpWorker
+
+ Engine <--> Math["math-worker (Python) math::add"]
+ Engine <--> Caller["caller-worker (TypeScript) math::add_two_numbers"]
+ Engine <--> State["iii-state"]
+ Engine <--> HttpWorker["iii-http"]
+```
+
+Every arrow is a WebSocket connection between a Worker and the Engine. There is no direct
+worker-to-worker traffic. When `caller-worker` invokes `math::add`, the call goes through the
+Engine, which looks up the current location of `math::add` in its registry and routes the invocation
+to `math-worker`.
+
+## Workers
+
+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
+them.
+
+The Python and TypeScript Workers are independent processes in different languages, with different
+runtimes, possibly on different machines. Neither one knows the execution context of the other. They
+both talk to the Engine, and the Engine handles the rest.
+
+This is what "any language, any runtime" means in practice: the worker contract is small enough to
+implement in any language that can use a WebSocket and JSON, and the Engine treats every Worker the
+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.
+
+
+## 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:
+
+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`.
+1. The SDK call `worker.trigger({ function_id: 'math::add', ... })` is another version of the same
+ idea: one Function inside one Worker firing a Trigger that invokes another Function, routed
+ through the Engine just like the CLI version.
+
+ Both paths work against any registered Function without registering an explicit Trigger; every
+ `registerFunction()` inherently gets a Trigger that can be invoked with these two methods.
+
+1. The HTTP Trigger added by the `iii-http` Worker is done through `worker.registerTrigger()` and is
+ the common reactive way to implement Triggers.
+
+ In this example `iii-http` owns the HTTP socket; when a request arrives at
+ `POST /math/add-two-numbers` the following happens:
+ 1. `iii-http` looks up the matching Trigger and fires a request targeting the `math::add`
+ Function.
+ 1. The Engine receives the request and routes the invocation to `caller-worker`.
+ 1. Finally the response flows back the same way. The `math::add` Function never sees an HTTP
+ request. It sees a payload, like every other call.
+
+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
+
+`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
+is no enforcement of them within iii.
+
+Function IDs are stable across worker restarts. When `math-worker` stops and restarts, callers do
+not need to know: they keep invoking `math::add`, and the Engine routes the calls to whichever
+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
+registered Function and Trigger. When a Worker connects, the Engine records what Functions it
+provides. When a Worker disconnects, the Engine removes its Functions, cancels any in-flight
+invocations of those Functions, and notifies the rest of the system that the topology changed.
+
+Routing is independent of language, runtime, and location. The Engine does not need to know where
+`math::add` is running in Docker, on a Raspberry Pi, or in a browser tab. It just knows that _some_
+Worker provides it. The same tutorial can be redeployed across different runtimes without touching
+the function code.
+
+
+ See [Engine](/understanding-iii/engine) for startup flow, config hot-reload, and the live registry
+ and discovery surface.
+
diff --git a/docs/0-13-0/understanding-iii/index.mdx.skill.md b/docs/0-13-0/understanding-iii/index.mdx.skill.md
new file mode 100644
index 000000000..af34ae3c6
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/index.mdx.skill.md
@@ -0,0 +1,163 @@
+
+
+# Overview
+
+
+Unix gave processes a single interface. React gave components a single interface. iii gives every
+category of software (queues, schedulers, agents, frontends, sandboxes, business logic, etc.) a
+single interface: **Workers** host work, **Functions** are the work, **Triggers** are what causes
+the work to run, and the **Engine** routes between them. Once you have a mental model for those four
+pieces, everything else in iii is a variation on a theme.
+
+This page uses the [Quickstart tutorial](/quickstart) as an example.
+
+## 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.
+
+### Worker
+
+A Worker is anything that connects to the Engine and registers Triggers and Functions with it.
+Workers can run anywhere (on a laptop, in a container, in a browser tab, on a microVM) and in any
+language as long as they can open a WebSocket to the Engine.
+
+### Trigger
+
+A Trigger is what causes a Function to run. A Trigger has a type (HTTP, cron, queue message, state
+change, another Function calling `trigger`), a configuration (which path, which schedule, which
+queue), and the function ID it invokes.
+
+### Function
+
+A Function is a named handler inside a Worker. It takes a payload and returns a result. Function
+identifiers follow a `service::name` convention so they remain stable across worker restarts and
+language boundaries.
+
+### Engine
+
+The Engine is the coordinator. It accepts worker connections, maintains a live registry of available
+Functions and Triggers, and routes invocations to whichever Worker currently provides the requested
+Function.
+
+## The Quickstart
+
+The Quickstart tutorial produces a running system with two Workers connected to the same Engine:
+
+1. `math-worker` is a Python Worker that registers `math::add`.
+1. `caller-worker` is a TypeScript Worker that registers `math::add_two_numbers`, which calls
+ `math::add` through the Engine.
+
+By the end of the Quickstart, the system also includes the `iii-state` and `iii-http` Workers, an
+HTTP Trigger that exposes `math::add_two_numbers` at `POST /math/add-two-numbers`, and a key-value
+scope named `math` holding a `running_total`.
+
+The runtime topology looks like this:
+
+```mermaid
+graph TD
+ CLI["iii trigger (CLI)"] <-->|WS| Engine["iii engine :49134"]
+ HTTP["curl / HTTP"] <-->|HTTP :3111| HttpWorker
+
+ Engine <--> Math["math-worker (Python) math::add"]
+ Engine <--> Caller["caller-worker (TypeScript) math::add_two_numbers"]
+ Engine <--> State["iii-state"]
+ Engine <--> HttpWorker["iii-http"]
+```
+
+Every arrow is a WebSocket connection between a Worker and the Engine. There is no direct
+worker-to-worker traffic. When `caller-worker` invokes `math::add`, the call goes through the
+Engine, which looks up the current location of `math::add` in its registry and routes the invocation
+to `math-worker`.
+
+## Workers
+
+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
+them.
+
+The Python and TypeScript Workers are independent processes in different languages, with different
+runtimes, possibly on different machines. Neither one knows the execution context of the other. They
+both talk to the Engine, and the Engine handles the rest.
+
+This is what "any language, any runtime" means in practice: the worker contract is small enough to
+implement in any language that can use a WebSocket and JSON, and the Engine treats every Worker the
+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.
+
+
+## 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:
+
+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`.
+1. The SDK call `worker.trigger({ function_id: 'math::add', ... })` is another version of the same
+ idea: one Function inside one Worker firing a Trigger that invokes another Function, routed
+ through the Engine just like the CLI version.
+
+ Both paths work against any registered Function without registering an explicit Trigger; every
+ `registerFunction()` inherently gets a Trigger that can be invoked with these two methods.
+
+1. The HTTP Trigger added by the `iii-http` Worker is done through `worker.registerTrigger()` and is
+ the common reactive way to implement Triggers.
+
+ In this example `iii-http` owns the HTTP socket; when a request arrives at
+ `POST /math/add-two-numbers` the following happens:
+ 1. `iii-http` looks up the matching Trigger and fires a request targeting the `math::add`
+ Function.
+ 1. The Engine receives the request and routes the invocation to `caller-worker`.
+ 1. Finally the response flows back the same way. The `math::add` Function never sees an HTTP
+ request. It sees a payload, like every other call.
+
+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
+
+`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
+is no enforcement of them within iii.
+
+Function IDs are stable across worker restarts. When `math-worker` stops and restarts, callers do
+not need to know: they keep invoking `math::add`, and the Engine routes the calls to whichever
+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
+registered Function and Trigger. When a Worker connects, the Engine records what Functions it
+provides. When a Worker disconnects, the Engine removes its Functions, cancels any in-flight
+invocations of those Functions, and notifies the rest of the system that the topology changed.
+
+Routing is independent of language, runtime, and location. The Engine does not need to know where
+`math::add` is running in Docker, on a Raspberry Pi, or in a browser tab. It just knows that _some_
+Worker provides it. The same tutorial can be redeployed across different runtimes without touching
+the function code.
+
+
+ See [Engine](/understanding-iii/engine) for startup flow, config hot-reload, and the live registry
+ and discovery surface.
+
diff --git a/docs/0-13-0/understanding-iii/triggers.mdx b/docs/0-13-0/understanding-iii/triggers.mdx
new file mode 100644
index 000000000..d19564246
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/triggers.mdx
@@ -0,0 +1,115 @@
+---
+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.
+
+## Registration errors
+
+When the Engine cannot register a Trigger — most commonly because the Trigger type's Worker is not
+active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the
+Worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it
+during development.
+
+For built-in Trigger types — `http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`,
+`stream:join`, `stream:leave`, and `log` — the error message includes the install command for the
+missing Worker (the exact name reported matches what the Worker registers, e.g. `stream:join`
+rather than the generic `stream`). For unknown Trigger types that are not built-in, the error
+message points to the workers directory at .
+
+Example log line when `iii-http` is not active:
+
+```
+[iii] Trigger registration failed for "1c1b…" (http): Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http
+```
+
+Example log line for an unknown non-built-in Trigger type:
+
+```
+[iii] Trigger registration failed for "1c1b…" (my-custom-trigger): Trigger type "my-custom-trigger" not found. Search for a worker that provides this trigger type at https://workers.iii.dev/
+```
+
+Logging targets per SDK:
+
+- **Node** — `console.error`
+- **Python** — the `iii` logger, level `ERROR`
+- **Rust** — `tracing::error!`
diff --git a/docs/0-13-0/understanding-iii/triggers.mdx.skill.md b/docs/0-13-0/understanding-iii/triggers.mdx.skill.md
new file mode 100644
index 000000000..3446d7d53
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/triggers.mdx.skill.md
@@ -0,0 +1,82 @@
+
+
+# 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/0-13-0/understanding-iii/workers.mdx b/docs/0-13-0/understanding-iii/workers.mdx
new file mode 100644
index 000000000..d21647146
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/workers.mdx
@@ -0,0 +1,33 @@
+---
+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/0-13-0/understanding-iii/workers.mdx.skill.md b/docs/0-13-0/understanding-iii/workers.mdx.skill.md
new file mode 100644
index 000000000..c9ec9db25
--- /dev/null
+++ b/docs/0-13-0/understanding-iii/workers.mdx.skill.md
@@ -0,0 +1,31 @@
+
+
+# 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/0-13-0/using-iii/channels.mdx b/docs/0-13-0/using-iii/channels.mdx
new file mode 100644
index 000000000..8f4653660
--- /dev/null
+++ b/docs/0-13-0/using-iii/channels.mdx
@@ -0,0 +1,218 @@
+---
+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/0-13-0/using-iii/channels.mdx.skill.md b/docs/0-13-0/using-iii/channels.mdx.skill.md
new file mode 100644
index 000000000..4c9b60dca
--- /dev/null
+++ b/docs/0-13-0/using-iii/channels.mdx.skill.md
@@ -0,0 +1,214 @@
+
+
+
+## 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/0-13-0/using-iii/cli.mdx b/docs/0-13-0/using-iii/cli.mdx
new file mode 100644
index 000000000..011903292
--- /dev/null
+++ b/docs/0-13-0/using-iii/cli.mdx
@@ -0,0 +1,65 @@
+---
+title: "CLI"
+description: "The iii CLI: discover commands, trigger functions, and manage iii itself."
+owner: "devrel"
+type: "how-to"
+---
+
+For the most current list of commands, flags, and defaults, ask the binary directly:
+
+```bash
+iii --help #lists iii's flags and available subcommands
+iii subcommand --help #lists the specific actions each sucommand accepts
+```
+
+Each subcommand's help also provides the defaults and environment variables that affect each
+command.
+
+## Trigger a function from the CLI
+
+`iii trigger [arg=value ...]` invokes a registered function against a running engine.
+The engine routes the call to whatever worker registered the function; no trigger registration is
+involved.
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+
+ For the full invocation surface, payload formats, fire-and-forget, queue-routed actions, and the
+ equivalent SDK call from worker code, see [Triggers / Call a function
+ directly](/using-iii/triggers#call-a-function-directly).
+
+
+## Subcommands
+
+| Subcommand | What it does |
+| ------------- | --------------------------------------------------------------------------------------------------------- |
+| `iii trigger` | Invoke a registered function on a running engine. |
+| `iii worker` | Manage workers: add, remove, list, start/stop, update, verify. See [Workers](/using-iii/workers). |
+| `iii project` | Manage iii projects: scaffold a new one, generate Docker assets. See [Deployment](/using-iii/deployment). |
+| `iii console` | Launch the iii web console. See [Console](/using-iii/console). |
+| `iii cloud` | Manage hosted iii deployments. See [Managing iii Cloud deployments](#managing-iii-cloud-deployments). |
+| `iii update` | Update iii and its managed binaries. See [Updating iii itself](#updating-iii-itself). |
+
+Running `iii` with no subcommand starts the engine from `./config.yaml` (or the path passed to
+`--config`); pass `--use-default-config` to start with the built-in defaults instead.
+
+## Managing iii Cloud deployments
+
+The `iii cloud` subcommand group will manage hosted iii deployments. See
+[Deployment](/using-iii/deployment#iii-cloud-deployments) for details about the iii cloud.
+
+iii's cloud will be available soon.
+
+{/* llm-only: Always verify the claim about iii cloud above, it could have changed since when this document was written. */}
+
+## Updating iii
+
+`iii update` refreshes iii and its managed binaries to their latest versions. This is distinct from
+`iii worker update`, which refreshes pinned worker versions inside a project.
+
+
+ Specific targets can be updated individually via `iii update [target]`. Run `iii update
+ --list-targets` to see the list of targets.
+
diff --git a/docs/0-13-0/using-iii/cli.mdx.skill.md b/docs/0-13-0/using-iii/cli.mdx.skill.md
new file mode 100644
index 000000000..40e8ed967
--- /dev/null
+++ b/docs/0-13-0/using-iii/cli.mdx.skill.md
@@ -0,0 +1,63 @@
+
+
+# CLI
+
+
+For the most current list of commands, flags, and defaults, ask the binary directly:
+
+```bash
+iii --help #lists iii's flags and available subcommands
+iii subcommand --help #lists the specific actions each sucommand accepts
+```
+
+Each subcommand's help also provides the defaults and environment variables that affect each
+command.
+
+## Trigger a function from the CLI
+
+`iii trigger [arg=value ...]` invokes a registered function against a running engine.
+The engine routes the call to whatever worker registered the function; no trigger registration is
+involved.
+
+```bash
+iii trigger math::add a=2 b=3
+```
+
+
+ For the full invocation surface, payload formats, fire-and-forget, queue-routed actions, and the
+ equivalent SDK call from worker code, see [Triggers / Call a function
+ directly](/using-iii/triggers#call-a-function-directly).
+
+
+## Subcommands
+
+| Subcommand | What it does |
+| ------------- | --------------------------------------------------------------------------------------------------------- |
+| `iii trigger` | Invoke a registered function on a running engine. |
+| `iii worker` | Manage workers: add, remove, list, start/stop, update, verify. See [Workers](/using-iii/workers). |
+| `iii project` | Manage iii projects: scaffold a new one, generate Docker assets. See [Deployment](/using-iii/deployment). |
+| `iii console` | Launch the iii web console. See [Console](/using-iii/console). |
+| `iii cloud` | Manage hosted iii deployments. See [Managing iii Cloud deployments](#managing-iii-cloud-deployments). |
+| `iii update` | Update iii and its managed binaries. See [Updating iii itself](#updating-iii-itself). |
+
+Running `iii` with no subcommand starts the engine from `./config.yaml` (or the path passed to
+`--config`); pass `--use-default-config` to start with the built-in defaults instead.
+
+## Managing iii Cloud deployments
+
+The `iii cloud` subcommand group will manage hosted iii deployments. See
+[Deployment](/using-iii/deployment#iii-cloud-deployments) for details about the iii cloud.
+
+iii's cloud will be available soon.
+
+Always verify the claim about iii cloud above, it could have changed since when this document was written.
+
+## Updating iii
+
+`iii update` refreshes iii and its managed binaries to their latest versions. This is distinct from
+`iii worker update`, which refreshes pinned worker versions inside a project.
+
+
+ Specific targets can be updated individually via `iii update [target]`. Run `iii update
+ --list-targets` to see the list of targets.
+
diff --git a/docs/0-13-0/using-iii/console.mdx b/docs/0-13-0/using-iii/console.mdx
new file mode 100644
index 000000000..f4383f406
--- /dev/null
+++ b/docs/0-13-0/using-iii/console.mdx
@@ -0,0 +1,173 @@
+---
+title: "Console"
+description: "The visual UI for your running iii system."
+owner: "devrel"
+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.
+
+
+## Launch the console
+
+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
+```
+
+Open `http://127.0.0.1:3113` in your browser.
+
+
+ Naming review: `iii console` is verb-shaped (launches the console) but doesn't follow the `iii
+ noun verb` convention used elsewhere in the CLI. Flagged for naming review per
+ `project-rules/cli.md`.
+
+
+## Workers page
+
+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.
+
+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.
+
+
+ The isolation badge reflects what the worker self-reports via the `III_ISOLATION` environment
+ variable. The libkrun launcher injects `III_ISOLATION=libkrun` automatically; for container or
+ Kubernetes deployments, set `III_ISOLATION=docker` (or similar) yourself. Workers that don't set
+ the variable show no badge.
+
+
+## Functions page
+
+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:
+
+- A description (pulled from the function's request schema, if present).
+- A request-body editor pre-filled from the request schema as a template.
+- An **Invoke** button that calls the function with the editor's payload.
+- A result view that shows the response (or error) with the call duration.
+
+## Triggers page
+
+Lists every registered trigger with **All / HTTP / Cron / Event / Other** filter tabs that show
+counts per type. Each row shows a summary (HTTP method and path, the readable cron expression, or
+the event topic), a type badge, the `function_id` it routes to, and the function's description.
+
+Selecting a trigger opens a detail panel with type-specific testing tools:
+
+- **HTTP**. Method dropdown (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`), path parameter inputs, a
+ query-parameter builder, a JSON body editor for write methods, and a **Send Request** button.
+- **Cron**. Schedule (human-readable + raw expression), next-run preview, status, and a **Run Now**
+ button that fires the bound function immediately.
+- **Event**. Event topic display, a JSON payload editor, and an **Emit Event** button.
+
+State-bound triggers don't have a dedicated tester on this page; fire them by editing the relevant
+entry on the [States page](#states-page).
+
+## States page
+
+Browser for the engine's key-value state store. The layout is groups list (left), items table
+(center, sortable on Key / Type), and a detail panel (right) that opens on row click.
+
+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.
+
+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).
+
+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:
+
+- **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.
+
+## Traces page
+
+OpenTelemetry trace visualization across four view modes:
+
+- **Waterfall**. Timeline view with spans laid out by start time and duration. Default.
+- **Flame Graph**. Stack-based view; wider bars mean longer-running spans.
+- **Trace Map**. Topology graph showing service-to-service edges via parent-child spans.
+- **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.
+
+
+ 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.
+
+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).
+
+
+ Log collection requires the
+ iii-observability worker with logs enabled.
+
+
+## Configuration
+
+The console reads engine and observability settings from CLI flags and environment variables.
+
+To list every flag the binary accepts (with its default), run:
+
+```bash
+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.
diff --git a/docs/0-13-0/using-iii/console.mdx.skill.md b/docs/0-13-0/using-iii/console.mdx.skill.md
new file mode 100644
index 000000000..180e92069
--- /dev/null
+++ b/docs/0-13-0/using-iii/console.mdx.skill.md
@@ -0,0 +1,169 @@
+
+
+
+{/* 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.
+
+
+## Launch the console
+
+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
+```
+
+Open `http://127.0.0.1:3113` in your browser.
+
+
+ Naming review: `iii console` is verb-shaped (launches the console) but doesn't follow the `iii
+ noun verb` convention used elsewhere in the CLI. Flagged for naming review per
+ `project-rules/cli.md`.
+
+
+## Workers page
+
+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.
+
+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.
+
+
+ The isolation badge reflects what the worker self-reports via the `III_ISOLATION` environment
+ variable. The libkrun launcher injects `III_ISOLATION=libkrun` automatically; for container or
+ Kubernetes deployments, set `III_ISOLATION=docker` (or similar) yourself. Workers that don't set
+ the variable show no badge.
+
+
+## Functions page
+
+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:
+
+- A description (pulled from the function's request schema, if present).
+- A request-body editor pre-filled from the request schema as a template.
+- An **Invoke** button that calls the function with the editor's payload.
+- A result view that shows the response (or error) with the call duration.
+
+## Triggers page
+
+Lists every registered trigger with **All / HTTP / Cron / Event / Other** filter tabs that show
+counts per type. Each row shows a summary (HTTP method and path, the readable cron expression, or
+the event topic), a type badge, the `function_id` it routes to, and the function's description.
+
+Selecting a trigger opens a detail panel with type-specific testing tools:
+
+- **HTTP**. Method dropdown (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`), path parameter inputs, a
+ query-parameter builder, a JSON body editor for write methods, and a **Send Request** button.
+- **Cron**. Schedule (human-readable + raw expression), next-run preview, status, and a **Run Now**
+ button that fires the bound function immediately.
+- **Event**. Event topic display, a JSON payload editor, and an **Emit Event** button.
+
+State-bound triggers don't have a dedicated tester on this page; fire them by editing the relevant
+entry on the [States page](#states-page).
+
+## States page
+
+Browser for the engine's key-value state store. The layout is groups list (left), items table
+(center, sortable on Key / Type), and a detail panel (right) that opens on row click.
+
+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.
+
+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).
+
+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:
+
+- **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.
+
+## Traces page
+
+OpenTelemetry trace visualization across four view modes:
+
+- **Waterfall**. Timeline view with spans laid out by start time and duration. Default.
+- **Flame Graph**. Stack-based view; wider bars mean longer-running spans.
+- **Trace Map**. Topology graph showing service-to-service edges via parent-child spans.
+- **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.
+
+
+ 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.
+
+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).
+
+
+ Log collection requires the
+ iii-observability worker with logs enabled.
+
+
+## Configuration
+
+The console reads engine and observability settings from CLI flags and environment variables.
+
+To list every flag the binary accepts (with its default), run:
+
+```bash
+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.
diff --git a/docs/0-13-0/using-iii/deployment.mdx b/docs/0-13-0/using-iii/deployment.mdx
new file mode 100644
index 000000000..80ba65241
--- /dev/null
+++ b/docs/0-13-0/using-iii/deployment.mdx
@@ -0,0 +1,118 @@
+---
+title: "Deployment"
+description: "Running iii in development, staging, and production."
+owner: "devrel"
+type: "how-to"
+---
+
+This page covers running the iii engine and its workers in a production environment. For local
+development, see the [Quickstart](/quickstart) and the [Engine](./engine) page.
+
+## iii Cloud deployments
+
+The `iii cloud` subcommand group will manage hosted iii deployments. See [CLI](./cli) for the
+command surface as it stabilizes.
+
+iii's cloud will be available soon.
+
+{/* llm-only: Always verify the claim about iii cloud above, it could have changed since when this document was written. */}
+
+## Deploy with Docker
+
+Generate the Docker assets with the CLI and bring them up with Compose. Use
+`iii project init --docker` for a fresh project, or `iii project generate-docker` to add Docker
+assets to an existing one:
+
+```bash
+iii project generate-docker
+```
+
+Both forms emit three files at the project root: `Dockerfile`, `docker-compose.yml`, and `.env`.
+Re-running the generator does not overwrite existing files, so edits you make to the templates
+stick.
+
+Start the stack:
+
+```bash
+docker compose up -d
+```
+
+The generated `docker-compose.yml` exposes:
+
+| Port | Service |
+| ----- | ---------------------------------- |
+| 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
+adapters.
+
+## Configure a reverse proxy
+
+The engine does not terminate TLS. Place a reverse proxy in front of it to handle TLS and route the
+three transport surfaces (`/api/*`, `/stream/*`, `/ws`) to the right ports.
+
+### Caddy
+
+```
+your-domain.com {
+ handle /api/* {
+ reverse_proxy 127.0.0.1:3111
+ }
+ handle /stream/* {
+ reverse_proxy 127.0.0.1:3112
+ }
+ handle /ws {
+ reverse_proxy 127.0.0.1:49134
+ }
+ handle {
+ reverse_proxy 127.0.0.1:3111
+ }
+}
+```
+
+
+ This is only an example configuration. Refer to the [Caddy
+ documentation](https://caddyserver.com/docs/) for full configuration.
+
+
+### Nginx
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name your-domain.com;
+
+ location /api/ {
+ proxy_pass http://127.0.0.1:3111;
+ }
+
+ location /ws {
+ proxy_pass http://127.0.0.1:49134;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ location /stream/ {
+ proxy_pass http://127.0.0.1:3112;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:3111;
+ }
+}
+```
+
+
+ This is only an example configuration. See the [Nginx documentation](https://nginx.org/en/docs/)
+ for the full configuration such as SSL, header, and proxy options.
+
+
+{/* TODO: enumerate the `iii cloud` surface once stable (deploy, logs, ssh, env, etc.) and link out to the iii Cloud product docs in the section at the top of this page. */}
diff --git a/docs/0-13-0/using-iii/deployment.mdx.skill.md b/docs/0-13-0/using-iii/deployment.mdx.skill.md
new file mode 100644
index 000000000..92130089d
--- /dev/null
+++ b/docs/0-13-0/using-iii/deployment.mdx.skill.md
@@ -0,0 +1,116 @@
+
+
+# Deployment
+
+
+This page covers running the iii engine and its workers in a production environment. For local
+development, see the [Quickstart](/quickstart) and the [Engine](./engine) page.
+
+## iii Cloud deployments
+
+The `iii cloud` subcommand group will manage hosted iii deployments. See [CLI](./cli) for the
+command surface as it stabilizes.
+
+iii's cloud will be available soon.
+
+Always verify the claim about iii cloud above, it could have changed since when this document was written.
+
+## Deploy with Docker
+
+Generate the Docker assets with the CLI and bring them up with Compose. Use
+`iii project init --docker` for a fresh project, or `iii project generate-docker` to add Docker
+assets to an existing one:
+
+```bash
+iii project generate-docker
+```
+
+Both forms emit three files at the project root: `Dockerfile`, `docker-compose.yml`, and `.env`.
+Re-running the generator does not overwrite existing files, so edits you make to the templates
+stick.
+
+Start the stack:
+
+```bash
+docker compose up -d
+```
+
+The generated `docker-compose.yml` exposes:
+
+| Port | Service |
+| ----- | ---------------------------------- |
+| 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
+adapters.
+
+## Configure a reverse proxy
+
+The engine does not terminate TLS. Place a reverse proxy in front of it to handle TLS and route the
+three transport surfaces (`/api/*`, `/stream/*`, `/ws`) to the right ports.
+
+### Caddy
+
+```
+your-domain.com {
+ handle /api/* {
+ reverse_proxy 127.0.0.1:3111
+ }
+ handle /stream/* {
+ reverse_proxy 127.0.0.1:3112
+ }
+ handle /ws {
+ reverse_proxy 127.0.0.1:49134
+ }
+ handle {
+ reverse_proxy 127.0.0.1:3111
+ }
+}
+```
+
+
+ This is only an example configuration. Refer to the [Caddy
+ documentation](https://caddyserver.com/docs/) for full configuration.
+
+
+### Nginx
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name your-domain.com;
+
+ location /api/ {
+ proxy_pass http://127.0.0.1:3111;
+ }
+
+ location /ws {
+ proxy_pass http://127.0.0.1:49134;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ location /stream/ {
+ proxy_pass http://127.0.0.1:3112;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:3111;
+ }
+}
+```
+
+
+ This is only an example configuration. See the [Nginx documentation](https://nginx.org/en/docs/)
+ for the full configuration such as SSL, header, and proxy options.
+
+
+{/* TODO: enumerate the `iii cloud` surface once stable (deploy, logs, ssh, env, etc.) and link out to the iii Cloud product docs in the section at the top of this page. */}
diff --git a/docs/0-13-0/using-iii/engine.mdx b/docs/0-13-0/using-iii/engine.mdx
new file mode 100644
index 000000000..af8eee3d6
--- /dev/null
+++ b/docs/0-13-0/using-iii/engine.mdx
@@ -0,0 +1,75 @@
+---
+title: "Engine"
+description: "Configuring and running the iii engine."
+owner: "devrel"
+type: "how-to"
+---
+
+## The engine is just a router
+
+{/* skill:exclude-section */}
+
+Think of the engine like a router, it receives requests, routes them to workers, and routes responses back as needed.
+
+## Engine configuration
+
+The iii engine starts from a `config.yaml` file at your project root. Pass `--config ` to
+point at a different file, or `--use-default-config` to start with a default set of workers (handy
+for first-run and scratch work).
+
+```bash
+iii --config config.yaml
+```
+
+## Configuration file structure
+
+`config.yaml` has a single top-level key, `workers:`, that lists the workers the engine should load.
+Each entry has a `name` (a registry slug or local worker name) and a `config` block whose shape is
+defined by that worker.
+
+```yaml
+workers:
+ - name: iii-http
+ config:
+ port: 3111
+ host: 127.0.0.1
+
+ - name: iii-state
+ config:
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: ./data/state_store.db
+```
+
+{/* TODO: replace this paragraph with the auto-generated engine config reference. Per project-rules/config.md, the per-field `workers:` schema should not be hand-authored here; this section is a placeholder pending the generated reference. */}
+
+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.
+
+
+ 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
+ [Creating Workers / Connecting to the engine](/creating-workers/workers#connecting-to-the-engine) for more information.
+
+
+## Environment variable expansion
+
+Values in `config.yaml` support `${VAR:default}` syntax. The expansion uses the value of the
+environment variable `VAR`, falling back to `default` when the variable is not set. Use this to swap
+ports, URLs, and feature flags per environment without forking the config file.
+
+```yaml
+workers:
+ - name: iii-http
+ config:
+ port: ${HTTP_PORT:3111}
+ host: ${HTTP_HOST:127.0.0.1}
+```
+
+## Default configuration
+
+Run `iii --use-default-config` to start the engine with a default set of workers without writing a
+`config.yaml`. Useful for first-run and scratch work. Once you need to customize ports, adapters, or
+the set of workers, switch to a real `config.yaml`.
diff --git a/docs/0-13-0/using-iii/engine.mdx.skill.md b/docs/0-13-0/using-iii/engine.mdx.skill.md
new file mode 100644
index 000000000..bce43453a
--- /dev/null
+++ b/docs/0-13-0/using-iii/engine.mdx.skill.md
@@ -0,0 +1,67 @@
+
+
+# Engine
+
+
+## Engine configuration
+
+The iii engine starts from a `config.yaml` file at your project root. Pass `--config ` to
+point at a different file, or `--use-default-config` to start with a default set of workers (handy
+for first-run and scratch work).
+
+```bash
+iii --config config.yaml
+```
+
+## Configuration file structure
+
+`config.yaml` has a single top-level key, `workers:`, that lists the workers the engine should load.
+Each entry has a `name` (a registry slug or local worker name) and a `config` block whose shape is
+defined by that worker.
+
+```yaml
+workers:
+ - name: iii-http
+ config:
+ port: 3111
+ host: 127.0.0.1
+
+ - name: iii-state
+ config:
+ adapter:
+ name: kv
+ config:
+ store_method: file_based
+ file_path: ./data/state_store.db
+```
+
+{/* TODO: replace this paragraph with the auto-generated engine config reference. Per project-rules/config.md, the per-field `workers:` schema should not be hand-authored here; this section is a placeholder pending the generated reference. */}
+
+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.
+
+
+ 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
+ [Creating Workers / Connecting to the engine](/creating-workers/workers#connecting-to-the-engine) for more information.
+
+
+## Environment variable expansion
+
+Values in `config.yaml` support `${VAR:default}` syntax. The expansion uses the value of the
+environment variable `VAR`, falling back to `default` when the variable is not set. Use this to swap
+ports, URLs, and feature flags per environment without forking the config file.
+
+```yaml
+workers:
+ - name: iii-http
+ config:
+ port: ${HTTP_PORT:3111}
+ host: ${HTTP_HOST:127.0.0.1}
+```
+
+## Default configuration
+
+Run `iii --use-default-config` to start the engine with a default set of workers without writing a
+`config.yaml`. Useful for first-run and scratch work. Once you need to customize ports, adapters, or
+the set of workers, switch to a real `config.yaml`.
diff --git a/docs/0-13-0/using-iii/functions.mdx b/docs/0-13-0/using-iii/functions.mdx
new file mode 100644
index 000000000..37ccd1415
--- /dev/null
+++ b/docs/0-13-0/using-iii/functions.mdx
@@ -0,0 +1,98 @@
+---
+title: "Functions"
+description: "Registering and invoking functions in your iii project."
+owner: "devrel"
+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
+the iii system. The `id` follows the `service::name` form; the handler receives the call's payload
+and returns the result.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
+ return { c: payload.a + payload.b };
+ });
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ def add_handler(payload: dict) -> dict:
+ return {"c": payload["a"] + payload["b"]}
+
+ worker.register_function("math::add", add_handler)
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+
+ 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 }))
+ }));
+ ```
+
+
+
+
+## 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.
+
+
+ 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.
+
+
+
+ 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).
+
diff --git a/docs/0-13-0/using-iii/functions.mdx.skill.md b/docs/0-13-0/using-iii/functions.mdx.skill.md
new file mode 100644
index 000000000..2f729669e
--- /dev/null
+++ b/docs/0-13-0/using-iii/functions.mdx.skill.md
@@ -0,0 +1,96 @@
+
+
+# Functions
+
+
+{/* 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
+the iii system. The `id` follows the `service::name` form; the handler receives the call's payload
+and returns the result.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
+ return { c: payload.a + payload.b };
+ });
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="math-worker"),
+ )
+
+ def add_handler(payload: dict) -> dict:
+ return {"c": payload["a"] + payload["b"]}
+
+ worker.register_function("math::add", add_handler)
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterFunction, register_worker};
+
+ 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 }))
+ }));
+ ```
+
+
+
+
+## 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.
+
+
+ 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.
+
+
+
+ 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).
+
diff --git a/docs/0-13-0/using-iii/triggers.mdx b/docs/0-13-0/using-iii/triggers.mdx
new file mode 100644
index 000000000..1e143f3b8
--- /dev/null
+++ b/docs/0-13-0/using-iii/triggers.mdx
@@ -0,0 +1,174 @@
+---
+title: "Triggers"
+description: "Calling functions directly or by binding them to events in your iii project."
+owner: "devrel"
+type: "how-to"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+## Call a function directly
+
+Call a function by its `function_id` from worker code (`worker.trigger(...)`) or from the terminal
+(`iii trigger`). The engine routes the call to whatever worker registered the function; no trigger
+registration is involved. The `action` field controls delivery: by default the call waits for the
+function to return its result, or for the configured timeout to fire. Pass a different
+`TriggerAction` to change that.
+
+
+
+ ```typescript
+ import { registerWorker, TriggerAction } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 2, b: 3 },
+ // action: TriggerAction.Void(), // fire-and-forget
+ // action: TriggerAction.Enqueue({ queue: "math" }), // route through iii-queue
+ });
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions, TriggerAction
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="caller"),
+ )
+
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 2, "b": 3},
+ # "action": TriggerAction.Void(), # fire-and-forget
+ # "action": TriggerAction.Enqueue(queue="math"), # route through iii-queue
+ })
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, TriggerAction, TriggerRequest, register_worker};
+ use serde_json::json;
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".to_string(),
+ payload: json!({ "a": 2, "b": 3 }),
+ action: None,
+ // action: Some(TriggerAction::Void), // fire-and-forget
+ // action: Some(TriggerAction::Enqueue { queue: "math".to_string() }), // route through iii-queue
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+ ```bash
+ iii trigger math::add a=2 b=3
+ ```
+
+
+
+Some common actions are:
+
+- **Default (synchronous)**. No `action` set. The call waits for the function to return its result
+ or for the configured timeout to fire.
+- **`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
+ queue with retries; the call returns once the message is enqueued.
+
+
+ Workers can provide their own `TriggerAction`s. Check each [worker's
+ documentation](https://workers.iii.dev) for the action types it offers.
+
+
+## Register a trigger
+
+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).
+
+You bind triggers to functions via the `function_id`. The trigger declares its `type`, its `config`
+(defined by each type), and the function to invoke.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="my-worker"),
+ )
+
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterTriggerInput, register_worker};
+ use serde_json::json;
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
+ ```
+
+
+
+
+Per-type configuration is documented in each worker's Worker Docs (e.g.
+iii-http for the `http` type).
+
+## Bind multiple triggers to one function
+
+It's valid to bind multiple triggers to the same `function_id` and this can be done across any
+number of types. Register a second trigger with the same `function_id` and a different type or
+config; the function runs unchanged whether the call arrives over HTTP, on a cron schedule, or from
+a queue message.
+
+## 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.
+
+## 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.
diff --git a/docs/0-13-0/using-iii/triggers.mdx.skill.md b/docs/0-13-0/using-iii/triggers.mdx.skill.md
new file mode 100644
index 000000000..fbd398310
--- /dev/null
+++ b/docs/0-13-0/using-iii/triggers.mdx.skill.md
@@ -0,0 +1,170 @@
+
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+## Call a function directly
+
+Call a function by its `function_id` from worker code (`worker.trigger(...)`) or from the terminal
+(`iii trigger`). The engine routes the call to whatever worker registered the function; no trigger
+registration is involved. The `action` field controls delivery: by default the call waits for the
+function to return its result, or for the configured timeout to fire. Pass a different
+`TriggerAction` to change that.
+
+
+
+ ```typescript
+ import { registerWorker, TriggerAction } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ const result = await worker.trigger({
+ function_id: "math::add",
+ payload: { a: 2, b: 3 },
+ // action: TriggerAction.Void(), // fire-and-forget
+ // action: TriggerAction.Enqueue({ queue: "math" }), // route through iii-queue
+ });
+ ```
+
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions, TriggerAction
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="caller"),
+ )
+
+ result = worker.trigger({
+ "function_id": "math::add",
+ "payload": {"a": 2, "b": 3},
+ # "action": TriggerAction.Void(), # fire-and-forget
+ # "action": TriggerAction.Enqueue(queue="math"), # route through iii-queue
+ })
+ ```
+
+
+
+ ```rust
+ use iii_sdk::{InitOptions, TriggerAction, TriggerRequest, register_worker};
+ use serde_json::json;
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ let result = worker
+ .trigger(TriggerRequest {
+ function_id: "math::add".to_string(),
+ payload: json!({ "a": 2, "b": 3 }),
+ action: None,
+ // action: Some(TriggerAction::Void), // fire-and-forget
+ // action: Some(TriggerAction::Enqueue { queue: "math".to_string() }), // route through iii-queue
+ timeout_ms: None,
+ })
+ .await?;
+ ```
+
+
+
+ ```bash
+ iii trigger math::add a=2 b=3
+ ```
+
+
+
+Some common actions are:
+
+- **Default (synchronous)**. No `action` set. The call waits for the function to return its result
+ or for the configured timeout to fire.
+- **`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
+ queue with retries; the call returns once the message is enqueued.
+
+
+ Workers can provide their own `TriggerAction`s. Check each [worker's
+ documentation](https://workers.iii.dev) for the action types it offers.
+
+
+## Register a trigger
+
+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).
+
+You bind triggers to functions via the `function_id`. The trigger declares its `type`, its `config`
+(defined by each type), and the function to invoke.
+
+
+
+ ```typescript
+ import { registerWorker } from "iii-sdk";
+
+ const worker = registerWorker(process.env.III_URL);
+
+ worker.registerTrigger({
+ type: "http",
+ function_id: "math::add",
+ config: { api_path: "/math/add", http_method: "POST" },
+ });
+ ```
+
+
+ ```python
+ import os
+ from iii import register_worker, InitOptions
+
+ worker = register_worker(
+ os.environ.get("III_URL"),
+ InitOptions(worker_name="my-worker"),
+ )
+
+ worker.register_trigger({
+ "type": "http",
+ "function_id": "math::add",
+ "config": {"api_path": "/math/add", "http_method": "POST"},
+ })
+ ```
+
+
+ ```rust
+ use iii_sdk::{InitOptions, RegisterTriggerInput, register_worker};
+ use serde_json::json;
+
+ let url = std::env::var("III_URL").expect("III_URL must be set");
+ let worker = register_worker(&url, InitOptions::default());
+
+ worker.register_trigger(RegisterTriggerInput {
+ trigger_type: "http".into(),
+ function_id: "math::add".into(),
+ config: json!({ "api_path": "/math/add", "http_method": "POST" }),
+ metadata: None,
+ })?;
+ ```
+
+
+
+
+Per-type configuration is documented in each worker's Worker Docs (e.g.
+iii-http for the `http` type).
+
+## Bind multiple triggers to one function
+
+It's valid to bind multiple triggers to the same `function_id` and this can be done across any
+number of types. Register a second trigger with the same `function_id` and a different type or
+config; the function runs unchanged whether the call arrives over HTTP, on a cron schedule, or from
+a queue message.
+
+## 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.
+
+## 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.
diff --git a/docs/0-13-0/using-iii/workers-registry.mdx b/docs/0-13-0/using-iii/workers-registry.mdx
new file mode 100644
index 000000000..408bb4553
--- /dev/null
+++ b/docs/0-13-0/using-iii/workers-registry.mdx
@@ -0,0 +1,45 @@
+---
+title: "Worker Registry"
+description: "A central repository of iii workers."
+owner: "devrel"
+type: "how-to"
+---
+
+## Browsing the registry
+
+The iii Worker Registry at [workers.iii.dev](https://workers.iii.dev/) is the index of installable
+workers. Each worker page lists the functions and trigger types it provides, its configuration
+schema, supported platforms, and agent skills. Use that information to locate a worker that fulfills
+a capability your project needs.
+
+{/* TODO: Add a llm-only note here on how an llm can browse the registry once that surface is stable in the skills worker */}
+
+Workers can also be found on Docker and OCI-compatible registries.
+
+## Adding a worker
+
+`iii worker add` accepts three sources. In every case the worker is added to your project's
+`config.yaml` and started automatically.
+
+```bash
+iii worker add iii-state # registry name
+iii worker add ./workers/my-worker # local path
+iii worker add ghcr.io/org/worker:tag # Docker or OCI image
+```
+
+
+ Registry workers are published with semver versions. For how versions are picked, pinned with
+ `@`, updated, and recorded in `iii.lock`, see [Versioning and
+ pinning](./workers#versioning-and-pinning) and [Updating a
+ worker](./workers#updating-a-worker) on the Workers page.
+
+
+
+ For removing workers and the broader `iii worker` subcommand set (start, stop, restart, sync,
+ verify, etc.), see [Workers](./workers).
+
+
+## Artifact types
+
+Each registry worker is published as either a native binary (with per-platform artifacts for macOS,
+Linux, and Windows) or as a Docker / OCI compatible image that runs on every supported platform.
diff --git a/docs/0-13-0/using-iii/workers-registry.mdx.skill.md b/docs/0-13-0/using-iii/workers-registry.mdx.skill.md
new file mode 100644
index 000000000..792551c41
--- /dev/null
+++ b/docs/0-13-0/using-iii/workers-registry.mdx.skill.md
@@ -0,0 +1,43 @@
+
+
+# Worker Registry
+
+
+## Browsing the registry
+
+The iii Worker Registry at [workers.iii.dev](https://workers.iii.dev/) is the index of installable
+workers. Each worker page lists the functions and trigger types it provides, its configuration
+schema, supported platforms, and agent skills. Use that information to locate a worker that fulfills
+a capability your project needs.
+
+{/* TODO: Add a llm-only note here on how an llm can browse the registry once that surface is stable in the skills worker */}
+
+Workers can also be found on Docker and OCI-compatible registries.
+
+## Adding a worker
+
+`iii worker add` accepts three sources. In every case the worker is added to your project's
+`config.yaml` and started automatically.
+
+```bash
+iii worker add iii-state # registry name
+iii worker add ./workers/my-worker # local path
+iii worker add ghcr.io/org/worker:tag # Docker or OCI image
+```
+
+
+ Registry workers are published with semver versions. For how versions are picked, pinned with
+ `@`, updated, and recorded in `iii.lock`, see [Versioning and
+ pinning](./workers#versioning-and-pinning) and [Updating a
+ worker](./workers#updating-a-worker) on the Workers page.
+
+
+
+ For removing workers and the broader `iii worker` subcommand set (start, stop, restart, sync,
+ verify, etc.), see [Workers](./workers).
+
+
+## Artifact types
+
+Each registry worker is published as either a native binary (with per-platform artifacts for macOS,
+Linux, and Windows) or as a Docker / OCI compatible image that runs on every supported platform.
diff --git a/docs/0-13-0/using-iii/workers.mdx b/docs/0-13-0/using-iii/workers.mdx
new file mode 100644
index 000000000..c9e0ece1d
--- /dev/null
+++ b/docs/0-13-0/using-iii/workers.mdx
@@ -0,0 +1,180 @@
+---
+title: "Workers"
+description: "Using workers in your iii project."
+owner: "devrel"
+type: "reference"
+---
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+{/* TODO: Add back once worker init is merged. This page covers using existing workers. For creating new workers (`iii worker init`), registering functions and triggers, and building or publishing worker images, see [Creating Workers / Workers](/creating-workers/workers). */}
+
+## Workers are sort of like services
+
+{/* skill:exclude-section */}
+
+Think of workers as iii's version of a self-contained, isolated service that freely interoperates
+with every other worker. Any time you're adding functionality that functionality will come in the
+form of a worker.
+
+Workers differ from traditional services by not requiring any integration to use. Instead workers
+can be installed and managed like npm packages with the command `iii worker add` below. The
+difference is that you get complete deployable runtimes rather than a library. See the section on
+adding workers below.
+
+## Worker lifecycle
+
+Workers connect to iii over WebSocket. When a worker connects it becomes visible to the entire iii
+system and every other worker within it. When a worker disconnects, its functions and triggers stop
+being callable until it reconnects.
+
+
+ For the SDK calls that establish the connection from worker code, see [Creating Workers /
+ Workers](/creating-workers/workers#connecting-to-the-engine).
+
+
+## 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
+
+
+ You need iii [installed](/install) and [running](/using-iii/engine) before adding a worker. To
+ spin up a temporary iii instance for testing, run `iii --use-default-config` (see [Default
+ configuration](/using-iii/engine#default-configuration)).
+
+
+`iii worker add ` installs a worker into your project:
+
+```bash
+iii worker add iii-state
+```
+
+The worker is added to `config.yaml` and started automatically. To force a redownload of an existing
+worker, use `iii worker reinstall ` (equivalent to `add --force`).
+
+
+ For local paths, Docker / OCI images, and version pinning, see [Worker Registry / Adding a
+ worker](./workers-registry#adding-a-worker).
+
+
+## Listing workers
+
+`iii worker list` shows every worker declared in your project's `config.yaml` along with its current
+status:
+
+```bash
+iii worker list
+```
+
+## Starting and stopping workers
+
+Added workers start automatically with the engine. To control them manually, use the `start`,
+`stop`, and `restart` commands:
+
+```bash
+iii worker start # start one worker
+iii worker stop # stop one worker
+iii worker restart # stop then start
+```
+
+
+ To call functions inside running workers (directly with `iii.trigger` / `iii trigger`, or by
+ binding them to events with optional condition gates), see [Triggers](/using-iii/triggers).
+
+
+## Inspecting a worker
+
+To check a specific worker's state, follow its logs, or run a command inside the worker's sandbox,
+use:
+
+```bash
+iii worker status # config, sandbox state, recent logs
+iii worker logs # stream the worker's logs
+iii worker exec -- # run a command inside the worker
+```
+
+## Worker skills
+
+Every worker also ships with skills for Agentic work. Skills are managed by the `skills` worker, an
+actively developed content-registry worker added to a project like any other.
+
+Skill bodies load lazily. Top-level entries stay small; agents fetch deeper content via
+`iii:///` section URIs only when a function reference resolves to one.
+
+We ship high level skills as well which make it possible for any agent to make immediate use of iii
+and its workers.
+
+{/* TODO: Re-document this section against the skills worker's stable API. The current text reflects v0.2.4 at https://workers.iii.dev/workers/skills, and the surface may change before stable release. */}
+
+## Available functions and triggers
+
+Functions and triggers come from connected workers. To use a trigger of a given type, you need the
+worker that provides it to be connected. For example if you add `http` triggers via the iii-http
+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:
+
+```bash
+iii worker add iii-state@1.2.0
+```
+
+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.
+
+## 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
+```
+
+## 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
+workers can pin per-platform artifacts (macOS, Linux, Windows) in the same lockfile.
+
+Commit `iii.lock` alongside `config.yaml` for reproducible installs. Two commands operate on the
+lockfile directly:
+
+```bash
+iii worker sync # install workers exactly from iii.lock
+iii worker sync --frozen # CI form: verify the lockfile without mutating local files
+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`.
+
+{/* 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).
diff --git a/docs/0-13-0/using-iii/workers.mdx.skill.md b/docs/0-13-0/using-iii/workers.mdx.skill.md
new file mode 100644
index 000000000..74ac404df
--- /dev/null
+++ b/docs/0-13-0/using-iii/workers.mdx.skill.md
@@ -0,0 +1,165 @@
+
+
+# Workers
+
+
+{/* TODO: Re-link worker references to https://workers.iii.dev/workers/ once the Worker Docs migration ships. */}
+
+{/* TODO: Add back once worker init is merged. This page covers using existing workers. For creating new workers (`iii worker init`), registering functions and triggers, and building or publishing worker images, see [Creating Workers / Workers](/creating-workers/workers). */}
+
+## Worker lifecycle
+
+Workers connect to iii over WebSocket. When a worker connects it becomes visible to the entire iii
+system and every other worker within it. When a worker disconnects, its functions and triggers stop
+being callable until it reconnects.
+
+
+ For the SDK calls that establish the connection from worker code, see [Creating Workers /
+ Workers](/creating-workers/workers#connecting-to-the-engine).
+
+
+## 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
+
+
+ You need iii [installed](/install) and [running](/using-iii/engine) before adding a worker. To
+ spin up a temporary iii instance for testing, run `iii --use-default-config` (see [Default
+ configuration](/using-iii/engine#default-configuration)).
+
+
+`iii worker add ` installs a worker into your project:
+
+```bash
+iii worker add iii-state
+```
+
+The worker is added to `config.yaml` and started automatically. To force a redownload of an existing
+worker, use `iii worker reinstall ` (equivalent to `add --force`).
+
+
+ For local paths, Docker / OCI images, and version pinning, see [Worker Registry / Adding a
+ worker](./workers-registry#adding-a-worker).
+
+
+## Listing workers
+
+`iii worker list` shows every worker declared in your project's `config.yaml` along with its current
+status:
+
+```bash
+iii worker list
+```
+
+## Starting and stopping workers
+
+Added workers start automatically with the engine. To control them manually, use the `start`,
+`stop`, and `restart` commands:
+
+```bash
+iii worker start # start one worker
+iii worker stop # stop one worker
+iii worker restart # stop then start
+```
+
+
+ To call functions inside running workers (directly with `iii.trigger` / `iii trigger`, or by
+ binding them to events with optional condition gates), see [Triggers](/using-iii/triggers).
+
+
+## Inspecting a worker
+
+To check a specific worker's state, follow its logs, or run a command inside the worker's sandbox,
+use:
+
+```bash
+iii worker status # config, sandbox state, recent logs
+iii worker logs # stream the worker's logs
+iii worker exec -- # run a command inside the worker
+```
+
+## Worker skills
+
+Every worker also ships with skills for Agentic work. Skills are managed by the `skills` worker, an
+actively developed content-registry worker added to a project like any other.
+
+Skill bodies load lazily. Top-level entries stay small; agents fetch deeper content via
+`iii:///` section URIs only when a function reference resolves to one.
+
+We ship high level skills as well which make it possible for any agent to make immediate use of iii
+and its workers.
+
+{/* TODO: Re-document this section against the skills worker's stable API. The current text reflects v0.2.4 at https://workers.iii.dev/workers/skills, and the surface may change before stable release. */}
+
+## Available functions and triggers
+
+Functions and triggers come from connected workers. To use a trigger of a given type, you need the
+worker that provides it to be connected. For example if you add `http` triggers via the iii-http
+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:
+
+```bash
+iii worker add iii-state@1.2.0
+```
+
+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.
+
+## 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
+```
+
+## 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
+workers can pin per-platform artifacts (macOS, Linux, Windows) in the same lockfile.
+
+Commit `iii.lock` alongside `config.yaml` for reproducible installs. Two commands operate on the
+lockfile directly:
+
+```bash
+iii worker sync # install workers exactly from iii.lock
+iii worker sync --frozen # CI form: verify the lockfile without mutating local files
+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`.
+
+{/* 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).
diff --git a/docs/changelog/0-16-0/align-rust-register-function-with-signature.mdx b/docs/changelog/0-16-0/align-rust-register-function-with-signature.mdx
new file mode 100644
index 000000000..598ef09a6
--- /dev/null
+++ b/docs/changelog/0-16-0/align-rust-register-function-with-signature.mdx
@@ -0,0 +1,117 @@
+---
+title: 'Single register_function entry point in the Rust SDK'
+description: 'register_function takes (id, RegisterFunction) with three constructors (new, new_async, http). register_function_with, the tuple form, untyped, IntoFunctionRegistration, IntoFunctionHandler, RegisterFunctionOptions, iii_fn, iii_async_fn, IIIFn, IIIAsyncFn are removed. Handler error type is fixed to IIIError.'
+---
+
+## What changed
+
+The Rust SDK's function registration is collapsed into a single entry point that mirrors Node and Python:
+
+| SDK | Signature |
+| --- | --- |
+| Node | `registerFunction(functionId, handler, options?)` |
+| Python | `register_function(function_id, handler_or_invocation, *, description, metadata, request_format, response_format)` |
+| Rust | `register_function(id, RegisterFunction)` |
+
+`RegisterFunction` carries the handler plus all optional metadata. There are **three** constructors:
+
+```rust
+// Sync — schemas auto-extracted from the function's argument and return types.
+// Accepts both typed handlers and `Fn(Value) -> Result`.
+iii.register_function("greet", RegisterFunction::new(greet));
+
+// Async — same as `new` but for async fns. Same dual support.
+iii.register_function(
+ "http::fetch",
+ RegisterFunction::new_async(fetch).description("Fetches a URL"),
+);
+iii.register_function(
+ "echo",
+ RegisterFunction::new_async(|input: Value| async move { Ok(input) }),
+);
+
+// HTTP-invoked function (Lambda, Cloudflare Workers, etc.) — no local handler.
+iii.register_function(
+ "ext::lambda",
+ RegisterFunction::http(http_config).description("Proxies to a Lambda"),
+);
+```
+
+`Value` implements `JsonSchema` (via `schemars`), so untyped handlers go through `new` / `new_async` and emit a permissive `AnyValue` schema. No separate `untyped` constructor is needed.
+
+Builder methods (chainable, all consume `self`):
+
+| Method | Effect |
+| --- | --- |
+| `.description(s)` | Set the human-readable description. |
+| `.metadata(value)` | Attach arbitrary metadata. |
+| `.request_format(schema)` | Override the auto-extracted request schema. |
+| `.response_format(schema)` | Override the auto-extracted response schema. |
+
+`RegisterFunction::http` does no schema introspection, so the format setters are the only way to attach a schema for HTTP-invoked functions.
+
+## Handler error type fixed to `IIIError`
+
+Previously, sync/async handler bounds were `F: Fn(T) -> Result where E: Display` — the error type was generic. To enable clean type inference for `Fn(Value) -> ...` closures (where `Ok(...)` alone left `E` unbound), the bound is now `F: Fn(T) -> Result`.
+
+To smooth migration, `IIIError` now implements `From` and `From<&str>`. Existing handlers returning `Result` need to:
+
+1. Update the return type to `Result`.
+2. Either return `IIIError::Handler(s)` directly, or use `?`-propagation: `Err::<_, IIIError>("oops".into())` works, as does `Err("oops".to_string())?` once the function returns `Result<_, IIIError>`.
+
+```rust
+// Before
+fn greet(input: GreetInput) -> Result {
+ Ok(format!("Hello, {}!", input.name))
+}
+
+// After
+fn greet(input: GreetInput) -> Result {
+ Ok(format!("Hello, {}!", input.name))
+}
+```
+
+## Removed
+
+| Removed | Replacement |
+| --- | --- |
+| `III::register_function_with(id, handler, options)` | `III::register_function(id, RegisterFunction::new(handler).description(...))` for sync, or `RegisterFunction::new_async(handler)` for async |
+| `register_function((RegisterFunctionMessage, handler))` tuple form | `register_function(id, RegisterFunction::new(handler))` for sync, or `RegisterFunction::new_async(handler)` for async |
+| `RegisterFunction::untyped(f)` | `RegisterFunction::new_async(f)` (now accepts `Value` closures directly) |
+| `RegisterFunctionOptions` | Builder methods on `RegisterFunction` |
+| `IntoFunctionRegistration` trait | Single concrete `RegisterFunction` argument |
+| `IntoFunctionHandler` trait + impls | Each registration kind has its own `RegisterFunction::*` constructor |
+| `iii_fn`, `iii_async_fn`, `IIIFn`, `IIIAsyncFn` | `RegisterFunction::new` / `RegisterFunction::new_async` |
+| Handler bound `E: Display` | Bound is `Result<_, IIIError>`; use `From` / `From<&str>` to lift |
+
+`RegisterFunctionMessage::with_id` / `with_description` remain on the wire-protocol type but are not used in the public registration API.
+
+## Why
+
+Two BREAKING reshapes shipped together:
+
+1. The previous changelog entry already moved `register_function_with` to `(id, handler, options)` to match Node and Python. While doing the consumer migration, the redundancy between `register_function`, `register_function_with`, and the tuple form became evident: three call shapes, one trait machinery, two helper structs (`IIIFn` / `IIIAsyncFn`), and two traits (`IntoFunctionHandler`, `IntoFunctionRegistration`) — all serving roughly the same purpose with different ergonomics.
+
+2. With a single entry point, a single registration type, and three constructors (`new`, `new_async`, `http`), the surface area becomes one method and one builder. Schemars' `JsonSchema for Value` impl lets `new` / `new_async` cover both typed and `Value` handlers without a separate `untyped` escape hatch.
+
+The cost: every existing call site changes shape and `Result` handlers migrate to `Result`. The benefit: documentation, IDE completion, and cross-SDK familiarity all converge on a single pattern.
+
+## Migration
+
+Replace each registration call with the matching `RegisterFunction::*` constructor.
+
+| Before | After |
+| --- | --- |
+| `register_function(RegisterFunction::new("id", f))` | `register_function("id", RegisterFunction::new(f))` |
+| `register_function(RegisterFunction::new_async("id", f).description("d"))` | `register_function("id", RegisterFunction::new_async(f).description("d"))` |
+| `register_function((RegisterFunctionMessage::with_id("id".into()), sync_handler))` | `register_function("id", RegisterFunction::new(sync_handler))` |
+| `register_function((RegisterFunctionMessage::with_id("id".into()), async_handler))` | `register_function("id", RegisterFunction::new_async(async_handler))` |
+| `register_function_with(RegisterFunctionMessage::with_id("id".into()), http_config)` | `register_function("id", RegisterFunction::http(http_config))` |
+| `register_function_with("id", http_config, RegisterFunctionOptions::default())` | `register_function("id", RegisterFunction::http(http_config))` |
+| `register_function_with("id", sync_handler, RegisterFunctionOptions { description: Some("d".into()), ..Default::default() })` | `register_function("id", RegisterFunction::new(sync_handler).description("d"))` |
+| `register_function_with("id", async_handler, RegisterFunctionOptions { description: Some("d".into()), ..Default::default() })` | `register_function("id", RegisterFunction::new_async(async_handler).description("d"))` |
+| `RegisterFunction::untyped(handler)` | `RegisterFunction::new_async(handler)` |
+| `iii_fn(f)` / `iii_async_fn(f)` | `RegisterFunction::new(f)` / `RegisterFunction::new_async(f)` |
+| `fn handler(...) -> Result` | `fn handler(...) -> Result` |
+
+`RegisterFunction` is exported from the crate root: `use iii_sdk::RegisterFunction;`. `RegisterFunctionOptions` is no longer exported — drop the import.
diff --git a/docs/changelog/index.mdx b/docs/changelog/index.mdx
index ed3c9d49c..f61620d9c 100644
--- a/docs/changelog/index.mdx
+++ b/docs/changelog/index.mdx
@@ -5,6 +5,78 @@ owner: "devrel"
type: "reference"
---
+
+ ## Single `register_function` entry point in the Rust SDK
+
+ **Breaking.** The Rust SDK's function registration is collapsed into a single entry point that mirrors Node and Python:
+
+ ```rust
+ iii.register_function("greet", RegisterFunction::new(greet));
+ iii.register_function(
+ "http::fetch",
+ RegisterFunction::new_async(fetch).description("Fetches a URL"),
+ );
+ iii.register_function(
+ "ext::lambda",
+ RegisterFunction::http(http_config),
+ );
+ ```
+
+ `RegisterFunction` carries the handler plus all optional metadata. There are three constructors — `new`, `new_async`, `http` — and `Value` is accepted by `new` / `new_async`, so no separate `untyped` constructor is needed. `register_function_with`, the tuple form, `untyped`, `IntoFunctionRegistration`, `IntoFunctionHandler`, `RegisterFunctionOptions`, `iii_fn`, `iii_async_fn`, `IIIFn`, and `IIIAsyncFn` are removed.
+
+ Handler error type is fixed to `IIIError`. `IIIError` now implements `From` / `From<&str>` so existing `Result` handlers can migrate by updating the return type and relying on `?`-propagation.
+
+ See [the migration entry](/changelog/0-16-0/align-rust-register-function-with-signature) for the full before/after diff, builder methods, and step-by-step migration.
+
+ ## `Logger` and OpenTelemetry primitives moved to `iii-observability`
+
+ The `Logger`, `OtelConfig`, `ReconnectionConfig` (OTel variant), and the full OTel surface (`init_otel` / `shutdown_otel` / `flush_otel` / `with_span` / `execute_traced_request`, baggage and traceparent helpers, `current_span_id` / `current_trace_id`, span ops, payload redaction, `BaggageSpanProcessor`) now ship from a new shared package in every supported language:
+
+ | Language | Package | Import |
+ |---|---|---|
+ | Node | `@iii-dev/observability` (npm) | `import { Logger, initOtel, withSpan, executeTracedRequest } from '@iii-dev/observability'` |
+ | Python | `iii-observability` (PyPI) | `from iii_observability import Logger, init_otel, with_span, execute_traced_request` |
+ | Rust | `iii-observability` (crates.io)| `use iii_observability::{Logger, init_otel, with_span, execute_traced_request};` |
+
+ This isolates telemetry concerns from the SDK transport so workers that don't need OTel pull a smaller dependency set, and so the surface stays consistent across languages.
+
+ Two helpers that previously only existed in the Rust SDK are now available in Node and Python as well:
+
+ - `flush_otel` / `flushOtel` — force-flushes every provider without tearing OTel down. Use it before short-lived process exits where you still need pending spans, metrics, and logs delivered.
+ - `execute_traced_request` / `executeTracedRequest` — wraps an outgoing HTTP call (httpx in Python, `fetch` in Node) in an OTel `CLIENT` span. Injects W3C traceparent, records HTTP semantic-convention attributes, sets `ERROR` status on `>= 400` responses, and records exceptions on network errors.
+
+ ### Migration
+
+ Python and Rust continue to re-export the moved symbols from the SDK package for back-compat. Node removes the `iii-sdk/telemetry` subpath entry point — the named exports from `iii-sdk` itself stay, so `import { Logger } from 'iii-sdk'` keeps working. Direct imports from the new packages are preferred:
+
+ ```typescript
+ // Before (Node)
+ import { Logger, initOtel, withSpan } from 'iii-sdk'
+
+ // After (Node)
+ import { Logger, initOtel, withSpan } from '@iii-dev/observability'
+ ```
+
+ ```python
+ # Before (Python)
+ from iii import Logger
+ from iii.telemetry import init_otel, with_span
+
+ # After (Python)
+ from iii_observability import Logger, init_otel, with_span
+ ```
+
+ ```rust
+ // Before (Rust)
+ use iii_sdk::{Logger, OtelConfig, init_otel, with_span, execute_traced_request};
+
+ // After (Rust)
+ use iii_observability::{Logger, OtelConfig, init_otel, with_span, execute_traced_request};
+ ```
+
+ The new packages publish in lock-step with the rest of the monorepo on the same `iii/v*` release tag, so versions stay aligned with `iii-sdk`.
+
+
## `sandbox::run` — one call from zero to result
diff --git a/docs/docs.json b/docs/docs.json
index 801c08d16..6570c2c63 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -61,156 +61,64 @@
"navigation": {
"versions": [
{
- "version": "0.11.x",
+ "version": "0.16.x",
"tag": "Latest",
- "default": true,
"tabs": [
{
- "tab": "Documentation",
+ "tab": "Docs",
"groups": [
{
"group": "Getting Started",
- "pages": ["0-11-0/index", "0-11-0/install", "0-11-0/quickstart"]
- },
- {
- "group": "Primitives & Concepts",
- "pages": [
- "0-11-0/primitives-and-concepts/functions-triggers-workers",
- "0-11-0/primitives-and-concepts/discovery"
- ]
- },
- {
- "group": "How-to Guides",
- "pages": [
- "0-11-0/how-to/use-functions-and-triggers",
- {
- "group": "Functions",
- "pages": [
- "0-11-0/how-to/define-request-response-formats",
- "0-11-0/how-to/create-ephemeral-worker",
- "0-11-0/how-to/schedule-cron-task"
- ]
- },
- {
- "group": "Triggers",
- "pages": [
- "0-11-0/how-to/trigger-actions",
- "0-11-0/how-to/trigger-functions-from-cli",
- "0-11-0/how-to/use-trigger-conditions",
- "0-11-0/how-to/create-custom-trigger-type"
- ]
- },
- {
- "group": "Workers",
- "pages": [
- "0-11-0/how-to/worker-rbac",
- "0-11-0/how-to/managing-container-workers",
- "0-11-0/how-to/reproduce-worker-installs",
- "0-11-0/how-to/developing-sandbox-workers"
- ]
- },
- {
- "group": "Console & Observability",
- "pages": ["0-11-0/how-to/use-console", "0-11-0/how-to/observability-and-logs"]
- },
- "0-11-0/how-to/use-iii-in-the-browser",
- "0-11-0/how-to/configure-engine",
- {
- "group": "HTTP",
- "pages": [
- "0-11-0/how-to/expose-http-endpoint",
- "0-11-0/how-to/use-http-middleware"
- ]
- },
- {
- "group": "Queues",
- "pages": [
- "0-11-0/how-to/use-named-queues",
- "0-11-0/how-to/use-topic-queues",
- "0-11-0/how-to/dead-letter-queues"
- ]
- },
- {
- "group": "State & Data",
- "pages": [
- "0-11-0/how-to/manage-state",
- "0-11-0/how-to/react-to-state-changes",
- "0-11-0/how-to/use-channels",
- "0-11-0/how-to/stream-realtime-data"
- ]
- }
- ]
- },
- {
- "group": "Architecture",
- "pages": [
- "0-11-0/architecture/index",
- "0-11-0/architecture/engine",
- "0-11-0/architecture/external-workers",
- "0-11-0/architecture/workers",
- "0-11-0/architecture/trigger-types",
- "0-11-0/architecture/channels",
- "0-11-0/architecture/queues"
- ]
+ "pages": ["index", "install", "quickstart"]
},
{
- "group": "Workers",
+ "group": "Using iii",
"pages": [
- "0-11-0/workers/index",
- "0-11-0/workers/iii-http",
- "0-11-0/workers/iii-queue",
- "0-11-0/workers/iii-cron",
- "0-11-0/workers/iii-observability",
- "0-11-0/workers/iii-stream",
- "0-11-0/workers/iii-state",
- "0-11-0/workers/iii-pubsub",
- "0-11-0/workers/iii-exec",
- "0-11-0/workers/iii-bridge",
- "0-11-0/workers/iii-worker-manager",
- "0-11-0/workers/managed-worker-lockfile"
+ "using-iii/workers",
+ "using-iii/workers-registry",
+ "using-iii/triggers",
+ "using-iii/functions",
+ "using-iii/engine",
+ "using-iii/console",
+ "using-iii/cli",
+ "using-iii/deployment"
]
},
{
- "group": "Console",
- "pages": ["0-11-0/console/index"]
- },
- {
- "group": "Advanced",
+ "group": "Understanding iii",
+ "expanded": false,
"pages": [
- "0-11-0/advanced/adapters",
- "0-11-0/advanced/architecture",
- "0-11-0/advanced/custom-modules",
- "0-11-0/advanced/deployment",
- "0-11-0/advanced/protocol",
- "0-11-0/advanced/telemetry"
+ "understanding-iii/index",
+ "understanding-iii/engine",
+ "understanding-iii/channels"
]
},
{
- "group": "Examples",
+ "group": "Creating Workers",
"pages": [
- "0-11-0/examples/hello-world",
- "0-11-0/examples/todo-app",
- "0-11-0/examples/multi-trigger",
- "0-11-0/examples/conditions",
- "0-11-0/examples/state-management",
- "0-11-0/examples/cron",
- "0-11-0/examples/observability"
+ "creating-workers/index",
+ "creating-workers/workers",
+ "creating-workers/triggers",
+ "creating-workers/functions",
+ "creating-workers/channels"
]
}
]
},
{
- "tab": "API Reference",
+ "tab": "SDK & Engine Reference",
"groups": [
{
- "group": "SDKs",
+ "group": "Engine protocol",
+ "pages": ["sdk-reference/engine-sdk"]
+ },
+ {
+ "group": "Language SDKs",
"pages": [
- "0-11-0/api-reference/sdk-node",
- "0-11-0/api-reference/sdk-browser",
- "0-11-0/api-reference/sdk-python",
- "0-11-0/api-reference/sdk-rust",
- "0-11-0/api-reference/sandbox",
- "0-11-0/api-reference/disable-telemetry"
+ "sdk-reference/node-sdk",
+ "sdk-reference/python-sdk",
+ "sdk-reference/rust-sdk",
+ "sdk-reference/browser-sdk"
]
}
]
@@ -220,62 +128,68 @@
"groups": [
{
"group": "Changelog",
- "pages": ["0-11-0/changelog"]
+ "pages": ["changelog/index"]
+ },
+ {
+ "group": "0.16.0",
+ "pages": ["changelog/0-16-0/align-rust-register-function-with-signature"]
},
{
"group": "0.11.0",
"pages": [
- "0-11-0/changelog/0-11-0/everything-is-a-worker",
- "0-11-0/changelog/0-11-0/migrating-from-motia-js",
- "0-11-0/changelog/0-11-0/migrating-from-motia-py",
- "0-11-0/changelog/0-11-0/migrated-examples"
+ "changelog/0-11-0/everything-is-a-worker",
+ "changelog/0-11-0/migrating-from-motia-js",
+ "changelog/0-11-0/migrating-from-motia-py",
+ "changelog/0-11-0/migrated-examples"
]
}
]
}
]
},
+
{
"version": "0.13.x",
- "tag": "Next",
"tabs": [
{
"tab": "Docs",
"groups": [
{
"group": "Getting Started",
- "pages": ["index", "install", "quickstart"]
+ "pages": ["0-13-0/index", "0-13-0/install", "0-13-0/quickstart"]
},
{
"group": "Using iii",
"pages": [
- "using-iii/workers",
- "using-iii/workers-registry",
- "using-iii/triggers",
- "using-iii/functions",
- "using-iii/engine",
- "using-iii/console",
- "using-iii/cli",
- "using-iii/deployment"
+ "0-13-0/using-iii/workers",
+ "0-13-0/using-iii/workers-registry",
+ "0-13-0/using-iii/triggers",
+ "0-13-0/using-iii/functions",
+ "0-13-0/using-iii/channels",
+ "0-13-0/using-iii/engine",
+ "0-13-0/using-iii/console",
+ "0-13-0/using-iii/cli",
+ "0-13-0/using-iii/deployment"
]
},
{
"group": "Understanding iii",
"expanded": false,
"pages": [
- "understanding-iii/index",
- "understanding-iii/engine",
- "understanding-iii/channels"
+ "0-13-0/understanding-iii/index",
+ "0-13-0/understanding-iii/workers",
+ "0-13-0/understanding-iii/functions",
+ "0-13-0/understanding-iii/triggers",
+ "0-13-0/understanding-iii/channels",
+ "0-13-0/understanding-iii/engine"
]
},
{
"group": "Creating Workers",
"pages": [
- "creating-workers/index",
- "creating-workers/workers",
- "creating-workers/triggers",
- "creating-workers/functions",
- "creating-workers/channels"
+ "0-13-0/creating-workers/workers",
+ "0-13-0/creating-workers/functions",
+ "0-13-0/creating-workers/triggers"
]
}
]
@@ -285,15 +199,15 @@
"groups": [
{
"group": "Engine protocol",
- "pages": ["sdk-reference/engine-sdk"]
+ "pages": ["0-13-0/sdk-reference/engine-sdk"]
},
{
"group": "Language SDKs",
"pages": [
- "sdk-reference/node-sdk",
- "sdk-reference/python-sdk",
- "sdk-reference/rust-sdk",
- "sdk-reference/browser-sdk"
+ "0-13-0/sdk-reference/node-sdk",
+ "0-13-0/sdk-reference/python-sdk",
+ "0-13-0/sdk-reference/rust-sdk",
+ "0-13-0/sdk-reference/browser-sdk"
]
}
]
@@ -303,15 +217,15 @@
"groups": [
{
"group": "Changelog",
- "pages": ["changelog/index"]
+ "pages": ["0-13-0/changelog/index"]
},
{
"group": "0.11.0",
"pages": [
- "changelog/0-11-0/everything-is-a-worker",
- "changelog/0-11-0/migrating-from-motia-js",
- "changelog/0-11-0/migrating-from-motia-py",
- "changelog/0-11-0/migrated-examples"
+ "0-13-0/changelog/0-11-0/everything-is-a-worker",
+ "0-13-0/changelog/0-11-0/migrating-from-motia-js",
+ "0-13-0/changelog/0-11-0/migrating-from-motia-py",
+ "0-13-0/changelog/0-11-0/migrated-examples"
]
}
]
@@ -404,6 +318,179 @@
]
},
+ {
+ "version": "0.11.x",
+ "tabs": [
+ {
+ "tab": "Documentation",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": ["0-11-0/index", "0-11-0/install", "0-11-0/quickstart"]
+ },
+ {
+ "group": "Primitives & Concepts",
+ "pages": [
+ "0-11-0/primitives-and-concepts/functions-triggers-workers",
+ "0-11-0/primitives-and-concepts/discovery"
+ ]
+ },
+ {
+ "group": "How-to Guides",
+ "pages": [
+ "0-11-0/how-to/use-functions-and-triggers",
+ {
+ "group": "Functions",
+ "pages": [
+ "0-11-0/how-to/define-request-response-formats",
+ "0-11-0/how-to/create-ephemeral-worker",
+ "0-11-0/how-to/schedule-cron-task"
+ ]
+ },
+ {
+ "group": "Triggers",
+ "pages": [
+ "0-11-0/how-to/trigger-actions",
+ "0-11-0/how-to/trigger-functions-from-cli",
+ "0-11-0/how-to/use-trigger-conditions",
+ "0-11-0/how-to/create-custom-trigger-type"
+ ]
+ },
+ {
+ "group": "Workers",
+ "pages": [
+ "0-11-0/how-to/worker-rbac",
+ "0-11-0/how-to/managing-container-workers",
+ "0-11-0/how-to/reproduce-worker-installs",
+ "0-11-0/how-to/developing-sandbox-workers"
+ ]
+ },
+ {
+ "group": "Console & Observability",
+ "pages": ["0-11-0/how-to/use-console", "0-11-0/how-to/observability-and-logs"]
+ },
+ "0-11-0/how-to/use-iii-in-the-browser",
+ "0-11-0/how-to/configure-engine",
+ {
+ "group": "HTTP",
+ "pages": [
+ "0-11-0/how-to/expose-http-endpoint",
+ "0-11-0/how-to/use-http-middleware"
+ ]
+ },
+ {
+ "group": "Queues",
+ "pages": [
+ "0-11-0/how-to/use-named-queues",
+ "0-11-0/how-to/use-topic-queues",
+ "0-11-0/how-to/dead-letter-queues"
+ ]
+ },
+ {
+ "group": "State & Data",
+ "pages": [
+ "0-11-0/how-to/manage-state",
+ "0-11-0/how-to/react-to-state-changes",
+ "0-11-0/how-to/use-channels",
+ "0-11-0/how-to/stream-realtime-data"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Architecture",
+ "pages": [
+ "0-11-0/architecture/index",
+ "0-11-0/architecture/engine",
+ "0-11-0/architecture/external-workers",
+ "0-11-0/architecture/workers",
+ "0-11-0/architecture/trigger-types",
+ "0-11-0/architecture/channels",
+ "0-11-0/architecture/queues"
+ ]
+ },
+ {
+ "group": "Workers",
+ "pages": [
+ "0-11-0/workers/index",
+ "0-11-0/workers/iii-http",
+ "0-11-0/workers/iii-queue",
+ "0-11-0/workers/iii-cron",
+ "0-11-0/workers/iii-observability",
+ "0-11-0/workers/iii-stream",
+ "0-11-0/workers/iii-state",
+ "0-11-0/workers/iii-pubsub",
+ "0-11-0/workers/iii-exec",
+ "0-11-0/workers/iii-bridge",
+ "0-11-0/workers/iii-worker-manager",
+ "0-11-0/workers/managed-worker-lockfile"
+ ]
+ },
+ {
+ "group": "Console",
+ "pages": ["0-11-0/console/index"]
+ },
+ {
+ "group": "Advanced",
+ "pages": [
+ "0-11-0/advanced/adapters",
+ "0-11-0/advanced/architecture",
+ "0-11-0/advanced/custom-modules",
+ "0-11-0/advanced/deployment",
+ "0-11-0/advanced/protocol",
+ "0-11-0/advanced/telemetry"
+ ]
+ },
+ {
+ "group": "Examples",
+ "pages": [
+ "0-11-0/examples/hello-world",
+ "0-11-0/examples/todo-app",
+ "0-11-0/examples/multi-trigger",
+ "0-11-0/examples/conditions",
+ "0-11-0/examples/state-management",
+ "0-11-0/examples/cron",
+ "0-11-0/examples/observability"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "API Reference",
+ "groups": [
+ {
+ "group": "SDKs",
+ "pages": [
+ "0-11-0/api-reference/sdk-node",
+ "0-11-0/api-reference/sdk-browser",
+ "0-11-0/api-reference/sdk-python",
+ "0-11-0/api-reference/sdk-rust",
+ "0-11-0/api-reference/sandbox",
+ "0-11-0/api-reference/disable-telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Changelog",
+ "groups": [
+ {
+ "group": "Changelog",
+ "pages": ["0-11-0/changelog"]
+ },
+ {
+ "group": "0.11.0",
+ "pages": [
+ "0-11-0/changelog/0-11-0/everything-is-a-worker",
+ "0-11-0/changelog/0-11-0/migrating-from-motia-js",
+ "0-11-0/changelog/0-11-0/migrating-from-motia-py",
+ "0-11-0/changelog/0-11-0/migrated-examples"
+ ]
+ }
+ ]
+ }
+ ]
+ },
{
"version": "0.10.x",
"tabs": [