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 aservice::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 withworker.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:math-workeris a Python Worker that registersmath::add.caller-workeris a TypeScript Worker that registersmath::add_two_numbers, which callsmath::addthrough the Engine.
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 assuspended. 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 withtrigger(), 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 theservice::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 withregisterFunction() 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. WithTriggerAction.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:- The CLI command
iii trigger math::add a=2 b=3callsmath::add. The Engine routes the invocation tomath-worker, the Worker that providesmath::add. math::add_two_numberscallsmath::addwithworker.trigger({ function_id: 'math::add', ... }). The Engine routes this call in the same way as the CLI call.- The
httpWorker owns the HTTP socket. When a request arrives atPOST /math/add-two-numbers, it callshttp::add_two_numbersincaller-worker, which callsmath::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.
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. Withon, 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 theargs 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 ispending 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 thanobservers.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 runaudit::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 wheremath::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.