Triggers & scheduling

Let an org react to the clock and the outside world — schedules, scheduled scripts, inbound webhooks, process watchers, and polled inboxes that seed goals.

Last updated

Without triggers an org is sealed: it advances its mission on a fixed tick and on directives you type into the dashboard. A ## Triggers section in your QUORUM.md opens it up — so it reacts to the clock and to inbound events, in service of that same mission. A fired trigger seeds a goal, exactly like an operator message, and flows through the normal cycle.

One ### heading per trigger (the heading is its name); the prose is the directive it seeds when it fires. The kind is inferred from the fields you give it. A section missing either — no heading, or no prose and no directive: key — is skipped quietly, so if a trigger never appears on the dashboard’s Triggers panel, check it has both.

## Triggers

### Weekly review

- every: 7d

Review progress against the mission and decide the single most important thing
to ship next week.

The four kinds

Schedule — react to the clock

A schedule trigger declares exactly one of:

field fires
every on a fixed recurrence since it last fired — 45s, 30m, 6h, 7d, 2w. Never fired yet? It starts one interval after it was created — a roadmap-review shouldn’t run against an org that has done nothing yet. Polls are the exception and start right away: a run: job and an inbox both cost nothing when there’s nothing there
cron when local wall-clock time matches a standard 5-field expression min hour dom mon dow — supports *, a number, ranges a-b, lists a,b, and /n steps
at once, at or after a single ISO datetime — a one-shot future commitment

Downtime is handled per kind: an at moment the org slept through still fires on the next boot (“at or after”), and an overdue every fires as soon as the org is back. cron matches only the live wall-clock minute — minutes the org slept through are skipped, never replayed.

### Monday planning

- cron: 0 9 * * 1

Kick off the week: re-plan the top goal and rebalance work across the team.

### Attend the 3pm call

- at: 2026-07-06T15:00:00
- owner: Account Lead

Join the scheduled customer call and capture the decisions and follow-ups.

The one-shot at kind is an appointment — a time-bound commitment the org keeps punctually. That’s any one-shot work, not just meetings: join the 3pm call, verify the release at 10pm, act on a deadline.

  • The owner attends. When the moment arrives and owner names an employed agent (by role or id), the work lands on the board directly assigned to them and runs immediately — no planning ceremony, no queue. Task review and budgets apply as for any task, and work that turns out bigger than one agent delegates or escalates like any other. (For a mechanical check that needs no judgment, a RUN[at …] job is cheaper — no agent at all unless it prints output.)
  • No owner? It still jumps the queue. The fired goal takes priority over the normal goal rotation and is planned the cycle it fires; an owner: naming a role the org lacks auto-hires it through the plan.
  • On time, not tick-aligned. Every future appointment arms a timer that wakes the org at the exact moment: an idle org attends to the second, no matter the tick interval; a busy org attends as soon as in-flight work settles (running sessions are never preempted).

Webhook — react to an inbound event

A trigger with no schedule or command (or an explicit kind: webhook) is fired by an external request to POST /api/trigger/<name>:

### Inbound signup

- kind: webhook

A new signup arrived. Qualify the lead, add it to the CRM, and line up a
conversation to validate pricing.
curl -X POST http://your-host:3000/api/trigger/inbound-signup \
  -H "X-Quorum-Secret: $QUORUM_TRIGGER_SECRET" \
  -H "content-type: application/json" \
  -d '{"text": "acme.co — 40-seat team, from the pricing page"}'

The endpoint is fail-closed. It does nothing unless you set a shared secret in the QUORUM_TRIGGER_SECRET environment variable, and every call must present it — as an X-Quorum-Secret header or Authorization: Bearer <secret> (compared in constant time). It’s also only reachable when you bind the dashboard beyond loopback (--host 0.0.0.0); by default it listens on 127.0.0.1 only.

Crucially, the caller can’t inject instructions. Only the trigger’s own configured directive is seeded; an optional {"text": "..."} body is attached as context. So even an exposed endpoint can’t steer the org off its mission.

Process watcher — react to a job finishing

A trigger with a command runs that shell command and watches it. When the process exits, the trigger fires — carrying the exit code and a tail of its output as context, so the org can act on the result.

### Deploy finished

- command: ./scripts/wait-for-deploy.sh

Review the deployment that just completed and log any regressions found.

Watches are re-established on every start, so a restart re-arms them.

Inbox — hold a conversation with the outside world

A trigger with a url (or an explicit kind: inbox) is conversational inbound: the runtime polls the endpoint on the trigger’s cadence (every, default 5m) and seeds genuinely new mail as a goal, so the reply obligation flows through the normal cycle — the org hears back from the people it emails.

### Support inbox

- url: https://mail.example.dev/messages
- secret: mail-inbox-token
- allow: ["*@example.dev", "vip@partner.io"]

Read each new message and reply on the "email" channel; file real prospects
with CONTACT.

Your machine is never exposed: mail lands somewhere the org can poll — typically a Cloudflare Email Worker storing messages at the edge, which the org’s own build agent can deploy — and the runtime performs each poll, presenting the secret as a bearer token that no agent ever holds. The endpoint returns a JSON array of messages (or {"messages": [...]}), each with from, text, and optionally id, subject, and at.

