Skip to main content
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, Observers run a Function when another Function has an event, 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 as an example.

The four pieces

This is a brief recap of the four pieces; the sections below expand each one with the Quickstart as an example. More details about their actual usage are in Using iii / Workers and the rest of the “Using iii” section.

Worker

A Worker is anything that connects to the Engine and registers 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.

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.

Observer

An Observer connects two Functions: a source and a target. Each time the source has an event, the Engine runs the target. By default, each successful call of the source is an event. A Worker creates an observer with worker.observe(source, target).

Engine

The Engine is the coordinator. It accepts worker connections, maintains a live registry of available Functions and observers, 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.
  2. 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 state and http Workers, an HTTP route at POST /math/add-two-numbers that calls math::add_two_numbers, and a key-value scope named math holding a running_total. The runtime topology looks like this: 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

Workers do the work 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. Once connected, those Functions are invocable from anywhere in the system without per-pair integration code between the caller and the Worker. A Worker can also observe Functions, so that its own Functions run when other Functions have events. 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 observers. 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 drop out of the routing table on disconnect, and every other Worker keeps serving. Its observers wait as suspended. If the Worker does not connect again in time, the Engine removes them.

In the Quickstart

Both Workers in the Quickstart fulfill the same contract: open a WebSocket connection to the Engine. Once connected they can register Functions, call other Functions with trigger(), and observe 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.
For the connection lifecycle from worker code, see Creating Workers / Workers.

Functions

A Function is a named handler inside a Worker. It takes a payload and returns a result. From the iii system’s perspective, a Function is identified by its name and addressable across language and location boundaries. Callers do not know what Worker is providing the Function, what language the handler is written in, or where the Worker is running. The Engine routes each invocation to a Worker that currently provides the target Function. A Function has no fixed shape beyond payload-in / result-out. Some Functions are pure computation. Some perform side effects (state writes, HTTP calls, queue enqueues). Some are agentic, invoking other Functions in turn. The Engine does not distinguish: routing is the same for all of them.

Function identifiers

Function identifiers use the service::name convention. The service segment groups related Functions together as a namespace, scope, or worker name. The name segment is the specific handler. Identifiers like math::add, state::get, and http::serve follow this convention. The convention is a recommendation, not a hard rule. Any string is a valid function ID at the engine level, but the service::name form makes the Function’s intent obvious to readers and avoids collisions between unrelated Functions registered by different Workers.

Calling a Function

Registering a Function with registerFunction() makes it callable through worker.trigger() from any connected Worker and through the iii trigger CLI command. The Engine looks up the function_id in its live registry, finds a Worker that currently provides the Function, and sends the invocation to it. The handler sees the payload alone. It does not see where the call came from.

Call actions

By default, a call waits until the Function returns its result or the timeout expires. Use this when the caller needs the value that the Function returns. With TriggerAction.Void(), the call returns at once and does not wait for a result. Use it for side effects that the caller does not need to wait for.
Workers can also define their own call actions. The queue Worker provides TriggerAction.Enqueue( {queue}), which routes the invocation through a named queue with retries.

In the Quickstart

The Quickstart calls Functions in three ways:
  1. The CLI command iii trigger math::add a=2 b=3 calls math::add. The Engine routes the invocation to math-worker, the Worker that provides math::add.
  2. math::add_two_numbers calls math::add with worker.trigger({ function_id: 'math::add', ... }). The Engine routes this call in the same way as the CLI call.
  3. The http Worker owns the HTTP socket. When a request arrives at POST /math/add-two-numbers, it calls http::add_two_numbers in caller-worker, which calls math::add_two_numbers. The response flows back the same way. The Functions never see an HTTP request. They see a payload, like every other call.
The Quickstart binds the HTTP route with worker.registerTrigger(). This API is deprecated in 0.25, and the http Worker still uses it for routes. See Upgrading from 0.24.x. 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.

Observers

An observer runs a target Function each time a source Function has an event. The source Function does not change, and it does not know which observers it has. Any Worker that is allowed to call the source can observe it. Observers replace triggers: registerTrigger and registerTriggerType are deprecated in 0.25.

Events

By default, each successful call of the source is an event. The target runs after the call completes, and it receives the call’s input, output, and caller. With on, an observer can also get the failed calls of the source, or all its calls. Filters on the source select which events run the target, for example only orders with a total of 500 or more.

Delivery

Most Functions use auto delivery: each call is an event. A Function can also use owner delivery. Then the Function’s own code decides which observers get each event, from the args that each observer gives. For example, a file watcher can watch one path for each observer. The Engine and the observability Worker also host source Functions for their own events, such as engine::on-functions-changed, engine::on-workers-changed, configuration::on-updated, and observability::on-log.

Observer lifecycle

An observer is pending until its source and target are ready, and then it is active. When the observing Worker disconnects, its observers become suspended. If the Worker connects again within observers.observer_grace_ms (30 s by default), it keeps its observers. If not, they become disposed. An observer that the Engine or the source rejects is error.

Chains and the loop guard

A call of a target is a normal call, so another observer can observe it. Each step adds 1 to the depth of the event. When the depth is more than observers.max_observe_depth (8 by default), the Engine drops the event. This stops observer loops.

In the Quickstart

The Quickstart does not create observers. As an example, a Worker can run audit::record after each successful call of math::add:
math::add does not change. For the full model, see Observers. To make your own Functions observable, see Observers in Creating Workers.

The Engine

The Engine is a single process that holds the registry of every connected Worker, every registered Function, and every observer. 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 knows that some Worker provides it. The same tutorial can be redeployed across different runtimes without touching the function code.
See Engine for startup flow, config hot-reload, and the live registry and discovery surface.