docs

Scheduling

Scheduling

A trigger is a record in the agent_triggers dataset of your agent space: a program spec, a schedule, arguments, and an owner naming the device that runs it. The running agent converges its scheduler on those records every tick, so creating, editing, pausing and moving a job is just writing a record — from the UI, from a program, or from curl.

A trigger record

{
  "name": "daily digest",
  "kind": "cron",
  "spec": {"cron": "0 8 * * *"},
  "program": "agent:rollup@v1",
  "args": {"space": "bao", "chatId": "<chatId>"},
  "owner": "",
  "enabled": true,
  "maxConsecutiveFailures": 3
}
Field Meaning
kind cron, once or event
spec {"cron": "<expr>"} or {"every_s": n} · {"at": <epoch seconds>} · {"dataset", "objectId", "filter?"}
program a program spec — agent:remind@v1, a connector, or one you authored
args the JSON passed to main(args)
owner the device pin — a peer id, or "" for "any active device"
enabled pause without deleting
limits {fuelPerRun?, timeoutS?, maxCostPerRun?} — per-run resource bounds declared on the definition
maxConsecutiveFailures circuit-breaker threshold (default 3)

The runtime adds a rollup on the same record — lastRunAt, lastStatus, lastDurationMs, lastFuel, lastCostUsd, runCount, consecutiveFailures, lastRunRef — so "did it run?" is one read.

Where the records live

Triggers sit on the trigger anchor: the bao/triggers/v1 child of the agent space's bao/v1 bundle. Resolve it by seed, never by name:

ANCHOR=$(curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/bundles/bao%2Fv1/children \
  -H 'Content-Type: application/json' -d '{"seed": "bao/triggers/v1"}' | jq -r .objectId)

curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/upsert \
  -H 'Content-Type: application/json' \
  -d "{\"objectId\": \"$ANCHOR\", \"dataset\": \"agent_triggers\",
       \"records\": [{\"id\": \"daily-digest\", \"fields\": {
         \"name\": \"daily digest\", \"kind\": \"cron\", \"spec\": {\"cron\": \"0 8 * * *\"},
         \"program\": \"agent:rollup@v1\", \"args\": {\"space\": \"$SPACE\", \"chatId\": \"$CHAT\"},
         \"enabled\": true}}]}"

The record id is yours to choose (a slug). The owning device adopts the record within one 5-second tick.

How a tick works

every 5 s, on each running device:
   reconcile ── read agent_triggers ──▶ adopt / refresh / evict
   health    ── mark inert definitions (invalid_spec, unsupported_kind)
   fire      ── due entries → run program → write trigger_runs + rollup

The dataset is the source of truth. A registry-only edit would be reverted by the next reconcile, which is why the control-plane routes write through to the record.

Why it matters. There is no scheduler service. The schedule syncs with your data, encrypted, to every device you own; whichever device holds the pin fires it, and the run log lands in the same space. Take the laptop offline and its pinned jobs pause; repin them to a machine that is up and they move within a tick.

Semantics in one table

cron once event
fires when now ≥ next occurrence, armed strictly forward from adoption now ≥ at and never ran a matching change lands (proposed — see the page)
missed while owner down does not exist — no catch-up burst fires late on the next tick live-only (proposed)
after firing re-arms forward auto-disables, stays as its audit trail re-arms
failure counts toward the breaker consumes the shot — no retry counts toward the breaker