Skip to main content
The triggers: array on an agent declares which events wake it and what happens when they arrive: start a fresh session, deliver into a live one, or continue the session bound to the event’s target. This page is the exhaustive field reference for that block. For the conceptual walkthrough, read triggers and events; for the payload of each event, see the event catalog. A trigger has three parts: what to listen for (event, where, connection), what to say (message or attachedUserPrompt), and where to send it (routing).
.auto/agents/pr-review.yaml

Two trigger shapes

Every entry in triggers: is one of two shapes:
  • Event trigger — listens on one event (or a list of events) from GitHub, chat, Linear, a custom webhook endpoint, or auto’s own lifecycle.
  • Heartbeat triggerkind: heartbeat with a cron expression; no event at all. See heartbeat triggers.
Both shapes are validated strictly: an unknown key inside a trigger fails apply.

Event trigger fields

string
The event key to listen on, e.g. github.pull_request.opened or chat.message.mentioned. Trimmed, non-empty. Exactly one of event or events is required — declaring both fails with “Use either event or events, not both”, and declaring neither fails with “Trigger requires event or events”.
string[]
A list of event keys sharing this trigger’s filter, message, and routing. At apply time the definition expands into one stored trigger per key; entries must be unique. Use this when several lifecycle events deserve identical handling:
string
The name of the connection resource this trigger listens through — the GitHub installation, Slack workspace, or Linear organization that scopes it. github.*, linear.*, and chat.* events must resolve to an active connection at apply time: name it here, or omit connection when the project has exactly one matching connection. Zero matching connections — or several, with no name to disambiguate — fails apply. auto.* and webhook.* events take no connection. See event key prefixes.
boolean
When true, apply silently skips this trigger if the referenced connection has no active grant instead of failing the whole apply. The skipped trigger is omitted from the applied agent and re-activates on the next apply or GitHub Sync run once the connection is set up. Useful in shared fragments and templates that reference connections a project may not have yet.
string
Names a custom webhook endpoint for webhook.* events. Apply creates or adopts the endpoint and returns a receipt containing its ingest URL (POST /api/v1/webhook-endpoints/{slug}/events). Pair with auth when the endpoint does not exist yet. See cron and webhooks for the full ingest contract.
object
How callers of a custom webhook endpoint authenticate. A discriminated union on kind:secretRef names a project secret that holds the signing key or token. Creating a new endpoint requires auth — applying a trigger that names a nonexistent endpoint without one fails with “Webhook endpoint … does not exist; include auth to create it”. A trigger reusing an existing endpoint may omit auth (the endpoint’s stored auth applies); declaring auth that differs from the stored endpoint fails apply.
object
default:"{}"
A map of payload paths to match clauses. All clauses must match (logical AND); an empty or omitted where matches every event on the key. See where filters.
string
A template (1–20,000 characters) rendered against the normalized event payload and delivered to the session as the triggering message. {{dot.path}} tokens resolve into the payload — {{github.pullRequest.number}}, {{message.text}}. Templates prefixed {{payload.…}} are rejected at apply time; the payload is the template root. See trigger messages for rendering rules and what happens when message is omitted.
string
A verbatim (non-template) start message, 1–20,000 characters of non-whitespace text. Legal only on spawn-routed triggers — any other routing kind fails apply with “Attached user prompt is supported only for spawn routing”. When set, it replaces the delivered start message, and the rendered message template is demoted to hidden context. See attached user prompts vs. hidden trigger context.
object[]
GitHub check runs this trigger manages on the pull request that fired it. Legal only when every event on the trigger starts with github.pull_request. — anything else fails apply with “Trigger checks are only supported for GitHub pull request events”. Each entry:See PR checks for the runtime lifecycle.
boolean
default:"false"
Marks this trigger as a catch-all: it fires only when no non-fallback trigger on the same event key (and webhook endpoint) matched its where. The fallback trigger’s own where still applies as an extra gate. See fallback triggers.
object
required
What to do with a matched event: kind: spawn, kind: deliver, or kind: bind. See routing.
string
Authoring-only identity for import merging: triggers merge across imports by name, falling back to event, then the joined events list, then cron:<cron>:<timezone>. The name is stripped before validation and never stored. remove.triggers deletes by the same key.

Heartbeat triggers

A heartbeat trigger wakes the agent on a schedule instead of an external event. Apply creates one Temporal Schedule per heartbeat trigger and routes each tick through the normal trigger pipeline under a synthetic event key (heartbeat.<agentResourceId>.<ordinal>) that only this trigger can match.
"heartbeat"
required
Discriminator selecting the heartbeat shape.
string
required
The cron expression, 1–512 characters. auto passes the string verbatim to the Temporal Schedule, so it accepts what the Temporal server accepts — classic 5-field cron (*/15 * * * *, 0 9 * * 1-5) is the form used in production configs.
string
default:"UTC"
IANA timezone the cron expression is evaluated in, 1–128 characters.
message, where, fallback, and routing work exactly as on event triggers. event, events, connection, optional, endpoint, and auth are forbidden on a heartbeat, and checks is rejected because a heartbeat is not a pull-request event. The tick payload is { trigger: "heartbeat", heartbeat: { scheduleId, agentResourceId, triggerOrdinal, scheduledAt } }, so {{heartbeat.scheduledAt}} is the useful template token. Ticks deduplicate on <scheduleId>:<scheduledAt>.
.auto/agents/chief-of-staff.yaml
Here onUnmatched: drop is deliberate: when the agent’s one live session has archived itself between batches, the cron tick must not resurrect it.

Event selection

Event keys are dot-separated strings. The first segment names the source and decides what the trigger needs at apply time: Any other prefix fails apply with Unsupported trigger event provider.
Beyond the prefix, event keys are not validated against a closed catalog. A typo’d key (github.pull_request.close instead of .closed) applies cleanly and simply never fires. Check keys against the event catalog when a trigger seems dead.

Where filters

where narrows a trigger to the payloads it should act on. Keys are payload paths; values are match clauses. Every clause must match for the trigger to fire. Path grammar. A key is either a single bare key with no dots (type) or a $.-prefixed dot path ($.github.action, $.linear.issue.labelNames). Segments must be non-empty and free of surrounding whitespace. Anything else fails apply with “Trigger filter paths must be a single bare key or use $.path.segments”. Clause forms. A clause is a bare scalar for strict equality, or an object with exactly one operator: Details worth knowing:
  • A path that traverses a missing or non-object segment resolves to undefined. Undefined never satisfies equality, contains, or in — but it does satisfy { exists: false } and notIn. That makes notIn the right operator for excluding a value while still matching older payloads that predate the field:
  • changedTo derives its comparison path by inserting previous before the final segment: $.a.b.c reads $.a.b.previous.c. It only works on payloads that actually carry such a sibling object; most sources do not. For Linear label transitions, use $.linear.updatedFrom.… with contains instead (see Linear events).
  • Filters match against the normalized payload documented per event in the catalog, not the raw provider webhook — though the raw body is usually available under $.raw.…. For custom webhooks, the payload is the raw request body, so paths are whatever the caller posts.
Common filter idioms from production configs:

Routing

routing.kind selects one of three behaviors. All three funnel into the same decision: create a new session, deliver a message into one or more existing sessions, or record the event as unmatched.

kind: spawn

Start a fresh session carrying the event. Each matched event creates one session (idempotent per event + trigger, so a retried delivery never double-spawns).
object
Optional bind-at-spawn: { target, context?, eventContext? }. After creating the session, the router writes a session binding for the event’s routing target of the given type, so later bind-routed events resolve back to this session. target is one of the binding target types; context is a JSON object stamped on the binding relationship; eventContext annotates the emitted auto.session.binding.bound transition.
The bind block also accepts inline lifecycle: manual | held and continuity: session | agent, which compile into the agent’s bindings: map (a conflicting explicit declaration for the same target and field fails apply). agent.singleton cannot take inline policy.
Spawn races convert to deliveries rather than failing or duplicating: when a concurrent session already claimed the chat thread, the agent’s concurrency slot, or the trigger’s continuation target, the spawn is converted into a delivery to the winning session, so the event is never lost and never handled twice.

kind: deliver

Deliver the event as a message into existing live session(s).
object
The resolution strategy:Legacy spellings allLiveRuns and attributedRuns still parse and normalize to the canonical kinds.
string
default:"drop"
Policy when no live session resolves: drop, warn, error, or spawn. See onUnmatched.
object
Deliver-then-bind sugar: { target, lifecycle?, continuity? } on a deliver arm means the delivered session also binds the event’s chat thread. The target must be a chat-thread target (slack.thread today), and the declaration compiles bind: onMention into the agent’s bindings: map. Used on mention triggers so the thread routes future events back to whichever session handled the mention:
Two authoring rules protect chat deliver triggers from feedback loops, enforced at apply time:
  • A chat.message.* trigger with routeBy: attributedSessions must filter "$.auto.authored": false — otherwise the agent’s own replies in the thread would route back into it.
  • A spawn trigger and an attributedSessions deliver trigger on the same event must use mutually exclusive "$.auto.attributions" filters: the spawn requires { exists: false }, the delivery { exists: true }. Otherwise one message could both spawn a new session and deliver into the existing one.

