> ## Documentation Index
> Fetch the complete documentation index at: https://motiadev-add-real-system-tutorial-round-2.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Product updates and announcements for iii.

<Update label="0.19.0" description="June 2026">
  ## `engine::triggers::info` now exposes `response_schema`

  Trigger types can declare the schema a bound handler must **return** when the trigger fires. `engine::triggers::info` surfaces it as a new optional `response_schema` field alongside the existing `configuration_schema` (how to configure the trigger) and `request_schema` (what the handler receives) — the full trigger contract is now discoverable from a single call:

  ```json theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  {
    "id": "http",
    "configuration_schema": { "…": "route fields — path, method, middleware" },
    "request_schema": { "…": "envelope your handler receives" },
    "response_schema": {
      "properties": {
        "status_code": { "…": "HTTP status to send; defaults to 200 when omitted" },
        "headers": { "…": "{ \"Header-Name\": \"value\" } map or [\"Header-Name: value\"] strings" },
        "body": { "…": "serialized as JSON, text, or bytes per your Content-Type" }
      }
    },
    "instance_count": 1
  }
  ```

  The `http` trigger type is the first to declare a return contract: its `response_schema` is the response envelope the `iii-http` worker reads from a handler's return value — `status_code` / `headers` / `body`, every field optional. Previously, "what should my HTTP handler return" wasn't discoverable from the trigger itself: you had to inspect an already-bound handler via `engine::functions::info`, or guess field names (`status` vs `status_code` — it's `status_code`).

  Trigger types that place no constraint on the handler's return omit the field entirely, so existing consumers of `engine::triggers::info` are unaffected. In-process (Rust) trigger types can declare their own contract with the new `TriggerType::with_call_response_format::<T>()` builder.

  ## SDK: inbound `unregistertrigger` for custom trigger types

  When a trigger instance is removed — via `trigger.unregister()` or because the subscribing worker disconnects — the engine notifies the worker that owns the trigger type so it can run `unregisterTrigger` and tear down listeners, routes, or subscriptions.

  Node, Browser, Python, and Rust SDKs already handled inbound `registertrigger`; they now handle inbound `unregistertrigger` the same way. Custom trigger type providers (`registerTriggerType`) receive the binding `id` (and can look up stored config from their own registry keyed by that id).

  **Built-in trigger types** (`http`, `cron`, `state`, `subscribe`, `durable:subscriber`, stream, and others) are unchanged: the engine calls each in-process worker’s `unregister_trigger` directly and never sends a WebSocket message to an SDK worker.

  ### What this fixes

  * Unregistering a trigger bound to a **custom** trigger type now invokes the provider’s `unregisterTrigger` callback instead of leaving stale bindings server-side.
  * When the **provider worker reconnects**, the engine re-sends `registertrigger` for existing bindings (unchanged); cleanup on consumer disconnect now correctly pairs with `unregisterTrigger` on the provider.
</Update>

<Update label="0.16.0" description="May 2026">
  ## 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  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<String>` / `From<&str>` so existing `Result<R, String>` handlers can migrate by updating the return type and relying on `?`-propagation.

  See [the migration entry](./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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // Before (Node)
  import { Logger, initOtel, withSpan } from 'iii-sdk'

  // After (Node)
  import { Logger, initOtel, withSpan } from '@iii-dev/observability'
  ```

  ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  # 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // 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`.
</Update>

<Update label="0.13.0" description="May 2026">
  ## `sandbox::run` — one call from zero to result

  A new meta-function composes `sandbox::create` + `sandbox::fs::write` + `sandbox::exec` + `sandbox::stop` into a single call. The classic four-step "create → write → exec → stop" dance drops to one. The sandbox is auto-stopped on both success and failure unless you pass `keep_sandbox: true`.

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  # before (4 calls)
  SB=$(iii trigger sandbox::create image=python | jq -r .sandbox_id)
  iii trigger sandbox::fs::write sandbox_id="$SB" path=/workspace/run.py content='print(2+2)'
  iii trigger sandbox::exec sandbox_id="$SB" cmd=python3 args='["/workspace/run.py"]'
  iii trigger sandbox::stop sandbox_id="$SB"

  # after (1 call)
  iii trigger sandbox::run --json '{"image":"python","code":"print(2+2)"}'
  ```

  ## `sandbox::catalog::list`

  A new function returns the daemon's image catalog — bundled presets plus operator-registered `custom_images` entries from `iii.config.yaml`. Closes the "what images are available on this host?" discovery loop without operator hand-off.

  ## `sandbox::exec` and `sandbox::create` accept more input shapes

  `sandbox::exec.cmd` now accepts three shapes:

  * `cmd` + `args` (classic POSIX)
  * `argv` array
  * shell-line `cmd` (shlex-split when `args` / `argv` are empty)

  `sandbox::exec.env` and `sandbox::create.env` accept **either** a `Vec<"K=V">` list **or** a `{ K: V }` map. Env-var names are pinned to `[A-Za-z_][A-Za-z0-9_]*`; digit-leading or `/`/`-`/`=` names are rejected as `S001`.

  ## `sandbox::fs::read` returns inline bodies for small text

  Additive: a new optional `body` field on the `sandbox::fs::read` response carries the file contents as a UTF-8 string for text files under 1 MiB that decode cleanly. The existing `content: StreamChannelRef` field is still always populated and still delivers the same bytes, so peers that statically type `content` as a stream ref keep working unchanged. New callers can short-circuit the channel subscription whenever `body` is present:

  ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  const { content, body } = await trigger({ function_id: 'sandbox::fs::read', payload: { sandbox_id, path } })
  const text = body ?? await readChannel(content) // prefer inline body, fall back to stream
  ```

  Cost: small text is buffered into the channel as well as the inline body so legacy subscribers still receive it. Bounded at 1 MiB per call.

  ## Structured `sandbox::*` errors with resubmittable `fix` payloads

  Every `sandbox::*` function now returns a structured envelope on failure:

  ```json theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  {
    "code": "S211",
    "type": "FsParentNotFound",
    "message": "parent directory /workspace/a/b does not exist",
    "docs_url": "https://github.com/iii-hq/iii/.../README.md#S211",
    "retryable": false,
    "fix": { "parents": true },
    "fix_note": "merge `fix` into the original request and resubmit: `parents: true` auto-creates missing intermediate directories"
  }
  ```

  * `docs_url` anchors directly at the in-repo `S`-code subsection. **Breaking:** the base URL flipped from `https://iii.dev/docs/errors/sandbox/Sxxx` to `https://github.com/iii-hq/iii/blob/main/crates/iii-worker/src/sandbox_daemon/README.md#Sxxx` while the canonical `iii.dev` error pages are still pending. Bookmarks and scrapers built on the old URL need to follow the new anchors.
  * `fix` is a non-null JSON payload the agent can merge into the original request and resubmit verbatim when recovery is unambiguous (parent-missing writes, `sandbox::run` sub-step failures, etc.).
  * `fix_note` describes how to use the fix or — when `fix` is `null` — explains why no auto-recovery exists.
  * `sandbox::run` sub-step failures surface the inner `S`-code transparently and name the failing step in `fix.context`, plus `fix.sandbox_id` when `keep_sandbox: true`.
  * FS error `message` strings now carry a kind prefix (e.g. `"file not found: {path}"` instead of bare `{path}`). The authoritative `code` / `type` fields are unchanged; only callers that grep the message text are affected.

  ## `sandbox::exec` default timeout raised to 5 minutes

  **Breaking.** The default `timeout_ms` for `sandbox::exec` moves from 30 s to 300 s. Sized for cold `npm install` / `pip install` / `cargo build`. Previously the 30 s default fired as an opaque engine-gate denial before the daemon could return a structured `timed_out: true` response. Callers that relied on the 30 s fast-fail to bound runaway commands should now set `timeout_ms` explicitly.

  ## Handler-boundary tracing on every `sandbox::*` handler

  Every `sandbox::*` handler emits a `tracing::info!` event on both success and error with a stable field set: `function_id`, `sandbox_id`, `success`, `error_code`, `error_type`, `retryable`, `duration_ms`. Operators can dashboard sandbox usage without grepping unstructured logs.

  ## 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  # 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")
  ```
</Update>

<Update label="0.12.0" description="May 2026">
  ## `iii sandbox` subcommand removed

  **Breaking.** The `iii sandbox` CLI subcommand is gone. Every sandbox operation now goes through `iii trigger`:

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  # 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 '<obj>'` 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 '<obj>'` flag, or both:

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  # 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 <target>` (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 <prior>`.
</Update>

<Update label="0.11.0" description="April 2026">
  ## 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 →](./0-11-0/migrating-from-motia-js) · [Python migration guide →](./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 <name> -- <cmd>` 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // 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 theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  // 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 →](./0-11-0/everything-is-a-worker)
</Update>
