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

# Lifecycle Events

> auto's internal events — apply lifecycle, connection lifecycle, session-binding transitions, and onboarding — with payloads, placeholders, and example triggers.

Beyond provider webhooks, auto emits its own `auto.*` events when platform state changes: a resource apply starts or finishes, a connection becomes usable in a project, a session binding transitions, an onboarding run advances. Agents use these to continue work on facts instead of polling or asking a human to say "done". This page catalogs each event, what emits it, and the trigger shapes that consume it.

`auto.*` triggers need no `connection:` — the events are internal and routed per project through the same pipeline as every other event. Payload placeholders render against the bare payload (`{{apply.operationId}}`, `{{provider}}`), and `where` filters address it with `$.` paths, exactly as on provider events.

## Apply lifecycle

### `auto.project_resource_apply.{started,completed,failed}`

Emitted by the apply workflow for every non-dry-run apply of project resources — whether it came from [GitHub Sync](/concepts/github-sync) after a merge or from an apply the platform ran outside Sync. Dry runs emit nothing. Each event is deduplicated on the apply's operation id plus status, so one apply produces at most one `started` and one terminal `completed` or `failed`.

The payload's stable core is the `apply` object:

```json theme={null}
{
  "type": "auto.project_resource_apply.completed",
  "artifact": {
    "type": "github.pull_request",
    "externalId": "github:12345:acme/widgets:pull/12"
  },
  "apply": {
    "operationId": "github-sync:push:binding-id:abc123",
    "status": "completed",
    "auditAction": "github_sync.apply",
    "dryRun": false,
    "prune": true,
    "source": "inline",
    "request": {
      "counts": { "resources": 2, "delete": 0, "assets": 0 },
      "resources": [
        { "kind": "agent", "name": "coder" },
        { "kind": "environment", "name": "agent-runtime" }
      ],
      "delete": []
    },
    "plan": {
      "counts": {
        "create": 1, "update": 2, "archive": 0, "unchanged": 4,
        "pruned": 0, "diagnostics": 0, "triggers": 3
      },
      "changedResources": [
        { "action": "create", "kind": "agent", "name": "coder" }
      ],
      "changedResourcesOmitted": 0,
      "createdAgentNames": ["coder"]
    }
  },
  "project": {
    "organizationId": "org_...",
    "projectId": "proj_..."
  }
}
```

Field notes:

* `apply.auditAction` is `github_sync.apply` for Sync-driven applies and `project_resources.apply` for applies that ran outside Sync — the filter that separates "a merge landed" from every other apply path.
* `apply.plan` is present only on `completed`; `apply.error { name, message }` is present only on `failed` (with a structured `diagnostic` attached for validation failures). `started` carries the `request` summary but no plan.
* `apply.plan.changedResources` is a bounded preview (up to 50 changed resources, with `changedResourcesOmitted` counting the rest). `apply.plan.createdAgentNames` is the complete sorted list of agents the apply created — use it, not the preview, for exact first-install routing like `$.apply.plan.createdAgentNames: { contains: "coder" }`.
* The top-level `artifact` is present when the apply carries one. GitHub Sync resolves the merged pull request for production-branch pushes (with a merge-commit fallback for the async commit→PR association), so a Sync apply event usually carries the merged PR's artifact — which means `bind` routing can deliver it straight back to the session that owns that PR.

Placeholders: `{{apply.operationId}}`, `{{apply.plan.counts.create}}`, `{{apply.plan.counts.update}}`, `{{apply.error.message}}`.

The canonical consumer is a session that shipped a `.auto/` change by PR and wants to resume the moment Sync applies it:

```yaml theme={null}
triggers:
  - event: auto.project_resource_apply.completed
    where:
      $.apply.auditAction: github_sync.apply
    message: |
      GitHub Sync applied project resources.

      Operation: {{apply.operationId}}
      Created: {{apply.plan.counts.create}}
      Updated: {{apply.plan.counts.update}}
      Archived: {{apply.plan.counts.archive}}

      Verify the deployed resources and continue the workflow without
      asking the user to confirm the merge.
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
```

For applies without an artifact (applies outside Sync, or the rare Sync apply whose PR could not be resolved), use a project-scoped route instead — `deliver` with `routeBy: { kind: allLiveSessions }` and a narrow `where`, or `spawn`. For `failed` events, surface `{{apply.error.message}}` first, then use `apply.request.resources` and `apply.request.delete` to identify which file or delete operation needs repair.

## Connection lifecycle

### `auto.connection.established` / `auto.connection.removed`

A connection event fires exactly when the set of connections usable by a project changes. `established` means "this connection just became usable here"; `removed` means it no longer is. Nothing a project can observe changed — a token refresh, a repeat allow, a same-account re-auth — means no event.

`established` fires once per grant × project when:

* a provider OAuth flow completes with the project allowed (Slack, Linear, built-in MCP providers, …)
* a GitHub App installation is connected to the project
* a Telegram manager bot or model-provider API token is connected with the project allowed
* an existing org-level connection is allowed into the project
* an agent-tool MCP OAuth connection completes

`removed` fires once per grant × project when availability ends: `reason: "revoked"` when the grant is removed, `reason: "replaced"` when a replace flow retires the old grant (the replacement then fires `established` with `reestablished: true`). Internal platform grants never emit either event, and agent-tool MCP connections currently have no removal path — they only emit `established`.

Two payload shapes, discriminated by `kind`. Provider grants:

```json theme={null}
{
  "type": "auto.connection.established",
  "kind": "provider",
  "provider": "slack",
  "connection": "slack",
  "providerGrantId": "pg_...",
  "credentialKind": "oauth",
  "externalAccount": {
    "providerAccountId": "T0123ABC",
    "loginOrName": "acme",
    "accountType": "workspace"
  },
  "scopes": ["chat:write"],
  "resourceSelection": { "kind": "all" },
  "requester": { "kind": "user", "userId": "user_..." },
  "reestablished": false,
  "project": { "organizationId": "org_...", "projectId": "proj_..." }
}
```

Agent-tool MCP connections:

```json theme={null}
{
  "type": "auto.connection.established",
  "kind": "mcp",
  "provider": "mcp",
  "connection": "my-linear-mcp",
  "tool": "linear",
  "serverUrl": "https://mcp.linear.app/sse",
  "credentialKind": "oauth",
  "scopes": ["read", "write"],
  "reestablished": false,
  "project": { "organizationId": "org_...", "projectId": "proj_..." }
}
```

`removed` payloads drop `scopes`, `resourceSelection`, `reestablished`, and `requester` and carry `reason` instead. On `established`, `requester` is present when the flow had an authenticated actor: `{ kind: "user", userId }` for an auto user, `{ kind: "provider", provider, externalId, displayName, accountRef }` for a provider identity.

<Warning>
  Select connections with `where:` filters on the payload — never with the trigger-level `connection:` binding. That binding resolves a connection name to a grant at apply time, which cannot work for a connection that does not exist yet, the exact situation this event exists for.
</Warning>

A working consumer from this repo's own production config — an observer that announces each new connection in Slack (trimmed):

```yaml .auto/agents/connection-observer.yaml theme={null}
name: connection-observer
session:
  archiveAfterInactive:
    seconds: 3600
displayTitle: "Connection feed: {{provider}} / {{connection}}"
systemPrompt: |
  You report newly established project connections to Slack and do
  nothing else. Send exactly one concise message per delivery, then
  archive your session. Never include credentials, tokens, scopes,
  server URLs, or the raw event payload.
tools:
  auto:
    kind: local
    implementation: auto
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
      optional: true
triggers:
  - event: auto.connection.established
    message: |
      Report this newly established project connection to Slack.

      - Kind: {{kind}}
      - Provider: {{provider}}
      - Connection: {{connection}}
      - External account: {{externalAccount.loginOrName}}
      - Tool: {{tool}}
      - Re-established: {{reestablished}}
    routing:
      kind: spawn
```

To watch one provider, add `where: { $.provider: slack }`. This composes with `optional: true` connection-bound triggers: an agent whose Slack triggers are marked optional (so apply skips them while Slack is absent) can carry an `auto.connection.established` trigger that wakes it to bootstrap the moment Slack arrives — and in a Sync-enabled project, an established event also schedules a regating Sync run automatically, so those optional triggers activate without a new commit.

Semantics worth knowing:

* **Forward-looking only.** Triggers fire on changes after they exist. An agent applied after the connection was made gets no synthetic catch-up event; check `auto.connections.list` at first run for "on connect or already connected" behavior.
* **Delivery is per project.** An org-level grant allowed into three projects fires three events, each visible only to that project's agents.
* **Dedup is durable.** Events dedup on the grant × project access-row lifetime, so retried flows and repeat allows cannot double-fire; removing and re-allowing mints a fresh row and fires again.
* **Emission is fail-safe.** A connection flow never fails because event dispatch did; a dispatch failure after the record is written is retried by the pending-event sweeper.