kind: bind

Resolve the event’s routing target — the PR, issue, or thread the event is about — to the session bound to it for this agent, and deliver there. This is the continuation primitive: one session owns a PR across pushes, comments, reviews, and check results.
string
required
The binding target type to resolve: github.pull_request, github.issue, slack.thread, agent.singleton, linear.issue, or auto.session.
string
default:"drop"
Policy when no session is bound to the target. spawn gives the “bind or start fresh” pattern: the first PR event spawns a reviewer, and every later event folds into it.
boolean | object
default:"false"
Declarative deliver-then-release: after the router routes this event, the platform releases the target’s active binding as a follow-up. The bound session receives the message first; true normalizes to { context: null }, and { context: { … } } annotates the release. A no-op when no active binding exists. Rejected on target: agent.singleton (the concurrency slot is reconciler-owned, not trigger-releasable). Required companion of lifecycle: held bindings: a held target must have at least one bind-routed trigger with release: true so the platform can let go of it.
object
Observer machinery for agents that watch other sessions. Legal only when target: auto.session and the trigger’s event is one of auto.session.binding.bound, auto.session.binding.updated, or auto.session.binding.unbound — the lifecycle events whose payloads carry the observed binding.target. Discriminated on action:
  • { action: "bind", context?, eventContext? } — after delivering the transition to the observer session, the platform binds that observer to the target carried by the observed event.
  • { action: "unbind", eventContext? } — releases the observer’s own active claim on that target (idempotent when absent; another session’s claim is left untouched).
This is how an orchestrator adopts a worker’s PR the moment the worker declares it ready, without polling:
The bind arm also accepts inline lifecycle and continuity directly on the routing object, folded into bindings: the same way as the spawn sugar.
Legacy routing spellings from older configs and pinned template versions still parse and normalize: deliver + routeBy: { kind: singleton } becomes bare deliver; deliver + routeBy: { kind: ownedArtifact, artifactType } becomes bind over that target; kind: deliverOrSpawn becomes deliver + onUnmatched: spawn. Write the canonical forms in new configs.

Bind targets

At ingest, every event derives its routing target fail-safe from its payload: An event with no derivable target can never match a bind route — it always lands on onUnmatched. Route those events with spawn or deliver instead. The per-event catalog pages list the target for every key. Sessions acquire bindings through bind-at-spawn (routing.bind on a spawn trigger), deliver-then-bind sugar, observedTarget actions, and the session calling the auto.bind runtime tool itself (see auto tools). Per-target policy — lifecycle, continuity, auto-bind modes — lives in the agent’s bindings: map, documented in the agent file reference.

onUnmatched

deliver and bind routes name a policy for events that resolve to no live session: onUnmatched: spawn is legal for any agent. On a concurrency-capped agent, an unmatched slot delivery’s spawn additionally claims the agent slot inside the session-creation transaction — and converts to a delivery if a concurrent spawn wins the slot first, so the cap holds. Choose the policy per event, not per agent. From the production chief-of-staff config: a human reply in a subscribed thread uses onUnmatched: spawn (a message during a replacement window must never drop — it spawns the successor carrying it), while a lone emoji reaction uses onUnmatched: drop (never boot a whole session for one reaction).

Trigger messages

Rendering

message templates render {{dot.path}} tokens against the normalized event payload. Strings, numbers, and booleans interpolate directly; objects and arrays are JSON-stringified; a missing or null path renders as an empty string. Whitespace inside braces is tolerated: {{ github.action }} equals {{github.action}}. The template root is the payload — write {{github.pullRequest.number}}, never {{payload.github.pullRequest.number}}. The payload. prefix is rejected at apply time (it is correct only in mount ref templates, which render against a different wrapper — see mounts). Delivered messages get useful context appended automatically when the rendered text doesn’t already contain it: chat events append Channel:, Thread:, and Message: id lines plus one Attachment: line per file; GitHub comment and review events append Attachment: lines naming the brokered download tool for GitHub-hosted files.

What a spawned session receives

For a spawn route (including onUnmatched: spawn), the start message resolves in this order:
  1. attachedUserPrompt, verbatim, when the trigger declares one.
  2. The rendered message template — except on a bind route’s unmatched spawn, where a defined agent initialPrompt wins instead: a bind trigger’s message is continuation text for an already-briefed session, so first contact starts from the agent’s own kickoff prompt.
  3. The agent’s initialPrompt, rendered against the same payload.
  4. Text recovered from the payload itself (a chat message’s text and author, or a reaction description).
  5. A generic fallback instructing the agent to review the triggering event and act.
A trigger always produces a working start message — a fired trigger never strands its event on a session that boots and waits. For deliver and bind routes into a live session, the rendered message (or the payload-derived fallback chain, when message is omitted) is injected as a message into the running conversation.

Attached user prompts vs. hidden trigger context

message and attachedUserPrompt fill different roles on a spawn trigger:
  • message is operational text: event data plus handling instructions, written for the agent.
  • attachedUserPrompt is the text a human actually typed — a form submission, a command, a request captured upstream — that should appear as the session’s user prompt, untouched by templating.
When both are set, the session starts with attachedUserPrompt as its visible first message, and the rendered message template is stored as trigger-message context — injected into the session’s system prompt under a “Trigger message context” heading, marked as hidden context the trigger supplied. The agent sees both; the conversation transcript leads with the human’s words. The attached prompt (rather than the trigger machinery text) also seeds the session’s generated display title.

Interrupt vs. deferred injection

A message delivered into a live session normally interrupts the in-flight turn so the event steers immediately. Two GitHub event types instead defer until the session is idle: github.check_run.completed and github.workflow_run.completed. CI results are not worth aborting a mid-turn tool call over; check action button events (github.check_run.requested_action.*) still interrupt because a human pressed a button. This mapping is fixed per event type and not configurable.

PR checks

A trigger on github.pull_request.* events can declare checks: — GitHub check runs that auto creates on the PR and the routed session reports on. This puts the agent’s verdict in the merge box, next to CI. The lifecycle:
  1. When the trigger routes an event, auto creates each declared check on the PR head in queued state.
  2. The session receives four checks.* runtime tools: checks.list, checks.begin, checks.success, and checks.failure. The check’s instructions tell the agent when to call them.
  3. checks.begin moves the check to in-progress; checks.success / checks.failure conclude it with a title and summary shown on GitHub.
  4. Timeouts backstop an agent that never reports. beginTimeout (alias timeout) runs from check creation until checks.begin; completeTimeout runs from begin until conclusion. When a deadline expires, the check auto-completes with the configured conclusion (success, failure, or skipped) and a “Check begin timed out” / “Check completion timed out” title.
A new qualifying event on the same PR (for example a push) supersedes the previous check cycle and starts a fresh one, so the check always reflects the current head.
.auto/agents/pr-review.yaml

Fallback triggers

For each incoming event, the router first evaluates every normal trigger on that event key (scoped to the same webhook endpoint, for webhook.* events). Only when none of them matched their where filters do the fallback: true triggers get a chance — and each fallback’s own where still applies. This is the one cross-trigger negation the per-payload matcher cannot express (“nothing else handled this”), which is why it is an explicit flag rather than an empty filter. The main use is custom webhook endpoints receiving unshaped payloads: specific triggers match known event shapes, and a fallback on webhook.received catches everything else for triage. See cron and webhooks.

Concurrency interaction

Triggers and the agent’s concurrency cap are designed together; see runtime controls for the cap itself.
  • Slot delivery. A deliver route with no routeBy resolves “the agent’s one live session” via the agent-wide agent.singleton binding. Apply rejects it unless the agent declares concurrency: 1 or some slot delivery on the agent carries onUnmatched: spawn (whose spawn claims the slot itself): without a cap, “the one live session” is not a defined thing.
  • Slot claiming. On a capped agent, every spawn path — a spawn route, or an unmatched slot delivery’s onUnmatched: spawn — claims the slot in the session-creation transaction. A spawn that loses the claim race converts into a delivery to the winner, so events are neither dropped nor duplicated and the cap is never exceeded.
  • Wind-down semantics. When the slot member archives itself, the next slot delivery is unmatched and its onUnmatched policy decides what happens: spawn triggers (mentions, new work) revive the agent with a fresh session carrying the event; drop triggers (heartbeats, reactions) let it rest.
  • Fan-out routes are uncapped reads. routeBy: allLiveSessions and attributedSessions deliver to whatever live sessions exist; on a concurrency: 1 agent that is at most one.
A replace: auto agent adds one more interaction: platform-initiated replacement of the slot member briefly leaves no live session, so any human-facing slot delivery should carry onUnmatched: spawn to guarantee messages arriving in that window spawn the successor instead of dropping.