> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Triggers and Events

> How events flow into auto, how triggers match them, and how spawn, bind, and deliver routing decide which session does the work.

Triggers are how a factory wakes up. Every agent declares, in YAML, which events it cares about and what should happen when one arrives: start a new session, wake the session already working on that pull request or thread, or drop a message into running sessions. This page explains the event model and the routing decisions conceptually; [routing](/concepts/routing) and [binding](/concepts/binding) collect the production rules, the field-by-field trigger schema lives in [the trigger reference](/reference/triggers), and every event key is cataloged under the [event catalog](/reference/events/github).

## The event pipeline

Every event — a GitHub webhook, a Slack message, a cron tick, a custom webhook `POST` — follows the same path:

```mermaid theme={null}
flowchart LR
    A["Provider webhook /<br/>cron tick / API POST"] --> B["Normalize +<br/>dedup"]
    B --> C["Match triggers<br/>(event key + where)"]
    C --> D{"Routing kind"}
    D -->|spawn| E["New session"]
    D -->|bind| F["Session bound<br/>to the target"]
    D -->|deliver| G["Existing<br/>session(s)"]
```

Ingress normalizes each provider's raw payload into a stable event shape with a namespaced key (`github.pull_request.opened`, `chat.message.mentioned`, `linear.issue.updated`), records it once (a duplicate delivery with the same dedup key never re-routes), and hands it to the trigger router. The router finds every trigger in the project listening on that event key, applies each trigger's `where` filter to the payload, and executes the matching triggers' routing.

The trigger's `message` is a template rendered against the event payload — `{{github.pullRequest.number}}`, `{{message.text}}` — and becomes the message the session receives. See [variables and templating](/reference/variables-and-templating) for the template grammar.

## Event sources

Events come from four kinds of sources. The first segment of the event key names the source, which also determines what the trigger needs at apply time.

### Provider connections

Most events arrive through a [connection](/concepts/connections-and-identities) — an installed GitHub App, a Slack workspace, a Linear workspace, a Telegram bot. The trigger names the connection it listens through:

```yaml theme={null}
triggers:
  - event: github.pull_request.opened
    connection: github-acme
    routing:
      kind: spawn
      bind:
        target: github.pull_request
```

* **GitHub** (`github.*`) — pull request lifecycle, issues, comments, reviews, check runs, workflow runs, pushes, plus a synthetic event auto derives itself: `github.pull_request.merge_conflict`. See [GitHub events](/reference/events/github).
* **Chat** (`chat.*`) — Slack, Discord, and Telegram all normalize into one provider-neutral contract: `chat.message.{channel,direct,mentioned,subscribed,edited}` and `chat.reaction.{added,removed}`. The `connection:` field selects the workspace; filter on `$.chat.provider` when one agent listens across providers. See [Slack events](/reference/events/slack) and [Telegram events](/reference/events/telegram).
* **Linear** (`linear.*`) — `linear.issue.created` and `linear.issue.updated`. See [Linear events](/reference/events/linear).

A trigger bound to a connection can be marked `optional: true`: apply silently skips it while the connection has no active grant and activates it on the next apply once the connection exists. This keeps one `.auto/` directory portable across projects that have different connections.

### Custom webhooks

Any system that can send an HTTP `POST` can wake an agent. A trigger that declares `endpoint:` plus an `auth:` policy (`hmac_sha256`, `bearer_token`, or `none`) reserves a project-scoped ingest URL at apply time. The posted JSON body becomes the event payload verbatim: a top-level `event` field maps to the `webhook.<event>` key, and a body without one lands on `webhook.received`. See [cron and webhooks](/reference/events/cron-and-webhooks) for the endpoint mechanics.

### Cron (heartbeat)

A heartbeat trigger has no event key — it declares `kind: heartbeat` with a `cron` expression and optional `timezone` (default `UTC`). Apply creates one durable schedule per heartbeat trigger, and each tick routes like any other event, carrying a small payload (`{{heartbeat.scheduledAt}}` and friends). Missed ticks are safe to catch up: each tick is deduplicated by schedule and scheduled time.

```yaml theme={null}
triggers:
  - kind: heartbeat
    cron: "0 9 * * 1-5"
    timezone: America/New_York
    message: "Good morning. Prepare the daily ship digest."
    routing:
      kind: deliver
      onUnmatched: spawn
```

### Lifecycle events

