A trigger lets an agent act when nobody is talking to it. It binds a source to a continuation. The source is something that happens: a comment, a mention, a site update, a schedule, a webhook delivery, or another run finishing. The continuation is what to do about it: start a conversation, wake a parked run, or run a tool or script with no model at all. Triggers belong to one agent and live on its agents server. They are the one piece of standing authority an agent holds, so the security page covers them too.
The data model
A trigger is a row with a name, an enabled flag, a source, an optional prompt, and an optional continuation. The protocol types are in agents/protocol/src/index.ts:
type AgentTriggerInput = {
name: string
enabled?: boolean
source: AgentTriggerSource
prompt?: string | AgentPromptBlock[] // required for newThread; a recovery prompt for tool/script
continuation?: TriggerContinuation // omitted means newThread
}Prompts accept the same rich Seed block format as agent system prompts. A plain string is parsed as markdown. Blocks are rendered to markdown before the thread starts.
Sources
source | fires when | fields |
|---|---|---|
| a new comment appears on a resource |
|
| a comment or document mentions one of the listed accounts |
|
| new activity appears under a resource prefix |
|
| a clock says so |
|
| something POSTs JSON to the trigger's delivery URL | none; the secret is created with the trigger |
| a run of this account reaches a terminal status | optional |
A user-mention input still accepts the legacy singular mentionedAccount and turns it into the list. An empty list is rejected. Creating an agent with a signing identity also creates an enabled user-mention trigger that follows that identity. That is why mentioning an agent's account in a comment summons it.
Continuations
kind | what the firing does | model involved |
|---|---|---|
| starts a session whose first message is the prompt plus a | always |
| delivers | never, the run decides |
| calls one tool headlessly as a journaled workflow run: | never, unless |
| runs a workflow module ( | only if the script calls |
For tool, input defaults to the trigger event itself. When you give an input, it is a JSON template. String values "$event" and "$event.<path>" are replaced from the event. For a webhook, the posted JSON is "$event.payload". The tool name is checked when the trigger is written, because a headless firing has nobody to read a "no such tool" error. A script is checked by the workflow linter at write time: no Date, Math.random, timers, fetch, or imports, and exactly one default export. It is capped at the workflow source limit. The journal and tool document pages explain the run and the tool it calls.
onFailure is none or thread. With none (the default), the error is recorded on the firing and the trigger. With thread, the ordinary trigger thread starts with an automationFailure block inside its context and an instruction to read run:<id> and recover. The prompt is optional for headless kinds. When it is omitted, a default recovery prompt is stored, because the prompt is only used to escalate.
A common pattern: an agent authors a tool with write ~/tools/<name> and wires a webhook to it with continuation: {kind: 'tool', tool, input: {payload: '$event.payload'}, onFailure: 'thread'}. From then on, deliveries run its code directly. A model starts only when the code fails.
Firing
Every activation is a firing. It is a row in trigger_firings with a unique key (account, trigger, activityKey), so a retried feed page or a repeated schedule tick cannot fire twice. Activity firings use the feed event's identity. Schedule firings use schedule:<triggerId>:<scheduledAt>. A webhook delivery uses its Idempotency-Key when the sender supplies one. A comment that mentions someone produces two sibling feed events: the comment and a citation. The monitor collapses them onto one key, so a mention fires exactly once, whichever sibling arrives first.
A firing's status records what happened:
created: a thread was started.
running, then succeeded: a headless run.
delivered or no-listener: a wake.
error.
escalated: a headless run failed and a recovery thread was started.
The firing links its sessionId when it started a thread and its runId when it started a headless run. So read run:<id> and the trigger page can show what happened.
Chains are loop-guarded. A run-completed firing is skipped when its ancestry already contains the same trigger within 8 hops, so two triggers cannot feed each other forever.
The monitors
The activity monitor polls the Hypermedia server's activity feed, which is the ListEvents request of the Seed API. The settings are:
SEED_AGENTS_ACTIVITY_POLL_INTERVAL_MS: how often it polls (default 5 seconds).
SEED_AGENTS_ACTIVITY_PAGE_SIZE: events per page (50).
SEED_AGENTS_ACTIVITY_MAX_PAGES: pages per poll (5).
It only contacts the feed for accounts that have at least one enabled non-schedule trigger. Each event goes to two consumers through one shared matcher: trigger matching, and any run parked on ctx.waitForEvent({eventType, resource, author}) for that account. The first poll for an account sets a baseline watermark and only processes events created after the earliest enabled trigger. If the server was down, it backfills for up to one hour and then moves on. Watermarks are durable (activity_watermarks), so a restart resumes and does not replay. Polling depends on triggers, so an activity-shaped wait on an account with no enabled activity trigger only ends by timeout. Signal-shaped waits do not depend on polling.
The schedule monitor checks enabled schedule triggers on the same interval, records their firings, and disables a once trigger after it runs.
The HTTP server delivers webhooks synchronously. A request to POST /agents/api/webhooks/<triggerId>/<secret>, or to POST /agents/api/webhooks/<triggerId> with Authorization: Bearer <secret>, with a JSON body answers 202 {accepted, duplicate}. The body must be application/json with no content encoding. The secret is 43 URL-safe characters. It is generated with the trigger and shown on its page to accounts that can edit the agent. The client builds the delivery URL from the server's base.
Run-completed fires inline when a run is finalized. There is no polling.
A trigger thread is a normal session with origin: 'trigger'. It carries an AgentSessionTriggerContext (trigger, firing, activity summary, the full event). The UI renders it as a context card and the model sees it as <trigger_context>. Trigger and headless runs dispatch on the background queue with maxAttempts: 1, so a failed firing is not retried automatically. The operations page describes the queue.
Working with triggers
In the Seed app
The agent page's Triggers tab lists triggers. Each one opens in an editable detail view with:
name and enabled toggle
the source and its form: document autocomplete for comment triggers, account and site autocomplete for mention and site-update triggers, and interval, weekly, and one-time schedule forms
the prompt in the block editor
a When it fires selector for the continuation, with a tool picker fed by the agent's tools, a JSON input template, a script editor, a signal field for wake, and the on-failure toggle
operational metadata: created, updated, last checked, last fired, last error
Recent firings with status, event summary, error, a link to the thread, and an inline run card for headless runs
Every edit autosaves. The list refreshes live from trigger-updated account events, so triggers an agent creates for itself appear at once. The desktop and web UI page covers the other screens.
The agent itself
An agent manages its own triggers with the read and write verbs:
read ~/triggers/ lists them, with the write contract inline.
read ~/triggers/<name> returns one trigger with its recent firings.
write ~/triggers/<name> creates, edits, enables, disables, or deletes by name (or id), and honours enabled as written.
The Space index always advertises this, so an agent can complete "do this every morning" in one turn. There is no consent step. The owner decided on 2026-08-19 that agents manage their own triggers directly. The security page records what that means for the threat model.
Signed API
There are five actions, all scoped to the account through the owning agent:
ListAgentTriggers {agentId}
GetAgentTrigger {triggerId}: returns the trigger, its sessions, up to 25 most recent firings, and webhookSecret for editors.
CreateAgentTrigger {agentId, trigger, clientRequestId?}
UpdateAgentTrigger {triggerId, patch}
DeleteAgentTrigger {triggerId}
Deleting a trigger detaches its runs and removes its firings. Sessions it created are kept. Details are in the signed API.
Tests and tooling
bun run test:trigger in agents/ boots the real daemon against a local stand-in for the activity feed. It creates an agent and a mention trigger over the signed API and asserts that a comment mention fires exactly one session. The unit suites are activity-triggers.test.ts, trigger-events.test.ts, activity-trigger-race.test.ts, and schedule-triggers.test.ts.
Where this is going
As of September 2026, triggers are SQLite rows edited through CRUD actions and the ~/triggers/ verb surface. The roadmap records the plan: trigger documents, content-addressed like ~/tools/ and versioned by CID, replacing the CRUD actions, with a document-change source and an appendTo continuation. None of that is built. The agent_triggers table also has a cooldown_ms column that nothing reads or writes. There is no cooldown feature.
See also
Trigger continuations, the dated record of when headless continuations shipped.
park and wait and wake source, for the runs a wake continuation reaches.
Persistence for the agent_triggers, trigger_firings, and activity_watermarks tables.
Operations for the monitor configuration and the run queue.
Agents glossary for every agent term.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime