Cron Schedule Kinds — Four Ways a Clock Can Wake an Agent
An interactive explainer for rk cron: the three schedule kinds (every, cron, backoff) and the wake_on edge trigger, each as an animated timeline you can play, restart, and change the rules of — plus the deliver policy that decides what happens when the agent is busy at fire time. Companion to the cron spec.
An rk cron entry is a small intent file: what text to deliver, which pane to deliver it to, and when. The “when” comes in three schedule kinds plus one optional edge trigger, and a deliver policy on the entry says what to do if the agent is busy at that moment — send anyway, hold until idle, or skip the fire. A ticker polls every 30 seconds and asks a pure function, “given the entries, the delivery log and the panes’ agent states on disk right now, what is due?” Nothing is remembered in memory, so a restart never loses the clock.
Every panel loads finished. Press Play to watch it happen; toggles change the rules and replay.
every fixed interval
A metronome. It fires whenever now − last delivery ≥ interval. The anchor is the newest line for this entry in the delivery log, or the moment the entry was created before its first fire.
What to notice
- Nothing is “scheduled ahead”. Each poll just re-checks the gap since the last delivery.
- If the daemon was down, the first poll after restart sees a gap larger than the interval and fires immediately, then the rhythm continues from there. Nothing piles up.
cron wall clock
The classic five-field expression, in the daemon’s local time. The evaluator finds the latest occurrence since the last delivery and fires if it is still inside a 2-minute grace window (covering the 30 s poll and short restarts).
What to notice
- An occurrence that fell while the daemon was down is skipped by default and logged as
missed. Firing a 09:00 digest at 14:00 is usually wrong. catch_up: onceopts in to exactly one late fire per gap, the moment the daemon is back.
backoff doubling ladder from the last real activity
Gaps of 1, 2, 4, 8, 16 min, then 30 min forever, counted from an anchor. The anchor is not the last fire. It is the last time the target pane went idle for a reason other than the clock, read from its @rk_pane_agent_state option.
The signal
- Every delivery makes the agent busy for a few seconds, then idle again. That flip lands within 120 s of the entry’s own delivery, so the clock says “I caused that” and the ladder keeps climbing.
- An idle flip not explained by a recent delivery is genuine activity: someone typed, or the agent did real work. The ladder resets to a 1-minute gap from that moment.
- So: quiet agent, ticks thin out. Busy agent, ticks stay frequent. The agent never has to tell the clock anything.
- Set
min = maxand the ladder is flat — “ping every 3 min of quiet”. Same anchor rules: the clock’s own pings don’t restart the count, real activity does.rk cron add "wake up" --idle-every 3mwrites exactly this entry — no new kind, no schema field.
wake_on edge trigger, OR’d onto any schedule
Not a schedule at all. Each poll fingerprints the state of every other pane on the server and fires when the fingerprint changed by an actionable transition.
Three rules keep it from feeding on itself
- Actionable = a pane became waiting (a question), became idle (finished), or disappeared. Becoming active is ignored: an agent starting work needs no attention.
- Self-exclusion: the target’s own pane is left out of the fingerprint. A delivery makes the target busy, and that must never count as the next edge.
- Debounce: an edge within 60 s of the entry’s last delivery is held, not dropped, and fires on a later poll. A burst coalesces into one delivery.
deliver what happens when the agent is busy at fire time
The schedule decides when a fire comes due; deliver decides what happens if the target agent is busy at that moment. One every 5m entry below, three policies, one busy stretch (the green block on the state strip).
Hold, drop, or neither
- immediate ignores the agent state: every boundary fires, busy or not — the payload lands in a working pane.
- when-idle holds a busy-pane fire and delivers the moment the agent goes idle; the rhythm re-anchors on that delivery. A hold is bounded: 2 hours past the due time it expires with a logged
held-expiredrather than landing hours late. - skip-if-busy drops a busy-pane fire: the skip is logged (
skipped-busy), so the next attempt is one full interval later, not the next 30 s poll — and the grid itself never moves.
Put together: the operator’s clock
The operator tick seeded on every tmux server — rk defines the clock; the operator consumer (fab) plants and tunes the entry, and rk operator only launches the operator the entry respawns — is one entry using two of these mechanisms at once. wake_on is the reactive channel: an agent asks a question, the operator is pinged within a poll. backoff is the fallback poll: it thins out to every 24 minutes when nothing is happening and snaps back to 3 minutes the moment someone touches the operator.
Either channel’s fire is a “delivery” of the same entry, appended to a per-server log as {ts, entry, target, reason, outcome}. That log, plus the panes’ state options, is the entire memory of the clock. The only way to silence an entry is to tell it: rk cron mute <id> --for 30m.
id: uqdy
name: operator tick
schedule: { kind: backoff, min: 3m, max: 24m }
wake_on: { event: agent-state-change, scope: server, debounce: 1m }
target: { kind: role, role: operator }
payload: operator tick
deliver: skip-if-busy
if_absent: respawn # dead operator? relaunch `rk operator`, then deliver
pinned: true
| Kind | Fires when… | Good for |
|---|---|---|
| every | the gap since the last delivery reaches the interval | plain heartbeats, polling a queue |
| cron | a wall-clock occurrence arrives (missed ones skipped unless catch_up: once) | “09:00 every day”, weekly reports |
| backoff | the doubling ladder from the last genuine idle moment comes due | attention that should fade while quiet |
| backoff, min = max ( --idle-every) | X after the last genuine idle moment, then every X while it stays quiet | reminders that wait for quiet |
| wake_on | another pane finished, asked something, or vanished | reacting within seconds instead of a poll |
The flat ladder’s fixed-grid cousin is --every X --deliver skip-if-busy: it fires only at grid points where the agent happens to be idle, skipping busy boundaries instead of waiting out a fresh X of quiet — the deliver panel above shows the difference.
All four are evaluated by the same 30-second poll, so any fire lands up to 30 s after it came due. Every payload must tolerate being delivered twice; a restart may re-fire one due tick.