Each Room runs a reactive scheduler that tracks the daemon clock and fires the
room's due schedules on every minute boundary. A schedule is stored in the
room's own datahike store (:schedule/*), so it forks and merges with the room
and is dropped when the room is deleted.
Two kinds of scheduled task:
:code schedule — evals a Clojure form in the agent's sandbox on each
fire (deterministic, no LLM turn) and posts the result as a room activity
message. The durable-pipeline primitive: put functions in a namespace in
your workspace repo, then schedule a call to them. The eval runs in a
future, off the engine's drain thread, with a five-minute timeout, so a slow
task does not hold up the scheduler or the engine while it runs.What does run on the drain is the tick itself. The room's scheduler spin is
spawned on the root execution context and, each minute, queries the room's store
for due schedules, posts each prompt task (disc/post!, which appends it durably)
and transacts each schedule's new :schedule/next-fire inside its spin body
(dvergr.rooms.scheduler/fire-due!). Those store reads and writes are synchronous
there, so a slow store delays other work on the root context's drain for as long
as they take.
(require '[dvergr.scheduler :as scheduler])
;; prompt schedules
(scheduler/every :day "09:00" :huginn "Run the morning intake sweep")
(scheduler/every :week :monday "14:00" :analyst "Weekly market review")
(scheduler/at "2026-08-01T09:00" :var "One-shot reminder")
(scheduler/interval 900000 :ops "Check the queue") ; every 15 minutes (ms)
;; code schedule — deterministic, no LLM
(scheduler/create
{:agent-id :me
:schedule {:every :day :at "07:00"}
:code "(require 'intake.news)(intake.news/scan!)"
:description "morning news scan"})
(scheduler/list) ; the room's schedules
(scheduler/cancel schedule-id)
| Form | Meaning |
|---|---|
{:every :day :at "07:00"} | daily at a wall-clock time |
{:every :week :on :monday} | calendar cadence (day-of-week) |
{:every :hour :at "00:15"} | every hour at a minute past it (the hour of :at is ignored); a second, "00:15:30", is kept but fires at the next minute's tick |
{:every :hour :n 4} | every 4 hours — :n multiplies a fixed unit |
{:interval-ms N} / {:every-ms N} | raw fixed interval |
{:at "2026-08-01T09:00" :once true} | one-shot at an instant (a local datetime is read in :tz, default the system zone; "…Z" or "…+02:00" keeps its own) |
:n is a positive integer with :every :minute\|:hour\|:day\|:week and
means "every N of that unit". It's for fixed intervals, so it can't combine with
a wall-clock :at or a calendar :on/:on-day.
Validation is strict — an unknown spec key, a non-positive :n or
:interval-ms, or an incompatible combination is rejected loudly rather
than silently producing the wrong cadence (a silent drop is how {:every :hour :n 4} could once have become an unintended hourly schedule).
The daemon clock (dvergr.runtime.clock) is one signal holding the current minute,
truncated to :00 seconds, and every room's scheduler re-runs when it changes. The
heartbeat sleeps 60 s between updates, so a tick lands up to a minute after the
boundary it names, not on it. A tick fires each schedule whose
:schedule/next-fire is at or before that minute. So a schedule fires on the first
tick whose minute is at or after its fire time: an :at with seconds (stored as
:schedule/second, so the computed fire times keep it) fires at the next
minute's tick, and an interval shorter than a minute fires at most once a
minute.
schedule_create (agent_id, task, interval_minutes, at
least 1; an optional description), schedule_list and schedule_cancel act on
the current room's schedules (dvergr.scheduler.tools, required at daemon start).
They make prompt schedules at a fixed interval only. An agent has them when its
:tools lists them; over MCP they are in the code toolset.catalog_deploy (MCP bench toolset). Deploys a room's workflow bundle: it
creates a prompt schedule in that room (every hour, day or week, with an
optional at HH:MM and on weekday) whose task is the bundle's task with
params filled in, given to agent, a participant of the room. The room's REPL
lists and cancels it like any other (dvergr.scheduler/list, cancel).Outside the sandbox, dvergr.scheduler.core exposes create-schedule!,
cancel-schedule!, and list-schedules on a Room value — the same functions
the sandbox namespace wraps.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |