sourceis what is observed: thefunction_id, itsargs(only for functions whose owner routes events, see Auto and owner delivery), declarativefilters, and what to capture.targetis the id of a registered function. There is no inline handler: register the target first.optionsholds the observer’s id, its delivery policy, and metadata.
iii.observe replaces registerTrigger for sources that are functions. See
Upgrading from 0.24.x to move existing triggers and trigger types.
The http, cron, queue, state, stream, pubsub, and database workers still publish trigger types in
0.25. Keep binding to them with
registerTrigger until they ship source functions;
the deprecation warning it logs is informational. You can observe any regular function, the
engine-hosted event sources, and providers that already moved to
observable functions.Observe a function
The example below sends large orders to an alert function.orders::create is a regular function
registered by another worker; it needs no changes to be observed. The target receives one
Observed envelope per matching invocation.
- Node / TypeScript
- Python
- Rust
- Go
observe returns a handle at once, also before the connection is up. The SDK sends the observer
when it connects and replays it with the same id after every reconnect, so you call it once at
startup, like registerFunction. The browser SDK (iii-browser-sdk) has the same observe API as
Node.
The source
A filter may read any field, whatever the observer captures:
capture only decides what travels to
the target.
Auto and owner delivery
How events reach observers depends on the observed function. Its owner chooses when it registers the function; observers use the sameobserve call in both cases.
Owner-delivered functions document their
args. For example, timer::on-tick, the example function
you write in Observable functions (it is not built in),
takes { every_ms }:
What the target receives
With the defaultdeliver: 'envelope', the target’s first argument is the Observed envelope.
Fields left out of capture are absent.
With
deliver: 'input' or deliver: 'output', the target receives that value alone, which suits a
target written as a regular function of the observed payload. An observer of an owner-delivered
function that the owner reaches with obs.call receives the owner’s payload instead (for example an
HTTP request), and its return value goes back to the owner.
The observer’s metadata option arrives as the activation’s metadata sidecar, the same second
argument (metadata in Python, MetadataFromContext(ctx) in Go) that trigger metadata used. The
target’s return value is discarded for auto delivery and obs.emit.
For local calls and
obs.emit, the hosting SDK reports the observed values to the engine. Values
JSON cannot hold, such as a number no double can represent, arrive as null, and a lone surrogate
in a string or key arrives as U+FFFD. Filters read the values the same way. Reports are at most
once: a report lost with a dropped connection is not sent again.Event size
The engine keeps one copy of an observed invocation for all observers of its function, with every field any of them captures or filters on. Capturinginput and output counts both: a function
that echoes a 40 MiB input makes an event of about 80 MiB. An event too large on its own is not
delivered, however idle the engine is:
- The fields the engine keeps come to more than 64 MiB (approximate in-memory size).
- The activation is larger than the target worker accepts in one message: 64 MiB, or 16 MiB for browser workers and some older SDKs. See Payload size.
- For local calls and
obs.emit, the hosting SDK’s report of the event is larger than its 8 MiB report buffer.
TOO_LARGE drop in the iii.observer.dropped.total metric. In the
first two cases it also counts in the observer’s dropped counts
(engine::observers::list), and the engine logs a warning naming the
observer, the function, the size, and the limit, at most once a minute per observer. An owner’s
obs.call whose activation the target cannot receive fails with payload_too_large. OVERFLOW is
different: the queues were full when the event arrived, and an event of the same size can get
through once they drain.
To keep events small, capture only what the target reads, for example capture: ["input"]. With
deliver: "input" or deliver: "output", set a matching capture too: captured fields are kept
even when the target receives one value. For payloads larger than these limits, pass a
channel reference instead of the data.
Filters
Filters are declarative JSON objects over the event:input, output, error, caller, and
metadata. Each path takes an operator object; there is no literal shorthand.
INVALID_FILTER) and checks its
input.* and output.* paths against the function’s request_format and response_format
(INVALID_FILTER_PATH for a path a closed schema does not have). The same evaluator runs in the
engine and in every SDK, so a filter matches the same events wherever it runs. See the
filter language reference for the operators, the rules for absent and null
values, and the limits.
Policy and metadata
The third argument configures the observer itself:- Node / TypeScript
- Python
- Rust
- Go
iii.observer.dropped.total metric. An
event waiting in several observers’ queues is held once, however many observers it matched.
Observer states
ready()resolves when the observer isactiveand rejects with the reason’s code when it turnserrorordisposedfirst (ObserveRejectionin Node, Python, and Rust;*iii.ObserveRejectionErrorin Go).onStateChange(listener)(on_state_changein Python and Rust,OnStateChangein Go) reports every transition with its reason. Listeners never run on the connection’s read path, so they may call the client.dispose()removes the observer. A disposed observer is not replayed.- An observer belongs to the connection that created it. When that worker disconnects, its observers
are
suspendedforobservers.observer_grace_ms(30 s by default); a worker that reconnects in time keeps them, with their counters. After that they aredisposed, and a worker that reconnects later gets that answer when its SDK replays them, somax_firesandexpires_in_msstill hold. The engine keeps the answer forobservers.disposed_retention_ms(1 hour by default). After that, a returning worker’s replay creates its observers again, as after an engine restart. - When the observed function’s worker disconnects, its observers go back to
pendingand becomeactiveagain when the function is registered again.
Caller and causality
Every invocation carries acaller: the worker that made the call and, when the call was made from
inside a handler, the function and invocation it was made from.
worker_id and worker_name are stamped by the engine from the calling connection and cannot be
forged. function_id, invocation_id, and parent (the caller of that handler, one level only)
come from the calling SDK, so use them for routing and filtering, not for authorization. A filter
such as { "caller.parent.worker_name": { "$ne": "search-indexer" } } stops an indexer from
reacting to its own writes.
When the engine activates a target (auto delivery and obs.emit), the target’s caller is the engine
(worker_name: "iii-engine", no worker_id), with the observed function and invocation as
function_id and invocation_id. For obs.call, the caller is the owner’s worker and handler. In
both cases the current invocation’s activation holds the observer_id and activation_id.
The SDKs propagate the caller automatically from the handler you are in. Each SDK also exposes the
current invocation (function_id, invocation_id, caller, activation, observe_depth):
In the browser, calls made after an
await outside withInvocation carry the depth of the deepest
invocation running on the page, so with several invocations running the depth is over-counted: an
observer chain may stop early at the loop guard, and some concurrent local calls go through the
engine (with the same results). While the page is offline, functions it hosts still run locally, up
to 256 nested local calls; past that a call fails at once with LOCAL_DEPTH_EXCEEDED.
Loop guard
An observer whose target calls the observed function again (directly or through other observers) would loop forever. Every invocation carries anobserve_depth, which grows by one on each
activation. Activations deeper than observers.max_observe_depth (8 by default) are dropped and
counted as LOOP_GUARD, and an observer whose target is the observed function itself is rejected at
creation with SELF_TARGET.
Workers on SDKs older than 0.25 do not propagate the depth; through them the engine estimates it
from their activations still in flight.
Local dispatch
A worker that calls a function it registered itself runs the handler in-process, without a round trip through the engine. When nobody observes the function, such a call puts nothing on the wire. When observers match, the SDK evaluates their filters itself and reports the matching invocations to the engine in batches, so observers see local and routed calls alike. A call runs locally when all of these hold:- The function was registered by this worker with a handler (not an HTTP-invoked function), and local dispatch was not turned off for it.
- The engine confirmed the registration for local dispatch (a 0.25 engine,
observers.local_dispatchon, and a session without RBACmiddleware_function_idthat may invoke the function). - The call resolves to the namespace the function is registered in.
- The action is not
Enqueue(aVoidcall runs locally, fire-and-forget). - The call does not ask for the remote path.
- Node / TypeScript
- Python
- Rust
- Go
observers.local_dispatch: false in the
engine configuration.
Engine-hosted event sources
The engine hosts event sources as auto-delivered functions in thedefault namespace. Observe them
like any function, with filters on the event. They replace the engine’s own trigger types, which
keep firing in 0.25.
These events have no output, so use
deliver: 'input' to hand the event itself to the target. Their
request_format is closed at the top level, so a misspelled input.* filter path fails with
INVALID_FILTER_PATH. Calling one of them directly fails with FORBIDDEN: their events are the
engine’s to emit.
- Node / TypeScript
- Python
- Rust
- Go
Inspect observers
The engine lists observers through three built-in functions:
The output never includes an observer’s metadata. Under RBAC, a session sees only the observers of
functions it may invoke.
Errors
An observer that cannot be created turnserror, and ready() rejects with one of these codes:
Authorization
To observe a function, a session must be allowed to invoke both the observed function and the target (RBACexpose_functions). The check is repeated when the target registers again on another
connection. The RBAC block of a worker-manager listener accepts an
on_observer_registration_function_id hook, with the same contract as
on_trigger_registration_function_id: it may rewrite function_id, target_function_id, args,
filters, and metadata, or deny the observer (FORBIDDEN). See
Worker manager.
Compatibility
- Against an engine older than 0.25,
observefails withUNSUPPORTEDand nothing runs locally. - Functions hosted by workers on older SDKs are still observable: their invocations are routed through the engine, which evaluates the filters.
- Workers on older SDKs cannot observe. They keep using
registerTrigger.