Skip to main content

Documentation Index

Fetch the complete documentation index at: https://iii.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

0.13.0
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.
# 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:
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:
{
  "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:
SymbolRust (iii::*)Node (iii-sdk/telemetry)Python (iii.telemetry / iii.logger)Browser
get_tracer / getTracerdropped (still at iii::telemetry::get_tracer)droppedrenamed _get_traceralready absent (asserted)
get_meter / getMeterdropped (still at iii::telemetry::get_meter)droppedrenamed _get_meteralready absent (asserted)
is_initializeddropped (still at iii::telemetry::is_initialized)n/arenamed _is_initializedalready absent (asserted)
SpanKinddropped (use opentelemetry::trace::SpanKind)n/an/aalready absent (asserted)
SpanStatus / SpanStatusCodedropped (use opentelemetry::trace::Status)droppedn/aalready 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.
// 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')
// 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");
# 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")
0.12.0
May 2026

iii sandbox subcommand removed

Breaking. The iii sandbox CLI subcommand is gone. Every sandbox operation now goes through iii trigger:
# 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:
# 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 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>.
0.11.0
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 → · Python migration guide →

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 →

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 → · Trigger architecture →

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 →

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 → · 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 →

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 →

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:
// 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 →

Node SDK: registerFunction signature change

Breaking. The registerFunction API now takes the function ID as a plain string instead of an options object:
// 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 YAMLmodules: top-level key renamed to workers:, class: field renamed to name: with short identifiers.
  • Rust APIModule 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 →