Skip to main content

Documentation Index

Fetch the complete documentation index at: https://motiadev-docs-phase-2.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

What “writing a trigger” means

A worker uses triggers two ways. Most of the time, you bind the worker’s functions by registering a trigger on an existing trigger type such as: http, cron, queue messages, state changes, and any other event source in the system. Less often, you register a new trigger type from your worker so other workers can bind their functions to events your worker emits. This page primarily covers the latter: making your own triggers, if you want to use existing triggers in new workers then refer to Using iii / Triggers.
For the caller-side mechanics (direct invocation with worker.trigger or iii trigger, the TriggerAction variants, gating with conditions, multiple bindings per function), see Using iii / Triggers.

Bind a function to an existing trigger type

Most workers consume trigger types that other workers already publish: http from iii-http to expose a function as an endpoint, cron from iii-cron to run a function on a schedule, queue triggers from iii-queue to fire a function on each message, state from iii-state to react to data changes. Bind one of your worker’s functions to a trigger type with worker.registerTrigger({ type, function_id, config }). The worker that publishes the trigger type must be connected when you register; otherwise the registration fails.
worker.registerTrigger({
  type: "http",
  function_id: "math::add",
  config: { api_path: "/math/add", http_method: "POST" },
});
The config shape is defined per trigger type and documented in each publishing worker’s Worker Docs.
Other binding mechanics are covered in Using iii / Triggers: the unregister handle, binding multiple triggers to the same function, gating with condition_function_id, and the TriggerAction variants (Void, Enqueue, etc.).

Attach metadata to a trigger

Each trigger binding accepts an optional metadata JSON object set by the consumer at registration time. The engine stores it as-is and surfaces it in two places:
  1. The publishing worker’s TriggerHandler.registerTrigger(config) callback sees it as config.metadata, so the publisher can act on consumer-supplied tags (priority hints, audit labels, routing keys for the publisher’s own bookkeeping).
  2. engine::triggers::list returns it on each TriggerInfo, so the console and any other worker doing discovery can read it.
worker.registerTrigger({
  type: "http",
  function_id: "math::add",
  config: { api_path: "/math/add", http_method: "POST" },
  metadata: { team: "platform", env: "staging" },
});
Trigger types have no metadata field of their own. Metadata is attached per binding, not per type.Don’t confuse trigger metadata with trigger type schemas (trigger_request_format and call_request_format):
  • Metadata is set by the consumer on each binding (ie. the worker.registerTrigger() call). It’s a free-form tag bag the engine stores as-is, used for the publisher’s bookkeeping and for discovery. For example, a consumer binding to http might attach metadata: { team: "platform", env: "staging", on_call: "alice" } so the publishing worker can log the team, and engine::triggers::list surfaces this information on request.
  • Schemas are set by the publisher when declaring the trigger type. They document the JSON shapes the consumer interacts with. For example, the http type published by iii-http declares:
    • config (what the consumer passes at bind time): { api_path, http_method }.
    • invocation payload (what their bound function receives on each request): { method, headers, query_params, body }.

Declaring a Trigger Type

So far this documentation has focused on being the consumer: your worker’s functions get bound to trigger types other workers publish. This section flips the roles. Your worker is now the publisher, and you want functions that other workers register to fire on events your worker observes (an HTTP request, a webhook hit, a file change, a database update).

Components of a Trigger Type

A trigger type is two things bundled together:
  1. A string id that consumers reference when they bind to a trigger (ex. type: "mini-http").
  2. A per-binding routing table that your worker maintains in-process. The engine’s registry records the binding canonically (this is what engine::triggers::list returns), but the engine doesn’t dispatch on it. The engine forwards every bind/unbind from any consumer worker on the network to the publisher worker as a callback, and this worker decides what to do with each one.
You declare the trigger type once at startup with worker.registerTriggerType({ id, description }, handler). The TriggerHandler interface you implement exposes two callbacks. The engine invokes them on your publisher worker whenever a consumer binds or unbinds:
  • registerTrigger(config): Runs when any consumer worker binds a function to your trigger type. The config carries the trigger instance’s id, the consumer’s function_id, and the consumer-supplied config matching the shape your type accepts. Stash it.
  • unregisterTrigger(config): Runs on unbind. Drop it from your table.
