Cron
Table of Contents
- Introduction
- Install
- Types
- HTTP API
- Management UI
- Body Templates
- Timer Lifecycle
- Rate Limiting
- Examples
Introduction
%cron is a Gall agent that fires pokes on a repeating timer. It exposes an HTTP API and a management UI at /apps/cron for creating, reading, updating, and deleting scheduled poke timers. All requests require urbauth cookie authentication.
Each timer stores a target poke specification (ship, agent, mark, body) along with a repeat interval in seconds. When the timer fires, %cron sends the poke to the target and reschedules itself. Failed pokes are logged but do not stop the timer.
Since timers are stored in agent state, these will survive restarts (whereas threads run with %spider do not).
Source files
| File | Description |
|---|---|
/sur/cron |
Type definitions |
/app/cron |
Agent implementation |
Install
Via ames/docket:
|install ~maslev %cron
Locally:
|new-desk %cron
|mount %cron
(copy in everything from desk to $PIER/cron)
|commit %cron
|install our %cron
Types
Defined in /sur/cron:
$poke-spec
Target for a scheduled poke.
| Face | Type | Description |
|---|---|---|
ship |
@p |
Target ship |
agent |
term |
Target agent |
mark |
term |
Poke mark |
body |
@t |
Poke body (cord), supports template substitution |
$timer
A scheduled poke on a repeating interval.
| Face | Type | Description |
|---|---|---|
id |
@ud |
Auto-incrementing identifier |
poke |
poke-spec |
Target poke specification |
period |
@ud |
Repeat interval in seconds |
active |
? |
Whether the timer is active |
created |
time |
Creation timestamp |
version |
@ud |
Monotonically increasing counter, bumped on each fire or update |
$timers
(map @ud timer) -- map of timers by sequential id.
HTTP API
All endpoints are served under /apps/cron via eyre. All requests require urbauth cookie authentication (403 if missing).
JSON API
Request and response bodies are JSON.
List timers
GET /apps/cron/api
Returns 200 with a JSON array of all timers.
Response:
[
{
"id": 0,
"poke": { "ship": "~bus", "agent": "hood", "mark": "helm-hi", "body": "" },
"period": 10,
"active": true,
"created": "~2026.3.10..22.42.26..8361",
"version": 5
}
]Get timer
GET /apps/cron/api/:id
Returns 200 with a single timer object, 400 if the id is invalid, or 404 if not found.
Create timer
POST /apps/cron/new
Request body:
{
"poke": {
"ship": "~bus",
"agent": "hood",
"mark": "helm-hi",
"body": ""
},
"period": 10
}| Field | Type | Required | Description |
|---|---|---|---|
poke.ship |
string | yes | Target ship (e.g. "~bus") |
poke.agent |
string | yes | Target agent name (e.g. "hood") |
poke.mark |
string | yes | Poke mark (e.g. "helm-hi") |
poke.body |
string | no | Poke body, defaults to "" |
period |
number | yes | Repeat interval in seconds |
Returns 201 with the created timer object. The timer starts immediately -- the first poke fires after period seconds.
Update timer
PUT /apps/cron/api/:id
All fields are optional. Omitted fields keep their current values. The poke object supports partial updates -- you can change just poke.body without resending ship/agent/mark.
Request body:
{
"poke": { "body": "{now}" },
"period": 5,
"active": false
}| Field | Type | Description |
|---|---|---|
poke.ship |
string | New target ship |
poke.agent |
string | New target agent |
poke.mark |
string | New poke mark |
poke.body |
string | New poke body |
period |
number | New repeat interval in seconds |
active |
boolean | Enable or disable the timer |
Returns 200 with the updated timer. If the timer is active after the update, it is rescheduled with the new period. Setting active to false effectively pauses the timer -- stale behn wakes are ignored via version checking.
Delete timer
DELETE /apps/cron/api/:id
Returns 204 with no body on success, 400 if the id is invalid, or 404 if not found. Any pending behn wake for this timer is silently ignored when it fires (the timer no longer exists in state).
Form API
Used by the management UI. All form routes accept application/x-www-form-urlencoded bodies and redirect back to /apps/cron on success (POST-Redirect-GET).
| Route | Action |
|---|---|
POST /apps/cron/new |
Create timer |
POST /apps/cron/:id |
Update timer |
POST /apps/cron/:id/pause |
Toggle pause |
POST /apps/cron/:id/delete |
Delete timer |
Error responses
All errors return a JSON object:
{ "error": "description of the problem" }| Status | Meaning |
|---|---|
400 |
Bad request (invalid id, missing fields, malformed JSON) |
403 |
Forbidden (missing urbauth cookie) |
404 |
Timer not found |
405 |
Method not allowed |
Management UI
Browse to /apps/cron to access the HTML management interface. The UI is server-rendered with no external dependencies (no JavaScript, no external CSS). Features:
- Table of all timers with editable fields (ship, agent, mark, body, period)
- Save button per row to update a timer
- Pause/Resume button per row to toggle active state
- Delete button per row
- Add row at top to create new timers
- All actions use standard HTML form submissions
Body Templates
The body field supports template substitution at fire time. Currently one token is supported:
| Token | Replacement |
|---|---|
{now} |
Current @da timestamp (e.g. ~2026.3.10..23.02.24..xxxx) |
Only the first occurrence of {now} is replaced. The substitution happens when the poke fires, not when the timer is created.
Timer Lifecycle
- Create:
POST /apps/cron/newstores the timer and sets a behn wake fornow + periodseconds. - Fire: When behn wakes the agent, it checks the timer is still active and the version matches (to ignore stale wakes from before an update or delete). If valid, it sends the poke to
[ship agent]with the specified mark and body, then bumps the version and schedules the next wake. - Ack handling: Poke acks arrive on the
/fire/:idwire. Successful acks are silently ignored. Nacks are logged viaslogwith"cron: poke nacked". - Update:
PUT /apps/cron/api/:idbumps the version (invalidating any pending wake) and, if active, schedules a new wake with the updated period. - Delete:
DELETE /apps/cron/api/:idremoves the timer from state. The next behn wake finds no timer and does nothing.
Rate Limiting
To prevent abuse, remote pokes (target ship != our ship) require a minimum period of 300 seconds (5 minutes). Local pokes have no minimum.
Examples
|hi our ship every 10 seconds
curl -sb ~/cookie -X POST http://localhost/apps/cron/new \
-H 'Content-Type: application/json' \
-d '{"poke":{"ship":"~zod","agent":"hood","mark":"helm-hi","body":""},"period":10}'|hi our ship with current time every 3 seconds
curl -sb ~/cookie -X POST http://localhost/apps/cron/new \
-H 'Content-Type: application/json' \
-d '{"poke":{"ship":"~zod","agent":"hood","mark":"helm-hi","body":"{now}"},"period":3}'Pause a timer
curl -sb ~/cookie -X PUT http://localhost/apps/cron/api/0 \
-H 'Content-Type: application/json' \
-d '{"active":false}'Resume a timer with a new interval
curl -sb ~/cookie -X PUT http://localhost/apps/cron/api/0 \
-H 'Content-Type: application/json' \
-d '{"active":true,"period":60}'Delete a timer
curl -sb ~/cookie -X DELETE http://localhost/apps/cron/api/0