Skip to main content
In 0.26, a trigger type is an observable function: registerTriggerType becomes registerFunction with the hooks on this page. To move a 0.25 worker, see Upgrading from 0.25.x to 0.26.x.
Every function your worker registers can be observed unless you specify otherwise (see Registration options). By default, iii runs each observer’s target after each successful call of the function, with what the observer captures: the call’s input, the handler’s return value, and the caller unless its capture says otherwise (the call’s metadata only when capture names it; see Observers). Your hooks receive the call’s metadata whatever the observers capture, so route on it in onObservationMatch or onObservationDeliver. This page describes how to manage this: what the observers of its functions receive, which calls reach which observer, what each observer sets up, and what the caller gets back. To observe a function, see Observers.

Hooks

Your worker defines function observation with four hooks, two are observer specific, and two are specific to each individual observation. Every hook is optional. Without it, iii’s default applies; setting it overrides that default for the function, or, set at the worker level, for every function the worker hosts. Each observer’s capture and filters still decide what its target receives, whichever hooks are set, unless onObservationDeliver runs the targets itself. Both observer lifecycle hooks can be set for one function or for every function the worker hosts, and both levels run. When an observer is added, the worker’s hook runs, then the function’s. When an observer is removed, the function’s hook runs, then the worker’s. For each function the SDK runs the observer lifecycle hooks one at a time, in the order the engine announced the observers; hooks of different functions run concurrently.

Observer lifecycle

These hooks run once per observer, when it starts and stops observing the function.

onObserverAdded(obs)

By default, every observer is accepted as soon as it is added. Setting this hook overrides that: it runs before the observer is accepted. Use it to set something up from the observer’s args. When it returns, the observer becomes active and receives the function’s calls. Throw to reject the observer instead. A hook that runs past 30 seconds (observers.owner_ack_timeout_ms) fails the observer with OWNER_TIMEOUT; a removed hook that runs past it is only logged.

onObserverRemoved(obs)

Runs after the engine has removed the observer and delivery to it has stopped. Use it to perform your own teardown. It runs once for each accepted observer, when the consumer disposes it, when max_fires or expiry ends it, when the observing worker disconnects, and after a late rejection. For an observer that is never accepted it runs only at the levels whose onObserverAdded resolved.
If onObserverRemoved throws an error it will cause the error to be logged but it will not prevent observer removal. Observer removal happens at the engine level.

Set a hook for one function or for every function

A hook set on the worker runs for every function the worker hosts. A hook set on one function runs for that function only.
For one function, pass its id to the same pair: The hook names in each SDK: The FunctionRef has the same methods with the id filled in: onObserverAdded(hook) and its siblings in Node, Python and Rust (on_observer_added(hook)), and OnObserverAdded(hook) in Go. In Node, Rust and Go each method returns its receiver, so calls chain. In Python each returns the hook, so it also works as a decorator, and on_observer_added("id") without a hook is a decorator factory. Added, removed and deliver hooks may be sync or async in Node and Python. A match predicate and key functions are sync, since they run on every call. In Rust a hook is an async closure or a type that implements AddedHook, RemovedHook or DeliverHook. A match hook is a predicate or a pair of key functions (see Match observers to calls): Pass null (None in Python, nil in Go) to remove a hook. Rust removes hooks with clear_* methods: clear_observer_added(), clear_observer_removed(), clear_observation_match() and clear_observation_deliver() on the FunctionRef; clear_observer_added_for(id), clear_observer_removed_for(id), clear_observation_match_for(id) and clear_observation_deliver_for(id) on the client; and clear_observer_added() and clear_observer_removed() on the client for the worker-wide hooks. Hooks are stored by function id. They stay in place across unregister() and a later registration of the same id, and they can be set before the function is registered. Once the worker’s connection is registered, the SDK logs a warning for each hook id the worker has not registered, which catches a mistyped id.
A hook for one function is set by id (see Hooks) or chained on the FunctionRef that registerFunction returns. Each method returns its receiver, so calls chain. Added, removed and deliver hooks can be sync or async. A match predicate is sync, since it runs on every call. In Rust a hook is an async closure or a type that implements AddedHook, RemovedHook or DeliverHook. Implement these traits with native async fn, without #[async_trait]: their futures must be Send and may borrow self across .await. See Implement Rust hooks with native async fn. Removing a hook returns the function to its default (see Hooks). Hooks are stored by function id. They stay in place across unregister() and a later registration of the same id, and they can be set before the function is registered. Once the worker’s connection is registered, the SDK logs a warning for each hook id the worker has not registered, which catches a mistyped id.

Reject an observer

An observer is accepted when every onObserverAdded that applies to it resolves. The SDK then acknowledges it to the engine, and the observer becomes active. So its database trigger exists before the first call it must receive. An onObserverAdded that throws rejects the observer. A thrown ObserveRejection(code, message) sets the code the consumer’s ready() fails with; any other error is OBSERVE_REJECTED.

No replay of past executions

iii does not store observation history. A hook reacts only to the executions of its function that happen after the hook is registered, and an observer receives only the calls that happen after it is accepted.

Reject an observer later

