# Triggers

> When an event arrives or a query becomes true, run actions.

Create triggers on **Triggers**, or with the `trigger` tool (`POST /v1/trigger`). The same name replaces a trigger; `untrigger` deletes, pauses, or resumes one.

On **Triggers**, + starts with a sentence: describe what you want ("text me when one of my pull requests is ready to merge") and Claude Haiku drafts the trigger from your sources, event types, and secret names. Coordinate checks the draft, dry-runs it against the log, and says it back as *When … then …*; nothing is saved until you choose **Create trigger**. **Set it up by hand** opens the form. Over the API, `draft_trigger` (`POST /v1/draft_trigger`) returns the same draft. Drafting needs `ANTHROPIC_API_KEY` on the deployment; your agent can always write the trigger itself.

## When an event arrives

```json
{"name": "pr-ready",
 "when": {"event": {"source": "github", "type": "github.pr.readiness", "where": {"payload.ready": {"eq": true}, "payload.author": {"eq": "derekmeegan"}}}},
 "then": {"slack": {"webhook_url": "secret:SLACK_WEBHOOK", "message": "PR #{{event.payload.number}} is ready: {{event.payload.url}}"}}}
```

A **matcher** is `{source?, type?: string | [...], where?: Condition}`, where paths are `payload.*`, `metadata.*`, `type`, or `source`. Events on Coordinate's own source match only when the matcher says `"source": "coordinate"`.

## When a query becomes true

```json
{"name": "spend",
 "when": {"query": {"sql": "SELECT local_date AS day, SUM(json_extract(payload, '$.cost_microusd')) / 1e6 AS dollars FROM events WHERE type = 'claude_code.usage' GROUP BY day ORDER BY day DESC LIMIT 1", "test": {"dollars": {"gte": 20}}, "key": "day"}},
 "then": {"emit": {"type": "budget.exceeded", "payload": {"day": "{{row.day}}"}}}}
```

The query runs after every append. Without `key`, the trigger holds when the query returns a row (and the first row passes `test`, if given). With `key`, each row is tracked on its own by that column. It fires when a result goes from false to true; a new query trigger's first result is its baseline.

## Conditions

`{"<path>": {"eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "exists" | "in" | "contains" | "starts_with" | "ends_with" | "matches": value}}` (all must hold), or `{"all": [...]}`, `{"any": [...]}`, `{"not": {...}}`. Every operator except `exists` is false for a missing field, and there is no type coercion. `contains` is a substring of a string or a member of a list, `starts_with` and `ends_with` compare strings, and `matches` is a case-insensitive regular expression. For example, a release that is not alpha or canary: `{"not": {"payload.tag": {"matches": "alpha|canary"}}}`.

## Firing

- `fires`: `"on_enter"` (default: every matching event, or every false-to-true change) or `"once"` (fire, then stop).
- `paused: true` saves without running. Resuming starts from now.
- `dry_run: true` shows what would have matched recently, or the query's current result, without saving.
- History (events marked `metadata.backfill`) never fires a trigger, and a trigger never fires on events it caused itself. Chains of triggers stop after 8 hops.

Each firing writes `trigger.fired`, and each action `action.succeeded` or `action.failed`, all correlated by the firing's id.
