> ## 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.

# Workers, Functions, and Observers

> A walkthrough of the four pieces that make up every iii system (Workers, Functions, Observers, and the Engine), using the Quickstart tutorial as an example.

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.

> [!NOTE]
>
> This page uses the [Quickstart tutorial](https://iii.dev/docs/0-25-0/quickstart.md) 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](https://iii.dev/docs/0-25-0/using-iii/workers.md)
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:

```mermaid theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
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["state"]
  Engine <--> HttpWorker["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

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.

> [!NOTE]
>
> For the connection lifecycle from worker code, see [Creating Workers /
> Workers](https://iii.dev/docs/0-25-0/creating-workers/workers.md#worker-lifecycle).

## 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.

> [!NOTE]
>
> 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](https://iii.dev/docs/0-25-0/upgrading/from-0-24-x.md#providers-not-migrated-yet).

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`:

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
worker.observe({ function_id: "math::add" }, "audit::record");
```

`math::add` does not change. For the full model, see [Observers](https://iii.dev/docs/0-25-0/using-iii/observers.md). To make
your own Functions observable, see [Observers](https://iii.dev/docs/0-25-0/creating-workers/observable-functions.md) 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.

> [!NOTE]
>
> See [Engine](https://iii.dev/docs/0-25-0/understanding-iii/engine.md) for startup flow, config hot-reload, and the live registry and discovery
> surface.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.