Events
Run actions in response to domain events published on the event bus.
Agent-readable version: Markdown · llms.txt
Instead of a clock, a routine can watch the event bus and fire when a matching domain event is published. This is the right shape for "when X happens, do Y" — e.g. "when a new email arrives from a certain sender, summarize it and text me".
This page covers event-driven routines and how apps publish the events that
drive them. For the routine basics — system:create_routine, input fields, the
duplicate guard — and for clock-based triggers, see Routines.
Event-bus triggers and filters
An EventBusTrigger names the event type to watch and can narrow it down by
payload:
interface EventBusTrigger {
type: "event-bus";
eventName: string;
sourcePattern?: string;
filter?: EventFilterCondition[]; // AND-ed; absent/empty = every event of this name
}
interface EventFilterCondition {
field: string; // dot-path into the payload, e.g. "from.email"
equals: string; // value at that path, String()-coerced, must equal this
}eventNameis the watchable event type — the same value the producer publishes.filteris a list of equality checks, all of which must pass. Each check reads a dot-path (field) into the event payload and passes when the value at that path, turned into a string withString(), equalsequals. A missing or emptyfilterfires on every event of that name.sourcePatterncan limit which source the event came from.
Example: a routine that fires only for emails from one sender, then summarizes them with an app action:
await appContext.runAction("system:create_routine", {
name: "summarize-dana-emails",
trigger: {
type: "event-bus",
eventName: "gmail.email.received",
filter: [{ field: "from.email", equals: "dana@example.com" }],
},
actionName: "mail-tools:summarize_and_notify",
args: { channel: "telegram", threadId: "123" },
});When the routine fires, the engine merges the triggering event's payload into
the action's args under the reserved key __triggerPayload — so
summarize_and_notify reads the email that fired it from
args.__triggerPayload, next to the args you set at creation. Your own
args may not contain that key; system:create_routine rejects it.
The same duplicate guards apply to event-bus routines — pass a stable key,
or match on name via listRoutines() before registering. See
Routines.
The event catalog and system:search_event_catalog
You don't have to guess event names or payload field paths. The event
catalog keeps track of which event types can be emitted right now, and you
find them with the system:search_event_catalog action:
const result = await appContext.runAction("system:search_event_catalog", {
query: "email received",
});Each catalog entry (EventCatalogEntry) has:
| Field | Meaning |
|---|---|
eventType | The watchable event name — bind it as trigger.eventName. |
appId | The producer app that owns and emits the type. |
payloadSchema | JSON Schema for the event payload — the fields a filter dot-path can match. |
schemaOrigin | "observed" (worked out from a recent emission) or "declared" (written by the producer). |
Treat an observed payloadSchema as a hint, not a promise: it is worked
out from a single recent non-empty emission, so it may leave out optional keys
and, on purpose, lists nothing as required. It is there to help you write a
correct trigger.filter dot-path. A declared schema is written by the
producer and is the one to trust. payloadSchema is missing until the type has
been emitted with a non-empty payload.
Publishing domain events
An app that wants other routines to react to what it does publishes events onto the bus. The same call also adds the event type to the catalog, so consumers can find it:
interface EventPublisher {
publish(event: {
name: string;
source: string;
payload?: Record<string, unknown>;
}): Promise<{ accepted: true }>;
}A backend action gets an EventPublisher by declaring
eventBus: EventPublisher in its deps bag — it is injected the same way as
agentRunner. Alternatively, call the platform system:publish_event action with
{ name, payload? }; there source defaults to your app id.
nameis the event type consumers bind to asEventBusTrigger.eventName.sourcesays who emitted it (can be matched against a trigger'ssourcePattern).payloadis the data carried with the event. Its fields are what a consumer'sfilterchecks read by dot-path, and the basis for the catalog's observedpayloadSchema. Publishing with a non-empty payload is what makes the type's schema show up.
Because the observed schema is worked out from real emissions, publish a typical, fully-filled payload so consumers writing filters can see every field they might want to match.