# Events
URL: /docs/building-apps/capabilities/events
Markdown: /docs/building-apps/capabilities/events.md
Description: Run actions in response to domain events published on the event bus.

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](/docs/building-apps/capabilities/routines).

## Event-bus triggers and filters [#event-bus-triggers-and-filters]

An `EventBusTrigger` names the event type to watch and can narrow it down by
payload:

```ts
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
}
```

* `eventName` is the watchable event type — the same value the producer
  publishes.
* `filter` is 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 with `String()`, equals `equals`. A missing or
  empty `filter` fires on every event of that name.
* `sourcePattern` can 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:

```ts
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](/docs/building-apps/capabilities/routines#avoiding-duplicates-with-listroutines).

## The event catalog and `system:search_event_catalog` [#the-event-catalog-and-systemsearch_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:

```ts
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 [#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:

```ts
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.

* `name` is the event type consumers bind to as `EventBusTrigger.eventName`.
* `source` says who emitted it (can be matched against a trigger's
  `sourcePattern`).
* `payload` is the data carried with the event. Its fields are what a consumer's
  `filter` checks read by dot-path, and the basis for the catalog's observed
  `payloadSchema`. 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.