CoordinateDocs
Build · Triggers
Build

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.