The platform emits its own `auto.*` events so agents can react to the factory itself: `auto.project_resource_apply.{started,completed,failed}` (a config apply ran), `auto.connection.{established,removed}` (the project's usable connections changed), `auto.session.binding.{bound,updated,unbound}` (a binding transitioned — the raw material for observer agents), and `auto.onboarding.phase_changed`. Lifecycle triggers need no connection. See [lifecycle events](/reference/events/lifecycle).

<Warning>
  Event keys are an open vocabulary — the schema accepts any non-empty string, and only the provider prefix is validated at apply. A misspelled key such as `github.pull_request.close` applies cleanly and simply never fires. Copy keys from the [event catalog](/reference/events/github) rather than typing them from memory.
</Warning>

## How a trigger matches

A trigger fires when all of the following hold:

1. **Event key match.** The trigger's `event:` (or one of its `events:`) equals the incoming key exactly. An `events:` list expands into one stored trigger per key.
2. **Origin match.** The event arrived through the trigger's declared connection or webhook endpoint. Two agents listening on `chat.message.mentioned` through different Slack workspaces never see each other's traffic.
3. **`where` filter match.** Every clause in the `where` map must hold against the payload. Keys are `$.dot.paths` into the payload; clauses are scalar equality or one of `contains`, `exists`, `in`, `notIn`, `changedTo`. An empty `where` matches everything.

```yaml theme={null}
triggers:
  - event: github.issue.labeled
    connection: github-acme
    where:
      $.github.label.name: agent
      $.github.auto.authored: false
    routing:
      kind: spawn
      bind:
        target: github.issue
```

Two matching refinements are worth knowing about:

* **Addressed events.** When a chat message or GitHub comment explicitly mentions an agent (`@auto.<agent>` or the agent's own bot), the platform produces an addressed copy of the event that only that agent's triggers can match, and excludes that agent from the broadcast copy so one message never delivers twice to the same agent (an agent that cannot receive the addressed copy keeps its broadcast delivery instead of losing the message). A directed chat mention also does not fan out: other agents subscribed to the thread stay subscribed but are not woken by a message aimed at someone else.
* **Fallback triggers.** A trigger marked `fallback: true` fires only when no non-fallback trigger on the same endpoint and event key matched. This is the idiomatic catch-all for a custom webhook endpoint that receives several event shapes: author specific triggers for the shapes you know, plus one fallback on `webhook.received`.

## Routing: spawn, bind, deliver

Routing answers "which session handles this event?" There are three kinds.

### `spawn` — start a fresh session

Spawn creates a new session for each matched event. Use it for events that begin a unit of work: a PR opened, an issue labeled for an agent, a first mention in a new thread.

A spawn trigger can also **bind at spawn**: `bind: { target: github.pull_request }` records that the new session owns that pull request, so later events on the same PR resolve back to it through the bind path instead of spawning duplicates.

```yaml theme={null}
routing:
  kind: spawn
  bind:
    target: github.pull_request
```

### `bind` — continue the session that owns the target

Bind routing resolves the event's target — the PR, issue, chat thread, or Linear issue the event is about — to the session bound to it, and delivers the message there. This is the continuation primitive: review comments, check results, and follow-up pushes on a PR all land in the one session that opened or claimed it.

Bindable targets are `github.pull_request`, `github.issue`, `slack.thread`, `linear.issue`, `agent.singleton`, and `auto.session`. Events that carry no derivable target (for example `github.push`) can never match a bind route — use spawn or deliver for those.

```yaml theme={null}
routing:
  kind: bind
  target: github.pull_request
  onUnmatched: spawn
```

### `deliver` — message existing sessions

Deliver drops the event into already-running sessions without consulting a binding. `routeBy` picks the audience:

* `{ kind: attributedSessions }` — sessions attributed to the event (for chat, the sessions bound into that thread). The workhorse for conversational follow-ups.
* `{ kind: allLiveSessions }` — every live session of the agent. Use sparingly, for genuinely broadcast signals.
* Omitted — resolves the agent's single concurrency slot member; legal only for a `concurrency: 1` agent (or with a slot-claiming `onUnmatched: spawn`). See [runtime controls](/reference/runtime-controls).

### Choosing a routing kind

| You want                                                        | Use                                                   |
| --------------------------------------------------------------- | ----------------------------------------------------- |
| A new session per PR / issue / thread / webhook                 | `spawn` (usually with `bind:` so follow-ups find it)  |
| Follow-up events to reach the session that owns the artifact    | `bind` on that target                                 |
| Chat replies to reach the sessions already in the thread        | `deliver` + `attributedSessions`                      |
| A cron tick or broadcast signal to reach a long-lived singleton | `deliver` with `concurrency: 1`, `onUnmatched: spawn` |

## When nothing matches: `onUnmatched`

`bind` and `deliver` routes declare what happens when no session resolves: `drop` (default, silently record and discard), `warn`, `error`, or `spawn` — turn the unmatched delivery into a fresh session carrying the event. For a concurrency-capped agent, an unmatched `spawn` also claims the agent's slot in the same transaction, and converts to a delivery if a concurrent spawn wins the race.

`onUnmatched: spawn` is what makes bind routing self-healing: the first event on a PR spawns the owner session, and every later event folds into it.

## Folding: one artifact, one conversation

The combination of bind-at-spawn, `bind` routing, and `onUnmatched: spawn` produces the pattern most factories converge on — **events fold forward into the session that owns the work** instead of fanning out into parallel sessions:

```yaml .auto/agents/reviewer.yaml theme={null}
triggers:
  - events:
      - github.pull_request.opened
      - github.pull_request.synchronize
      - github.pull_request.reopened
    connection: github-acme
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: spawn
  - event: github.issue_comment.created
    connection: github-acme
    where:
      $.github.auto.authored: false
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
```

One reviewer session owns each PR across pushes: a new head does not spawn a fresh review, it arrives as a message in the existing review's context. The platform reinforces this with two delivery-side guards: repeated GitHub comment and review payloads with an unchanged body are suppressed as duplicates before they reach a session, and `github.check_run.completed` / `github.workflow_run.completed` deliveries defer until the session is idle rather than interrupting the turn in progress — CI results queue up instead of derailing the work that produced them.

A bind route can also end the relationship: `release: true` delivers the event and then releases the binding (deliver-then-release), which is how "close out when the PR merges" is expressed.

## Attribution: knowing who authored what

Agents produce events too — an agent's PR comment or Slack reply comes back through the same webhooks as human activity. Every normalized payload therefore carries attribution metadata, and correct triggers filter on it:

* **`authored`** — `$.auto.authored` (chat) / `$.github.auto.authored` (GitHub) is `true` when the platform itself authored the content. Deliver triggers on chat messages with `attributedSessions` are *required* at apply time to filter `"$.auto.authored": false`; without it an agent would react to its own replies.
* **`attribution`** — on authored content, the originating session (`{ sessionId, agentName }`). The router uses it for loopback suppression: an authored message is never delivered back to the exact session that produced it, and never spawns a new session of the agent that authored it. Other sessions in the same thread still observe it — agents can talk to each other.
* **`attributions`** — the sessions currently attributed to the event's context (for chat, the sessions bound into the thread). This is what `routeBy: attributedSessions` resolves, and what the spawn/deliver split filters on: a spawn trigger takes `"$.auto.attributions": { exists: false }` (nobody owns this thread yet) while its sibling deliver trigger takes `{ exists: true }` — apply enforces that the two are mutually exclusive so one message cannot both spawn and deliver.

On GitHub, attribution survives the platform's shared bot identity: every agent-authored body is stamped with a hidden, machine-readable marker (plus a visible header naming the agent and session), and webhook ingress parses it back into `github.auto.attribution` so routing and filters see through the shared GitHub App login.

## Where to go next

<CardGroup cols={2}>
  <Card title="Routing" href="/concepts/routing">
    Production rules for spawn, deliver, and bind once a factory grows past its first agent.
  </Card>

  <Card title="Binding" href="/concepts/binding">
    Spawn once, bind to an artifact, and route follow-ups into the same session.
  </Card>

  <Card title="Trigger reference" href="/reference/triggers">
    Every trigger field: where-filter grammar, checks, auth, routing schemas.
  </Card>

  <Card title="Event catalog" href="/reference/events/github">
    Every event key with its payload shape, filter paths, and bind target.
  </Card>

  <Card title="Sessions" href="/concepts/sessions">
    What a spawned session is, its lifecycle, and how messages are injected.
  </Card>

  <Card title="Connections and identities" href="/concepts/connections-and-identities">
    The grants that events arrive through and the identities agents act as.
  </Card>
</CardGroup>