Because inbound mail is a conversation, it’s fenced fail-closed on both axes:

  • Senders. Only mail matching the allow list — exact emails, *@domain wildcards, or an explicit * — is ingested; everything else is dropped and audited. An inbox with no allowlist ingests nothing.
  • Content. The message text is attached to the goal as quoted data for the trigger’s directive. It can inform the work; it is never treated as instructions or as operator input — the same discipline as a webhook payload.

Messages are deduped by id across restarts, a burst is capped per firing (the rest ride the next poll), and when a sender matches a contact in the CRM, the goal context says so — the conversation threads to the customer record.

Run one inbox per function. support@ and sales@ are just two trigger sections — each with its own endpoint (or per-box path on one Email Worker), allowlist, cadence, and directive — and owner: pins where each one’s work lands:

### Support inbox

- url: https://mail.example.dev/messages/support
- secret: mail-inbox-token
- allow: ["*"]
- owner: Support Lead

Read each message, fix or answer the problem, and reply on the "email" channel.

### Sales inbox

- url: https://mail.example.dev/messages/sales
- every: 15m
- secret: mail-inbox-token
- allow: ["*"]
- owner: Head of Sales

Qualify each sender, CONTACT them into the CRM, and draft a tailored reply.

A message’s to address (when the endpoint supplies one) rides into the goal context, so even a single shared endpoint can say which address the mail arrived at.

Scheduled job — poll the outside world cheaply

A schedule trigger with a run script runs that script headlessly on its schedule — no agent, no token cost. A silent run (clean exit, no output) just advances the schedule; the org only wakes, seeding the directive as a goal with the script’s output as context, when the script prints something or fails.

### Poll the inbox

- every: 5m
- run: ./drain-inbox.sh

New replies arrived — read each and continue the conversation.

It’s how an org polls cheaply — drain an inbox, poll a queue, ping a health check — and pays for an agent turn only when there’s genuinely work to do. A job is killed if it runs past two minutes, so a hung poll can’t wedge a cycle. Jobs run from the jobs/ folder (below), so run: ./drain.sh finds the script next to its manifest.

Agents schedule themselves

Triggers aren’t only author-declared. While working a task, an agent can book a future goal itself by adding a line to its output:

SCHEDULE[2h]: Attend the customer call and capture the decisions

<when> may be a delay (2h, 30m, 3d), an absolute time (at 2026-07-06T15:00), or a recurrence (every 7d). Bookings are bounded — a few per task — and each is written as a persisted schedule trigger, so it fires at its moment even across restarts — and it’s owned by the booking agent: the rep that arranges a call is the one activated to attend it when the time comes. This is how the org commits to real, time-bound obligations on its own, rather than assuming the same process is still running. It’s the same output-marker pattern as HIRE[...] and DELEGATE[...], and it’s gated by the schedule capability (deny it to lock self-scheduling down).

An agent can likewise schedule a headless job with RUN[<when>]: <shell command> — the same script-on-a-schedule that only wakes an agent when it prints output (e.g. RUN[every 5m]: ./drain-inbox.sh to poll for replies without spending a turn each time). Because it defers shell execution, it’s gated by the code capability, not schedule.

The jobs/ folder

Jobs live in a jobs/ folder at the org root — the agent-owned counterpart to QUORUM.md. Each job is a manifest jobs/<name>.json beside its script:

{ "every": "5m", "run": "./drain-inbox.sh", "directive": "New replies arrived — read each and reply." }

RUN[...] writes one of these for you, but the point is that the org manages its own cron: an agent with code can create, edit, disable ("enabled": false), or delete jobs by writing and removing files in this folder — it’s the one shared directory in an agent’s sandbox — and the runtime picks the changes up live, no restart. Jobs run with jobs/ as their working directory. Operator jobs can still be declared in ## Triggers with a run: key; both sources run side by side.

Keys

key meaning
every / cron / at schedule (pick one — see above)
command process: the shell command to run and watch
run schedule: a script run headlessly on the schedule (no agent, no cost) from the jobs/ folder; wakes an agent only when it prints output
url inbox: the HTTPS endpoint the runtime polls (GET, JSON) for new messages
secret inbox: name of the org secret presented as a bearer token on each poll — read at poll time, never held by an agent
allow inbox: sender allowlist — exact emails, *@domain, or *. Required: no list, no ingest
owner who the fired work lands with. On a one-shot at, the attendee — the task is assigned to them directly and runs immediately. On other kinds, the plan assigns the work there (a missing role auto-hires)
kind force schedule, webhook, process, or inbox (usually inferred)
enabled false keeps a trigger defined but dormant (default true)
directive the seeded directive, as a key instead of the prose body
name the trigger’s name (default: the heading); a webhook fires at POST /api/trigger/<name>

Lifecycle

Triggers are persisted with their firing history, so editing ## Triggers and restarting takes effect immediately without re-firing what already ran. Deleting a section removes its trigger; triggers agents booked at runtime with SCHEDULE[...] — and the jobs/ folder — are managed separately, so an edit to ## Triggers never disturbs them. Each firing is recorded in the audit log and shown on the dashboard’s Triggers panel — name, kind, schedule, and how many times it has fired.