An observer that the function accepted can become invalid later. For example, a configuration reload can remove the resource that the observer watches. obs.reject(code, message) stops delivery to the observer immediately, and the SDK runs onObserverRemoved next, connected or not. The engine moves the observer to error with that code. If the connection is lost, the SDK sends the rejection again after it reconnects.
FunctionRef.observers() returns the active observers of the function.

Observation lifecycle

These hooks run per call. An observation is one call of the function reaching, or being considered for, one observer.
An observer’s args and the call settle the choice in onObservationMatch. Choose in onObservationDeliver when the choice compares observers with each other, or when making it produces part of the payload.

onObservationMatch(obs, call)

By default, every call is observed by every observer. Setting this hook overrides that: it selects, for one call, which observers get it, as a predicate that returns a boolean. iii then applies each matched observer’s capture and filters as usual.

onObservationDeliver(call, observers)

By default, iii carries out each observation: it runs the target of every matched observer whose capture and filters match the call, and gives the caller the handler’s return value. Setting this hook overrides that: you deliver the call yourself and decide what is returned to the caller.

Match observers to calls

onObservationMatch selects, for each call, which observers get it, so a function can route its calls by the args of its observers. The predicate receives the observer and the call, and returns a boolean: true when that observer gets this call, false when it does not. The SDK calls it once for each active observer, before that observer’s capture and filters; a predicate that throws counts as no match for that observer. iii does everything else as usual: each observer’s capture, filters and policy still apply.

Producing events with observers

Function observation is not limited to input/output. Functions can use the same observer pattern to produce events during execution. Do it with a second function that this worker hosts, and call it with the event as the payload. That second function works as the event source:
  • Its handler returns its input, so one call is one event.
  • Each call runs the observers of the event source.
  • The producing function gets the handler’s return value, so it keeps running.
  • private: true keeps the calls to this worker: other workers can observe the event source, and only this worker can call it (see Private functions).
FunctionRef.isObserved() returns true when the event source has at least 1 active observer. It has minimal overhead, so it can be used to skip events that are not being observed.
Because the worker calls its own function, the call runs in-process (local dispatch) and reaches the engine only when an observer’s filters match.

Send an observer its initial state

A source sends one observer something of its own, such as its current state when the observer is added or an event it missed, by calling through that observer. The call runs that observer’s target and no other. obs.accepted() resolves once the SDK has acknowledged the observer. A call made after it resolves reaches an active observer, since the engine handles a connection’s messages in order, as long as the observer’s target is registered: an observer whose target is not registered yet stays pending, and the call fails with OBSERVER_NOT_ACTIVE. The observer becomes active after its onObserverAdded returns, so the hook starts a task that waits for accepted() and then makes the call. accepted() fails for an observer rejected first, such as one whose onObserverAdded throws, and the task then sends nothing. The target receives the payload as given, with the observer’s registration metadata. Send what your function’s targets expect.

Call a target through its observer

iii delivers a call to every matched observer and does not wait for the targets. When the worker that hosts a function must reach one observer on its own, it makes a call that names that observer. The call runs that observer’s target and resolves to the target’s result. In one line, the call is:
The http worker does this for every request. http::on-request returns its input, one observer with { method, path } args declares one route, and the deliver hook picks the route of the request and calls through that route’s observer. The target’s return value is the response. The full flow:
Only the worker that hosts the observer’s function may make such a call. The observer’s policy, metadata and the loop guard still apply.

Registration options

registerFunction takes the options that describe a function to the engine; the SDK references list them. Three of them change what observation means:

Private functions

An event source is observed by everyone and called only by its own worker. A call from another worker would be a forged request or row change carrying the source’s identity. private: true expresses that:
  • The engine accepts a call of a private function only from the connection that hosts it. Any other connection, and the engine itself, gets FORBIDDEN. A call through an observer is the observing worker’s call, and an activation is the engine’s.
  • A private function is observable, and its observers receive every call its worker makes.
  • A private function cannot be an observer’s target (FORBIDDEN at observe).
  • A worker’s call to its own private function passes the input and the result by reference. No other worker can read either value, so the SDK copies neither, and the handler and hooks treat both as read-only. With NODE_ENV=development, the Node SDK freezes them, so a write throws. An observer’s target receives its own copy, serialized when the call matches.
Any function can be private; event sources are the common case.

Prevent function observation

Some functions must stay unobserved, for example because their input carries credentials. Register them with observable: false. Every observe of them fails with NOT_OBSERVABLE.

One host per function

A function id has one host per namespace, the worker that registers it and runs its handler (see Namespaces). Observer hooks therefore run once per observer, in that host. When the host disconnects, the observers of its functions go pending. The next worker that registers the function receives those observers and runs its hooks for each one: the added hook, or the worker-level pair, so the setup runs again in the new host. An observer becomes active again once its hook returns. Run one instance of a worker that hosts an event source, and let the platform restart it: stop the old instance before the new one starts (strategy: Recreate in Kubernetes).

The Observer object

Hooks get an Observer: the record of what the consumer asked for. Who observes, the observer’s metadata and its policy stay with the engine. The object stays the same for the full life of the observer, so the worker can keep it, for example in a map. To reject an observer from onObserverAdded, throw new ObserveRejection(code, message).

The Call object

onObservationMatch and onObservationDeliver receive the call: