CovenDocs

Automations scheduling

How Coven turns a routine's schedule into due slots, including time zones, daylight saving, missed slots after downtime, overlap, leases, retries, quarantine, timeouts, and cancellation, with the conformance cases that pin each rule.

6 min read

This page covers how the daemon decides when a routine runs and what happens when time, downtime, or a failure gets in the way. For the routine fields and actions, see Coven automations; for the state names used below, see Automations lifecycle.

The tables are tested cases

Each table renders a byte-for-byte copy of an upstream conformance vector file at the reviewed commit. Upstream CI runs every one of these files through the daemon's own scheduling code with coven automations conformance evaluate, and its Rust tests require every case to pass. The conformance target refuses a vector file whose cases differ from its built-in scenarios, so these are the inputs CI executes. The target uses temporary stores, a pinned clock, and a stand-in runtime rather than a live daemon. Passing them is audit evidence, not certification; see Automations conformance.

Schedules

A routine's rrule uses a small subset of iCalendar recurrence rules:

  • FREQ is required and is DAILY or WEEKLY.
  • BYHOUR lists hours from 0 to 23. Each slot starts on the hour. Without BYHOUR, a routine runs at 9.
  • BYDAY lists weekdays as MO to SU or MON to SUN, and only with FREQ=WEEKLY. Without BYDAY, a weekly routine runs on Monday.
  • Keys and values are case-insensitive.

Nothing else is accepted: no other frequency, and no INTERVAL, COUNT, UNTIL, BYMINUTE, or other key. A repeated key, a repeated hour or weekday (including MO with MON), and an empty part or value are refused too. An unsupported rule fails validation rather than being approximated.

22 cases from rrule-vocabulary.vectors.json:

rrule-vocabulary conformance cases
CaseRule as writtenResult
daily-defaultsFREQ=DAILYaccepted as daily, hours 9
daily-hour-normalizationfreq=daily;byhour=17,9accepted as daily, hours 9, 17
weekly-defaultsFREQ=WEEKLYaccepted as weekly, hours 9, days MO
weekly-day-normalizationfreq=weekly;byday=wed,MO,fr;byhour=8accepted as weekly, hours 8, days FR, MO, WE
unsupported-frequencyFREQ=HOURLYrejected
unsupported-keyFREQ=DAILY;COUNT=3rejected
daily-bydayFREQ=DAILY;BYDAY=MOrejected
duplicate-frequencyFREQ=DAILY;FREQ=WEEKLYrejected
duplicate-byhourFREQ=DAILY;BYHOUR=9;BYHOUR=17rejected
duplicate-bydayFREQ=WEEKLY;BYDAY=MO;BYDAY=TUrejected
out-of-range-hourFREQ=DAILY;BYHOUR=24rejected
duplicate-hourFREQ=DAILY;BYHOUR=9,9rejected
duplicate-weekdayFREQ=WEEKLY;BYDAY=MO,MOrejected
duplicate-weekday-aliasFREQ=WEEKLY;BYDAY=MO,MONrejected
empty-hour-entryFREQ=DAILY;BYHOUR=9,,17rejected
empty-weekday-entryFREQ=WEEKLY;BYDAY=MO,,TUrejected
unknown-weekdayFREQ=WEEKLY;BYDAY=XXrejected
missing-frequencyBYHOUR=9rejected
malformed-partFREQ=DAILY;BYHOURrejected
trailing-separatorFREQ=DAILY;rejected
empty-segmentFREQ=DAILY;;BYHOUR=9rejected
empty-valueFREQ=rejected

Time zones and daylight saving

A routine's timezone is utc or an IANA time zone name such as America/New_York. Slots are wall-clock hours in that zone:

  • A wall-clock time that does not exist because clocks spring forward is skipped. That day has no run.
  • A wall-clock time that repeats because clocks fall back runs once, at the earlier instant.

A stored routine never keeps local. A request that sends local has it resolved to an exact zone before the routine is stored. On Unix, a TZ override must itself be utc or an IANA name; anything else fails the request rather than falling back to the host zone. Without TZ, the host's zone is used. The rich coven.automations.command.v1 envelope refuses local outright. A routine stored as local by an older daemon is resolved when the store opens, as a new revision recorded with migration local-timezone-normalization-v1.

