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

# Upgrading from 0.24.x to 0.25.x

> Behavior changes 0.25 brings to existing workers: local dispatch, the loop guard, and SDK changes.

0.25 makes every function observable with `worker.observe(source, target, options?)`, and runs the
calls a worker makes to its own functions in-process. The trigger APIs keep working in 0.25, so most
projects upgrade without code changes. This page lists the behavior changes 0.25 brings to existing
workers.

> [!NOTE]
>
> 0.26 removes triggers and trigger types. To move them to observers and observable functions with
> 0.26 SDKs, see [Upgrading from 0.25.x to 0.26.x](https://iii.dev/docs/upgrading/from-0-25-x.md). To move them while you stay on
> 0.25, follow the [0.25 version of this guide](https://iii.dev/docs/upgrading/from-0-24-x.md), which uses the 0.25 APIs.

## What changes for existing workers

* **The old APIs keep working.** 0.24 SDKs run against a 0.25 engine unchanged, except that an
  `unregisterTriggerType` now [takes effect](#unregistertriggertype-now-takes-effect), and their
  functions are observable. 0.25 SDKs run against a 0.24 engine too: `observe` fails with
  `UNSUPPORTED`, observe hooks never run, and calls are not dispatched locally.

* **`registerTrigger` and `registerTriggerType` are deprecated** in every 0.25 SDK. They keep
  working in 0.25; 0.26 removes them.

* **One runtime warning per process per API.** The first call logs, through the SDK's logger (each
  SDK names its own API):

  ```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
  registerTrigger is deprecated and will be removed in a future release. Use iii.observe(source,
  target) instead. Migration guide: https://iii.dev/docs/upgrading/from-0-24-x
  ```

  `registerTriggerType` logs the equivalent message pointing to `registerFunction` with an `observe`
  descriptor.

* **Language markers.** TypeScript marks the APIs `@deprecated`, Python emits a `DeprecationWarning`
  at the call site, Go marks them `Deprecated:` (reported by linters such as staticcheck), and Rust
  marks the methods `#[deprecated(since = "0.25.0")]`.

> [!WARNING]
>
> Rust crates built with `#![deny(warnings)]` or `-D warnings` fail to compile on calls to
> `register_trigger`, `register_trigger_type`, or `unregister_trigger_type`. Put
> `#[allow(deprecated)]` on the calls you keep, or migrate them.

## Behavior changes

### Local dispatch

A worker that calls a function it registered itself now runs the handler in-process after the engine
confirms the registration. The call does not go through the engine. Results, error shapes, and
timeouts are the same. The differences:

* **No message size limit** on that path, because nothing is serialized. Through the engine, a
  message can be at most 64 MiB (16 MiB for browsers and SDKs older than 0.25), and a larger one
  fails with `payload_too_large`. A local call never fails with this error. If a test expects
  `payload_too_large` on a call to a function in the same worker, send the call with
  `dispatch: "remote"`.
* **No engine-side span or call metrics** for the local call. The SDK still records its own
  `execute` span, and reports the number of local invocations every 10 seconds
  (`localinvocationstats`), which the engine adds to `iii.invocations.total` with
  `dispatch="local"`.
* **Value semantics.** The handler receives a copy of the payload and the caller a copy of the
  result, as over the wire.
* **Rust:** a local call runs on the caller's task, and a sync handler (`RegisterFunction::new`)
  runs inline on it, where the call's timeout cannot interrupt it. For a handler that can block, use
  an async handler (`RegisterFunction::new_async`) or set `local_dispatch(false)` on it.
* **Recursion:** a local call nested more than 64 local calls deep goes through the engine. In the
  browser while offline, local calls run up to 256 levels deep, and a deeper one fails at once with
  `LOCAL_DEPTH_EXCEEDED`.
* **Python:** a sync handler called locally runs on a thread of its own and honors the caller's
  timeout. Whatever a local handler raises, `BaseException`s such as `KeyboardInterrupt` and
  `SystemExit` included, reaches the caller as an `InvocationError` with code `invocation_failed`
  and the original exception as its `__cause__`.
* **Disconnects:** once confirmed, local dispatch keeps serving calls to the worker's own functions
  while the connection is down.

Local dispatch is off for a session with an RBAC `middleware_function_id`, or one that may not
invoke the function, because a local call would bypass those checks. Enqueue calls always go through
the engine. To keep the old behavior for a function, a call, or the whole engine:

| Scope | Node / browser | Python | Rust | Go |
| - | - | - | - | - |
| One function | `{ localDispatch: false }` | `local_dispatch=False` | `RegisterFunction::new(f).local_dispatch(false)` | `RegisterFunctionOptions{LocalDispatch: &off}` |
| One call | `dispatch: "remote"` | `"dispatch": "remote"` | `TriggerRequest::new(id, payload).dispatch(Dispatch::Remote)` | `TriggerRequest{Dispatch: iii.DispatchRemote}` |
| Whole engine | `observers.local_dispatch: false` in the [engine config](https://iii.dev/docs/using-iii/configuration.md#observers) | | | |

0.26 renames `TriggerRequest` to `CallRequest`; see [Upgrading from 0.25.x](https://iii.dev/docs/upgrading/from-0-25-x.md).

### `unregisterTriggerType` now takes effect

Every SDK has always sent `unregistertriggertype`, but engines before 0.25 ignored it. The 0.25
engine removes the trigger type when the sending worker is its provider, and ignores the message
otherwise. The type's triggers then move to another provider of the type, or wait until a provider
registers again. If your worker calls `unregisterTriggerType` and expected the type to stay, remove
the call.

### Smaller changes

* **`RegisterFunction` frames** omit `request_format` and `response_format` when they are absent.
  Before, they sent `null`.
* **Loop guard.** Every invocation carries an observe depth. When the depth of an event is more than
  `observers.max_observe_depth` (8 by default), the engine drops it. Workers on 0.24 SDKs do not
  send the depth. For their calls, the engine uses the largest depth of the invocations still in
  flight on that worker.
* **Observe hooks that hang.** An `onObserverAdded` hook still running after 30 seconds fails the
  observer with `OWNER_TIMEOUT`, and the next hook of the same function may start while it runs.
* **Rust SDK:**
  * public protocol structs gained fields and `Message` gained variants, so struct literals of them
    and exhaustive `match`es on `Message` need updating (prefer the constructors, such as
    `TriggerRequest::new`);
  * the connection state turns `Reconnecting` as soon as the link drops;
  * a handler that panics now returns an `invocation_failed` error to the caller. Before, the call
    timed out;
  * frames omit an `invocation_id` of `None`. Before, they sent `null`.
* **Browser SDK:**
  * calls send `metadata`, `traceparent`, and `baggage`, and handlers receive the metadata as their
    second argument;
  * while the page is offline, functions it hosts run locally up to 256 nested calls, and a deeper
    call fails with `LOCAL_DEPTH_EXCEEDED`.

## Migration checklist

* [ ] Upgrade the engine and the SDKs
* [ ] Remove `unregisterTriggerType` calls that expected the trigger type to stay
* [ ] In Rust crates built with `-D warnings`, put `#[allow(deprecated)]` on the trigger calls you
  keep
* [ ] Check functions that call themselves for [local dispatch](#local-dispatch) differences (large
  payloads, blocking Rust sync handlers, tests that expect engine spans)
* [ ] Move triggers and trigger types to observers before you upgrade to 0.26: with 0.26 SDKs in
  [Upgrading from 0.25.x to 0.26.x](https://iii.dev/docs/upgrading/from-0-25-x.md), or on 0.25 with the
  [0.25 version of this guide](https://iii.dev/docs/upgrading/from-0-24-x.md)

## Result

The project runs on 0.25. Calls a worker makes to its own functions run in-process, and every
function can be observed with `worker.observe`.

> [!NOTE]
>
> For the observer model, see [Observers](https://iii.dev/docs/using-iii/observers.md). For the provider side, see
> [Observable functions](https://iii.dev/docs/creating-workers/observable-functions.md). For filter syntax, see the
> [filter language reference](https://iii.dev/docs/reference/filters.md).


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