The event pipeline
Every event — a GitHub webhook, a Slack message, a cron tick, a custom webhookPOST — follows the same path:
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 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 — an installed GitHub App, a Slack workspace, a Linear workspace, a Telegram bot. The trigger names the connection it listens through:- 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. - Chat (
chat.*) — Slack, Discord, and Telegram all normalize into one provider-neutral contract:chat.message.{channel,direct,mentioned,subscribed,edited}andchat.reaction.{added,removed}. Theconnection:field selects the workspace; filter on$.chat.providerwhen one agent listens across providers. See Slack events and Telegram events. - Linear (
linear.*) —linear.issue.createdandlinear.issue.updated. See Linear events.
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 HTTPPOST 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 for the endpoint mechanics.
Cron (heartbeat)
A heartbeat trigger has no event key — it declareskind: 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.
Lifecycle events
The platform emits its ownauto.* 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.
How a trigger matches
A trigger fires when all of the following hold:- Event key match. The trigger’s
event:(or one of itsevents:) equals the incoming key exactly. Anevents:list expands into one stored trigger per key. - Origin match. The event arrived through the trigger’s declared connection or webhook endpoint. Two agents listening on
chat.message.mentionedthrough different Slack workspaces never see each other’s traffic. wherefilter match. Every clause in thewheremap must hold against the payload. Keys are$.dot.pathsinto the payload; clauses are scalar equality or one ofcontains,exists,in,notIn,changedTo. An emptywherematches everything.
- 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: truefires 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 onwebhook.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.
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.
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: 1agent (or with a slot-claimingonUnmatched: spawn). See runtime controls.
Choosing a routing kind
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:
.auto/agents/reviewer.yaml
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) istruewhen the platform itself authored the content. Deliver triggers on chat messages withattributedSessionsare 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 whatrouteBy: attributedSessionsresolves, 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.
github.auto.attribution so routing and filters see through the shared GitHub App login.
Where to go next
Routing
Production rules for spawn, deliver, and bind once a factory grows past its first agent.
Binding
Spawn once, bind to an artifact, and route follow-ups into the same session.
Trigger reference
Every trigger field: where-filter grammar, checks, auth, routing schemas.
Event catalog
Every event key with its payload shape, filter paths, and bind target.
Sessions
What a spawned session is, its lifecycle, and how messages are injected.
Connections and identities
The grants that events arrive through and the identities agents act as.