11 cases from calendar-schedule-resolution.vectors.json:

calendar-schedule-resolution conformance cases
CaseRule and time zoneAfter (UTC)Next slot
daily-same-dayFREQ=DAILY;BYHOUR=9 in utc2026-08-28 08:002026-08-28 09:00 UTC
multiple-daily-hoursFREQ=DAILY;BYHOUR=9,17 in utc2026-08-28 12:002026-08-28 17:00 UTC
weekly-weekdayFREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=8 in utc2026-08-28 09:002026-08-31 08:00 UTC
iana-winter-offsetFREQ=DAILY;BYHOUR=9 in America/New_York2026-01-15 13:002026-01-15 14:00 UTC, 09:00 EST local
iana-summer-offsetFREQ=DAILY;BYHOUR=9 in America/New_York2026-07-15 12:002026-07-15 13:00 UTC, 09:00 EDT local
dst-spring-gapFREQ=DAILY;BYHOUR=2 in America/New_York2026-03-07 08:002026-03-09 06:00 UTC, 02:00 EDT local
dst-fall-foldFREQ=DAILY;BYHOUR=1 in America/New_York2026-11-01 04:002026-11-01 05:00 UTC, 01:00 EDT local
leap-day-boundaryFREQ=DAILY;BYHOUR=9 in utc2028-02-28 10:002028-02-29 09:00 UTC
month-boundaryFREQ=DAILY;BYHOUR=9 in utc2026-08-31 10:002026-09-01 09:00 UTC
year-boundaryFREQ=WEEKLY;BYDAY=FR;BYHOUR=8 in utc2026-12-31 12:002027-01-01 08:00 UTC
local-timezone-refusedFREQ=DAILY;BYHOUR=9 in local2026-08-28 08:00rejected: local_timezone_unresolved

Missed slots, downtime, and clock changes

The scheduler runs a pass when the daemon starts, before its first wait, and then every 60 seconds measured on a monotonic clock. These successful actions wake it for an immediate pass:

  • coven.automations.create, .update, .delete, .run, .tick, and .unquarantine;
  • definition.create.v1, definition.revise.v1, definition.activate.v1, definition.pause.v1, and definition.tombstone.v1;
  • run.cancel.v1, occurrence.cancel.v1, attempt.cancel.v1, attempt.retry.v1, occurrence.recover.v1, and occurrence.runNow.v1.

Disabling a routine, a rich command.v1 change, and imports take effect at the next 60-second pass.

Each pass reads the wall clock and applies the misfire policy, which is always latest:

  • After any gap, whether downtime, sleep, or a clock set forward, a pass plans only the most recent due slot. Earlier missed slots never run, and a backfill policy is refused.
  • A routine's still-planned older slots settle skipped with the reason superseded by latest misfire policy.
  • Each slot is planned once. A later pass that finds it already planned changes nothing.
  • A clock set backward never plans a slot older than one already planned.
  • A paused routine never plans.

The daemon does not detect suspend, resume, or clock jumps separately; reading the wall clock on every pass is what covers them.

4 cases from misfire-latest-planning.vectors.json:

misfire-latest-planning conformance cases
CaseRoutineTimeline (UTC)Two passesPlanned slots afterwards (UTC)
restart-collapses-to-latest-due-slotACTIVE FREQ=DAILY;BYHOUR=9created 2026-09-01 08:00; observed 2026-09-04 12:00planned then already_fenced2026-09-04 09:00
existing-latest-fence-replays-idempotentlyACTIVE FREQ=DAILY;BYHOUR=9created 2026-09-01 08:00; slot 2026-09-04 09:00 already planned; observed 2026-09-04 12:00already_fenced then already_fenced2026-09-04 09:00
clock-rollback-adds-no-older-fenceACTIVE FREQ=DAILY;BYHOUR=9created 2026-09-01 08:00; slot 2026-09-04 09:00 already planned; observed 2026-09-03 12:00not_due then not_due2026-09-04 09:00
paused-definition-never-plansPAUSED FREQ=DAILY;BYHOUR=9created 2026-09-01 08:00; observed 2026-09-04 12:00not_due then not_duenone

The startup cases below check when the pass that launches due work runs. The conformance target's stand-in runtime records the launch and nothing else, so these cases do not show how the run ends.

2 cases from startup-reconciliation-wake.vectors.json:

startup-reconciliation-wake conformance cases
CaseRoutine at startChange while waitingPass that launched itObserved
startup-runs-due-work-before-first-waitACTIVE FREQ=DAILY;BYHOUR=9nonestartup, 1 launchlaunched before the first wait: yes
definition-revision-wakes-sleeping-schedulerPAUSED FREQ=DAILY;BYHOUR=9revised to ACTIVE at 2099-09-02 08:00 UTCwake, 1 launchaction accepted: yes; scheduler woken: yes

Overlap

The overlap policy is always forbid. A due slot is not claimed while the same routine has another claimed or running occurrence or a running run; it stays planned. A settled occurrence does not block. A claim takes a 60-minute lease for its owner.

7 cases from overlap-forbid-claiming.vectors.json:

overlap-forbid-claiming conformance cases
CaseThe routine's other workDue slot claimedDue slot afterwardsLease
no-overlap-claimsnoneyesclaimed, attempt 1daemon-a until 2026-09-01 11:00 UTC
claimed-occurrence-blocksclaimed_occurrencenoplanned, attempt 0none
running-occurrence-blocksrunning_occurrencenoplanned, attempt 0none
running-run-blocksrunning_runnoplanned, attempt 0none
succeeded-occurrence-allowssucceeded_occurrenceyesclaimed, attempt 1daemon-a until 2026-09-01 11:00 UTC
failed-occurrence-allowsfailed_occurrenceyesclaimed, attempt 1daemon-a until 2026-09-01 11:00 UTC
cancelled-occurrence-allowscancelled_occurrenceyesclaimed, attempt 1daemon-a until 2026-09-01 11:00 UTC

Leases

A claim whose lease has expired before dispatch, with no runtime evidence, settles failed with the reason lease expired. A lease that expires exactly now counts as expired. Once runtime evidence exists or the occurrence is running, the lease no longer settles it; only the session's own evidence does.

5 cases from occurrence-lease-recovery.vectors.json:

occurrence-lease-recovery conformance cases
CaseOccurrenceNow, lease expiry (UTC)Runtime evidenceAfter lease recovery
expired-before-dispatchclaimed2026-09-01 10:00, 2026-09-01 09:59:59.999nofailed: lease expired
exact-lease-deadlineclaimed2026-09-01 10:00, 2026-09-01 10:00nofailed: lease expired
unexpired-claimclaimed2026-09-01 10:00, 2026-09-01 10:00:00.001nounchanged, claimed
runtime-owned-occurrencerunning2026-09-01 10:00, 2026-09-01 09:59:59.999nounchanged, running
claimed-with-runtime-evidenceclaimed2026-09-01 10:00, 2026-09-01 09:59:59.999yesunchanged, claimed

Retries and backoff

A routine's retry policy has:

  • maxAttempts, from 1 to 10;
  • backoffPolicy, one of none, fixed, or exponential;
  • backoffSeconds, from 1 to 86400, required for fixed;
  • retryableClasses, chosen from transient_dispatch, lease_expired, and runtime_unavailable.

A retry is a new attempt inside the same run and the run's original deadline. The wait counts from when the failure was observed:

  • none retries immediately.
  • fixed waits exactly backoffSeconds.
  • exponential picks a delay between 1 second and a ceiling of backoffSeconds × 2attempt − 2, capped at one day (86,400 seconds). The delay is derived from the run id and attempt number, so the same attempt always waits the same time.

When no automatic retry applies, an ordinary run is held running until its deadline so you can open an operator retry; see Operator retry.

5 cases from retry-backoff-timing.vectors.json:

retry-backoff-timing conformance cases
CaseBackoff, next attemptDelay (range)Failure observed, retry not before (UTC)
no-backoff-is-immediatenone; attempt 2 of 20 s (0–0 s)2026-09-01 10:00, then 2026-09-01 10:00
fixed-backoff-starts-at-observationfixed, 60 s base; attempt 2 of 260 s (60–60 s)2026-09-01 10:00:50, then 2026-09-01 10:01:50
exponential-second-attempt-uses-base-ceilingexponential, 10 s base; attempt 2 of 48 s (1–10 s)2026-09-01 10:00, then 2026-09-01 10:00:08
exponential-fourth-attempt-grows-ceilingexponential, 10 s base; attempt 4 of 417 s (1–40 s)2026-09-01 10:00, then 2026-09-01 10:00:17
exponential-backoff-caps-at-one-dayexponential, 50000 s base; attempt 3 of 361621 s (1–86400 s)2026-09-01 10:00, then 2026-09-02 03:07:01

Quarantine

When a run uses up maxAttempts, the routine is quarantined and the run is held for an operator retry like any other failed ordinary run. A quarantined routine stops planning, refuses manual runs and operator retries, and coven.automations.health reports nextDueAt as null with the quarantine's time, failure class, reason, and count of consecutive exhaustions. A later exhaustion keeps the latest evidence and increases the count. coven.automations.unquarantine releases the routine, and its next due slot is planned again; releasing a routine that is not quarantined changes nothing.

4 cases from retry-quarantine-recovery.vectors.json:

retry-quarantine-recovery conformance cases
CaseRetry exhaustions (UTC)Release (UTC)Quarantine afterwardsNext due slot
first-exhaustion-quarantines-and-blocksruntime_unavailable at 2026-09-02 08:30not calledquarantined since 2026-09-02 08:30 for runtime_unavailable, 1 in a rownot planned
repeated-exhaustion-updates-latest-evidenceruntime_unavailable at 2026-09-02 08:30; lease_expired at 2026-09-02 08:45not calledquarantined since 2026-09-02 08:45 for lease_expired, 2 in a rownot planned
explicit-release-restores-schedulingruntime_unavailable at 2026-09-02 08:30at 2026-09-02 08:45not quarantinedplanned and claimed
release-without-quarantine-is-noopnoneat 2026-09-02 08:45, changed nothingnot quarantinedplanned and claimed

Timeouts and cancellation

Every routine has timeoutMinutes, from 1 to 44640 (31 days); the rich definition shape calls it perRunMinutes. A run's deadline is its start plus the timeout:

  • At the deadline, Coven stops the session and settles the run failed and its attempt timed_out.
  • If the stop cannot be confirmed, the run stays unresolved instead of being reported finished.
  • A retry still waiting when the deadline passes is settled: the attempt timed_out, and the run and occurrence failed.
  • A run held for an operator retry has no live session. If no retry is opened by the deadline, the run and occurrence settle failed, and the failed attempt stays failed.

When a cancellation and a timeout race, whichever takes the run's stop fence first decides the outcome, and the session is stopped once. For the cancellation commands themselves, see Manual runs and health.

2 cases from cancellation-timeout-arbitration.vectors.json:

cancellation-timeout-arbitration conformance cases
CaseTimes (UTC)Stop fence first taken byOutcomesFinal states
cancellation-stop-fence-defers-late-timeoutcancel requested 2099-10-01 08:29:45; deadline 2099-10-01 08:30; timeout seen 2099-10-01 08:30:01cancellationcancellation cancelled; timeout deferred; session stopped oncerun cancelled; occurrence cancelled; attempt cancelled; cancellation cancelled
timeout-stop-fence-rejects-stale-cancellationcancel requested 2099-10-01 08:29; deadline 2099-10-01 08:30; timeout seen 2099-10-01 08:31nonecancellation illegal_transition; timeout timed_out; session stopped oncerun failed; occurrence failed; attempt timed_out; cancellation rejected

Previewing a schedule

The daemon has no preview or dry-run action for schedules. To see how a rule resolves, compare it with the calendar cases above; to see an existing routine's next run, read nextDueAt from coven.automations.health.

Refused vocabulary

The daemon refuses these rather than approximating them:

  • recurrence beyond the subset above;
  • misfire backfill, and any overlap policy other than forbid;
  • a durable local time zone;
  • webhook triggers and multi-step pipeline actions;
  • an atomic outputTarget.

The daemon's variantNegotiation capability metadata lists the refused variants; see Versioned definition commands.

Was this page helpful?No

Last updated on

On this page