Scheduling
Cron
A cron trigger runs a program on a repeating schedule. The spec is either a cron expression or a fixed interval; the scheduler arms the next occurrence forward from now on adoption and after every fire.
Spec
| Spec | Meaning |
|---|---|
{"cron": "0 8 * * *"} |
five-field expression (minute hour day month weekday), UTC; a leading seconds field is added for you. Six- or seven-field expressions are accepted as written |
{"every_s": 900} |
fire every n seconds, measured from the previous fire |
A cron expression that yields no next occurrence is an inert definition: the health pass stamps lastStatus: "invalid_spec" on the record until the spec is fixed.
Create one
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\": \"inbox-watch\", \"fields\": {
\"name\": \"inbox watch\", \"kind\": \"cron\", \"spec\": {\"every_s\": 900},
\"program\": \"mailWatch@v1\",
\"args\": {\"space\": \"$SPACE\", \"chatId\": \"$CHAT\"},
\"enabled\": true, \"maxConsecutiveFailures\": 3}}]}"
$ANCHOR is the trigger anchor from Scheduling. From a program the same write is one call on the any client:
c = use("any@v1")
anchor = c.bundle_child(space, "bao/v1", "bao/triggers/v1")["objectId"]
c.upsert_record(space, anchor, "agent_triggers", "inbox-watch", {
"name": "inbox watch", "kind": "cron", "spec": {"every_s": 900},
"program": "mailWatch@v1",
"args": {"space": space, "chatId": chat_id},
"enabled": True,
})
program can be anything resolvable from the agent space: a shipped program under its alias (agent:rollup@v1), a connector, or a program the agent authored into the working space (unqualified mailWatch@v1). A 40-line program on a cron beats scheduling a full reasoning turn every five minutes.
The program side
The program's main(args) receives the record's args verbatim. A cron job is a normal run: full trace, fuel budget, wall deadline. Long jobs checkpoint their own cursor in the space and exit before the fuel cliff — the pattern is on Limits — so a periodic trigger drains a backlog one budgeted slice at a time.
"""Summarize new mail every quarter hour; posts only when something matters."""
__any_tool__ = False
def main(args):
c = use("any@v1")
gmail = use("connectors:gmail@v1") # a connector from the connectors overlay
new = fetch_new(gmail) # your own helper: pages the connector,
# keeps a cursor in the space
for m in new:
if looks_important(m):
c.chat_send(args["space"], args["chatId"],
{"text": f"📬 {m['snippet']}", "agent": {"name": "bao", "done": True}})
return {"ok": True, "checked": len(new)}
Missed occurrences do not exist
The scheduler computes the next due time from now — when the owning device adopts the record, after each fire, whenever the definition is edited, and on an election takeover. A device that was off for a night fires the 8:00 digest tomorrow, not eight times on wake. This is a deliberate guard against the wake-and-replay burst, and it is the opposite of the once rule.
Consequences:
- Editing
spec,program,args,name,limitsormaxConsecutiveFailuresrebuilds the entry and re-arms it forward. - Flipping
enabledfalse → true resets the circuit breaker and re-arms forward; otherenablededits are honored in place. every_scounts from the previous fire, so a job that takes a while drifts; use acronexpression when the wall-clock time matters.
Note. During the ≤ one-poll window of an election handover, two devices can both believe they are active and double-fire a floating cron. Pin a job to one device (see Device pins) if a duplicate run would be harmful, or make the program idempotent — most jobs that write through
upsertalready are.
Pause, edit, delete
Pausing is enabled: false on the record; deleting the record evicts it from the registry within a tick. A deleted id stays tombstoned server-side, so recreating a trigger means a new id. The running agent's control API offers the same operations against the live registry and writes them through to the record — see Runs and monitoring.
The standing jobs
The shipped agent runs a small set of built-in crons — history rollup hourly, memory extraction every 15 minutes, and similar maintenance — that are code-owned: they follow the active-instance election rather than a record pin and are never evicted by the reconcile. They appear in the trigger list like any other row.