The trigger type can be torn down at any point during runtime with worker.unregisterTriggerType(...) (or worker.unregister_trigger_type(...) in Python and Rust). See the Unregister a Trigger Type section below for per-language signatures.

Example: A mini iii-http from scratch

The example below sketches a tiny version of iii-http, the worker that publishes the real http trigger type. The publisher worker:
  1. Declares an HTTP-shaped trigger type called mini-http
  2. Maintains a bindings map of { trigger id → function_id, method+path } as consumers bind/unbind
  3. Is then ready to look up the right binding when an HTTP request is received. Firing the bound function is covered in Dispatch events to bound functions below.
import { registerWorker } from "iii-sdk";
import type { TriggerConfig, TriggerHandler } from "iii-sdk";

const url = process.env.III_URL;
if (!url) throw new Error("III_URL must be set");
const worker = registerWorker(url);

type MiniHttpConfig = {
  api_path: string; // leading slash, e.g. "/orders"
  http_method?: "GET" | "POST" | "PUT" | "DELETE";
};

const bindings = new Map<string, TriggerConfig<MiniHttpConfig>>();

const httpHandler: TriggerHandler<MiniHttpConfig> = {
  async registerTrigger(config) {
    bindings.set(config.id, config);
  },
  async unregisterTrigger(config) {
    bindings.delete(config.id);
  },
};

worker.registerTriggerType(
  { id: "mini-http", description: "Routes HTTP requests to bound functions" },
  httpHandler,
);

Attach schemas to the trigger type

A trigger type can carry two optional JSON Schemas that describe its payloads:
  • trigger_request_format: The schema for the per-binding config consumers pass to worker.registerTrigger(...) when they bind a function to your trigger type.
  • call_request_format: The schema for the payload your worker delivers to bound functions when the trigger fires.
Both feed the iii console, the agent-readable skills, and the engine::trigger-types::list output so consumers know what to send and what they’ll receive.
Runtime validation is not yet supported. Attached schemas are informational only; the engine does not reject config values or call payloads that don’t match them. Treat the schemas as contract documentation for consumers, agents, and the console; same caveat as function request / response schemas.
Each SDK accepts these in its own idiomatic way:
SDKWhat you pass
Node / BrowserRaw JSON Schema objects on trigger_request_format / call_request_format. Convert Zod 4+ schemas with z.toJSONSchema(...).
PythonA Pydantic model class (auto-converted) or a raw dict on the same fields of RegisterTriggerTypeInput.
RustBuilder methods on RegisterTriggerType: .trigger_request_format::<T>() and .call_request_format::<T>(), where T: schemars::JsonSchema.

Unregister a Trigger Type

Tear down a trigger type at runtime when the work it routes is no longer needed. When the worker disconnects, all trigger types it advertised are removed automatically and the engine stops routing events that depended on them, so this step is only necessary if you want to drop a type while the worker stays connected. Call it any time after registerTriggerType while the worker stays connected (e.g. the underlying resource went into maintenance mode, a feature flag turned the surface off, or you want to rotate the type to a new schema without restarting). Continuing the mini-http example, here the worker drops mini-http because its HTTP listener was disabled by config:
// e.g. config reload disabled the HTTP listener; stop accepting new bindings
// while the worker keeps serving other trigger types.
worker.unregisterTriggerType({
  id: "mini-http",
  description: "Routes HTTP requests to bound functions",
});
Node’s registerTriggerType also returns a TriggerTypeRef with an .unregister() shortcut that delegates to worker.unregisterTriggerType(...). Python’s TriggerTypeRef only exposes register_trigger and register_function; tear down the trigger type itself via worker.unregister_trigger_type(...). Rust takes only the id string; Node and Python take the full input object but only the id field is used to identify the type being torn down.

Dispatch events to bound functions

There is no special “fire” API. When the underlying event source delivers something (an incoming HTTP request, a cron tick, a webhook hit), your publisher worker looks up the relevant entry in the bindings table it built inside its registerTrigger callback and invokes each matching function via worker.trigger(...). Continuing the mini-http example from above:
// Inside the worker's HTTP listener, after matching method+path to an
// entry in the `bindings` map from the declare-trigger-type example:
const binding = bindings.get(matchedTriggerId);
await worker.trigger({
  function_id: binding.function_id,
  payload: { method, headers, body },
});
On each dispatched event, the engine evaluates the consumer’s config and optional condition_function_id, then routes matching invocations to the bound function and returns the result to the caller.