### `auto.connection.authorization_attempt.finished` (not authorable)

When a session starts an OAuth flow through the [runtime auto tools](/runtime/auto-tools), the platform binds that session to the authorization attempt and delivers the terminal outcome — completed, failed, or expired — straight back to it. This continuation is automatic: agents do not declare a trigger for this event, and it answers "what happened to the authorization my session started?" rather than "what connections changed in this project?". A session that also declares an `auto.connection.established` trigger receives both messages, clearly distinct.

## Session-binding lifecycle

### `auto.session.binding.{bound,updated,unbound}`

Emitted transactionally on every binding transition: a session claims a target (a PR, an issue, a thread, an agent slot), a binding's context updates, or a binding releases. The payload describes the holder session and the transition:

* `session` — the holder: `{ id, agentResourceId, agent, projectId, bindingRevision }`
* `binding` — `{ id, revision, target { type, externalId }, source, releasePolicy, continuity, metadata, context, boundAt }`, plus `releasedBy` and `unboundAt` on `unbound`
* `transition` — `{ kind, cause, context }`, with `previousContext`, `previousHolderSessionId`, or `nextHolderSessionId` when applicable

`transition.cause` records what drove the transition: `manual_bind`, `manual_unbind`, `manual_update`, `attributed_event`, `mention`, `observed_binding_event`, `session_spawn`, `trigger_spawn`, `chat_send`, `trigger_release`, `archive`, `takeover`, `rollover`, or `system`.

Useful filters: `$.binding.target.type: github.pull_request`, `$.transition.kind: bound`, `$.transition.cause: trigger_spawn`.

These events route to the **holder session as an `auto.session` bind target**, which makes their consumer shape unusual: the listener is an *observer agent* whose sessions are bound to the sessions they watch (for example, a coordinator observing the workers it spawned — spawned-session observation is on by default via the agent's `session.observeSpawnedSessions`). The observer declares a `bind` route over `target: auto.session`, and can mirror the observed session's claims with `observedTarget`:

```yaml theme={null}
triggers:
  - event: auto.session.binding.bound
    where:
      $.binding.target.type: github.pull_request
      $.transition.cause: trigger_spawn
    message: |
      Observed session {{session.id}} ({{session.agent}}) bound
      {{binding.target.type}} {{binding.target.externalId}}.
      Track this pull request as part of the work you coordinate.
    routing:
      kind: bind
      target: auto.session
      onUnmatched: drop
      observedTarget:
        action: bind
```

`observedTarget: { action: bind }` claims the observed event's own `binding.target` for the observer session, so later events about that PR also reach the coordinator; `action: unbind` releases the observer's claim. `observedTarget` is legal only on `bind` routes over `target: auto.session` listening on these three event keys.

## Onboarding

### `auto.onboarding.phase_changed`

Emitted transactionally whenever a project onboarding run's state changes. Payload: `project { organizationId, projectId }`, `run { id, teamId, commission, phase, evidence, revision, completedAt }`, `transition { previousPhase, phase, changed { phase, commission, evidence, completed } }`, and `session { id, agent }` identifying the writer. Deduplicated per run revision.

Useful filters: `$.transition.phase`, `$.run.teamId`. Routing: project-scoped `deliver` or `spawn` — the event carries no bind target. Most teams never author triggers on this event; it exists for onboarding orchestration agents.

## Quick reference

| Event key                                              | Emitted when                                                    | Bind target                                                |
| ------------------------------------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------- |
| `auto.project_resource_apply.started`                  | A non-dry-run apply begins                                      | `github.pull_request` when the apply carries a PR artifact |
| `auto.project_resource_apply.completed`                | The apply succeeds                                              | same                                                       |
| `auto.project_resource_apply.failed`                   | The apply fails                                                 | same                                                       |
| `auto.connection.established`                          | A connection becomes usable in a project                        | —                                                          |
| `auto.connection.removed`                              | A connection stops being usable (`reason: revoked \| replaced`) | —                                                          |
| `auto.session.binding.bound` / `.updated` / `.unbound` | A session binding transitions                                   | `auto.session` (the holder)                                |
| `auto.onboarding.phase_changed`                        | An onboarding run advances                                      | —                                                          |
| `auto.connection.authorization_attempt.finished`       | A session-started OAuth attempt ends                            | platform-held; not authorable                              |

For provider events see [GitHub events](/reference/events/github), [Slack events](/reference/events/slack), and [Linear events](/reference/events/linear); for schedules and custom webhooks see [Cron and webhooks](/reference/events/cron-and-webhooks).
