Scheduled prompts

A scheduled prompt hands one or more agents the same text at set times: once, or every interval from a start time. Each prompt has a name, its title. The swarm keeps every prompt, and each agent it names fires it in its own container, so a fire needs neither the hive nor the swarm controller to be up at that moment, only the agent.

Operators manage them with swarmctl schedule on the swarm controller's host. Agents can't create, change or remove one. For a one-off wake an agent sets itself, it uses remind, which stays inside its container.

Managing them

# swarmctl schedule add --name standup --agent scribe --agent iris \
    --body "post the standup summary" --at 2026-10-05T09:00:00+02:00 --every 1d
added schedule kqzpmwab
kqzpmwab  "standup"  every 1d  from 2026-10-05T07:00:00Z  next 2026-10-05T07:00:00Z  agents: iris, scribe  | post the standup summary

# swarmctl schedule list
# swarmctl schedule list --agent scribe
# swarmctl schedule edit kqzpmwab --body "post the standup summary to the team room"
# swarmctl schedule edit kqzpmwab --add-agent atlas --remove-agent iris
# swarmctl schedule pause kqzpmwab
# swarmctl schedule resume kqzpmwab
# swarmctl schedule rm kqzpmwab

The full flag reference is swarmctl-cli.md.

When a prompt fires

A prompt's fire times are its start time plus whole intervals. Each agent works them out from the prompt itself, so:

A fire reaches the agent as a schedule loose end with the prompt's text. The agent's turn loop wakes for it like any other loose end. If the agent hasn't dealt with the previous fire yet, the new fire replaces it rather than adding a second one; once the agent dismissed the previous one, the next fire opens a new one.

Each agent reads its prompts every 10 seconds, so a fire can land up to 10 seconds after its time. A change reaches each agent within the same window, or within a minute if the controller's first attempt to pass it on failed.

When a prompt doesn't fire

Implementation

swarm-controller is the only client that writes either of the two KV buckets involved.

schedule-definitions holds every prompt as one JSON document under the key all. Agents hold no grant on this bucket. Each change re-reads the key and writes against its revision, so a prompt exists for every one of its agents or for none of them, and two changes at once don't lose one. The controller leaves a prompt it can't decode, such as one a newer build wrote, out of the list, with a warning in its log. An agent's key that already held the prompt keeps it as it was, so it goes on firing, and no other key gets it. The controller keeps the prompt in the document as it was, after the others, and won't reuse its id. A document that isn't an object with a list of schedules is an error.

agent-schedules holds one key per agent: the prompts naming that agent, without their names or other agents. A key the controller can't decode counts as no prompts, with a warning in its log, and the next rewrite replaces it.

{
  "schedules": [
    {
      "id": "kqzpmwab",
      "body": "post the standup summary",
      "anchor_unix": 1791183600,
      "interval_seconds": 86400,
      "paused": false
    }
  ]
}

interval_seconds is absent for a one-shot. After each change the controller rewrites the key of every agent the change named before or after, and once a minute it rewrites every agent's key, so a rewrite that failed converges. Ids are 8 lowercase letters, the same in every agent's key, never reused, and so never equal to a hive schedule's numeric id.

swarmctl schedule calls these routes over the controller's socket:

method path body
GET /api/schedules —; answers every prompt, sorted by name then id
POST /api/schedules name, agents, body, and optional anchor_unix, interval_seconds, paused; answers 201 with the prompt
PATCH /api/schedules/<id> any of name, body, anchor_unix, interval_seconds (null for a one-shot), paused, add_agents, remove_agents
DELETE /api/schedules/<id> —; answers 204

Each prompt they answer with has id, name, agents (sorted), body, anchor_unix, interval_seconds (null for a one-shot), paused, and next_fire_unix: the next time on the prompt's grid, as of the moment the controller answers, null when paused or a one-shot whose time has passed.

An agent's own queue credential grants exactly one subject in this bucket: $JS.API.DIRECT.GET.KV_agent-schedules.$KV.agent-schedules.<agent>, the direct read of its own key. It can't read another agent's key, open or watch the bucket, or write to it. A KV watch would need a consumer, and a consumer create subject doesn't name a key, so granting one would let the agent read every key; the agent reads its key every 10 seconds instead.

The harness (hive-agent's schedule_timer) keeps the last fired time per prompt id in the schedule_fired_slots table of its state database, and pushes each fire as the todo schedule/schedule:<id> with source scheduled, reopening an acknowledged one.