Clockwork

Stored schedules that start batch jobs

Clockwork is the scheduler of a Korall Engineering™ node. It keeps a book of stored schedules. Each schedule has a time rule and a batch job type. At each time of the rule, clockwork submits one batch job, and the batch system does the work.

Clockwork remembers what it fired, what it missed and how each run ended. So it can wait for the last run before it starts the next. After the node was off, it knows which runs it missed, and it starts them at once.

Every node has clockwork: Hub, Agent and Remote.


The schedule

Field Type Default Meaning
id text issued The book issues the id, as cw-3. The book never issues one id twice
name text empty What the operator reads. It does not have to be unique
rule text — The time rule, in one of the two syntaxes
zone IANA name the zone of the computer The zone of the rule, as Europe/Oslo. The schedule stores the name, so a move of the computer to another zone does not move the schedule
command batch job type — What a firing submits
arguments JSON object empty The data of the batch job
enabled bool true Off: no firing, and no missed run is counted
overlap skip, wait-for-last, parallel wait-for-last What a firing does while the last run is busy. See Overlap
catchUp none, once, all all What starts for the runs that the node missed. See Catch-up
catchUpMax 1 to 1000 10 The most missed runs that all starts
jitterSeconds 0 to 3600 0 A delay from 0 to this many seconds, drawn once for each firing

A schedule also keeps its state: the last time that it dealt with, the next time, the firings that wait, the batch job that is busy, and its last 200 runs.


The two syntaxes

Cron

Five fields: minute, hour, day of month, month and day of the week.

Field Values
minute 0 to 59
hour 0 to 23
day of month 1 to 31
month 1 to 12, or jan to dec
day of the week 0 to 7, or sun to sat. 0 and 7 are both Sunday

Each field takes *, a number, a range a-b, a list a,b,c, and a step */n or a-b/n. A name can be in any case.

When the day of month and the day of the week both start with something other than *, a day matches when either one matches. When one of them starts with *, both must match. This is the rule of Vixie cron.

Rule Fires
0 2 * * * Every day at 02:00
*/15 * * * * At minutes 0, 15, 30 and 45 of every hour
30 8 * * mon-fri At 08:30 from Monday to Friday
0 0 13 * fri At 00:00 on the 13th of each month and on each Friday
0 0 1 */3 * At 00:00 on 1 January, 1 April, 1 July and 1 October

The short forms:

Short form Means
@hourly 0 * * * *
@daily, @midnight 0 0 * * *
@weekly 0 0 * * 0
@monthly 0 0 1 * *
@yearly, @annually 0 0 1 1 *

Every

every(N).unit, then optionally .at(HH:MM) and .jitter(S).

Unit Fires
seconds, minutes, hours Every N units from the creation of the schedule. A change to or from summer time does not move it
days Every N days at the local time of .at(). Without .at(), at the minute of the creation
weeks Every N weeks, on the weekday of the first firing, at the local time of .at()

The singular forms (minute, day) are the same units. .at() is only for days and weeks. .jitter(S) sets the jitter, from 0 to 3600 seconds. A rule that sets .jitter() needs the jitter field of the schedule at 0.

Rule Fires
every(15).minutes Every 15 minutes
every(1).days.at(02:00) Every day at 02:00
every(2).weeks.at(18:00) Every second week, on the weekday of the first firing, at 18:00
every(1).hours.jitter(60) Every hour, up to 60 seconds late

Refused rules

Clockwork refuses a rule that it cannot fire, and the refusal names the field and the reason. The editor, the OPAL tasks and the book use the one parser.

Rule Refusal
empty rule: the rule is empty
0 0 30 2 * day of month: the rule never fires: none of its months has such a day
60 * * * * minute: 60 is out of range: the field takes 0 to 59
0 0 * * * * rule: a cron rule has five fields, and this one has six. Clockwork takes no seconds field: the finest step is one minute
@reboot rule: @reboot is an event, not a time. Clockwork fires at times
every(0).minutes every: every(0) never fires: N must be 1 or more
every(3).fortnights unit: `fortnights` is not a unit. Use seconds, minutes, hours, days or weeks

Time, zones and summer time

Clockwork evaluates a cron rule and an .at() rule in the zone of the schedule. It evaluates an elapsed rule in UTC. It stores every time in UTC.

Case Example What fires
A local time that the change to summer time skips 02:30 in Oslo on the last Sunday of March One firing, at the first instant after the gap: 03:00
A local time that the change to winter time repeats 02:30 in Oslo on the last Sunday of October One firing, at the first 02:30
A rule that matches each minute of a repeated hour */15 2 * * * Each quarter fires once. The second pass of the hour fires nothing

These rules are the rules of Vixie cron.

A nominal time is the time that the rule names. An actual time is the time when clockwork submitted the job. The two differ by the jitter, by a wait for the last run, and by a catch-up. Clockwork counts nominal times.


A firing

Each firing becomes one batch job:

Job field Value
type The schedule's command
data The schedule's arguments, and clockwork = {schedule, nominal, reason}
source clockwork:<id>, or clockwork-manual:<id> for a run now
originType, originId system, and the id of the schedule
displayLabel The name of the schedule and the nominal time, as Nightly (2026-10-18 02:00)
correlationKey clockwork:<id>:<nominal time>, or clockwork-manual:<id>:<time>

reason is scheduled, catch-up or manual. A job of a catch-up carries its own nominal time, so a job that does the work of one period can use it.

Clockwork submits through submitOrGet(). If the queue has a job with the same correlation key, it returns that job, so no second job starts for one nominal time. That holds after a crash between the submit and the save of the book.


Overlap

A run is busy from its submit until its batch job is done, failed, stopped or canceled.

overlap A firing while the last run is busy
skip Is not submitted. The history records it as skipped-busy
wait-for-last Waits. When the busy job ends, the oldest waiting firing starts at once
parallel Is submitted at once

Under wait-for-last, one scheduled firing waits at most. A newer one takes its place, and the history records the older one as coalesced. The firings of a catch-up and of a run now keep their place.

A waiting firing keeps the command and the arguments that the schedule had when the firing became due. An edit of the schedule changes the later firings.

A job that no worker runs stays pending in the queue. Its run stays busy, so the next firing waits or is skipped. The Batch screen says why no worker takes the job.


Catch-up

When the node was off, or the computer slept, or the clock jumped forward by more than one period, clockwork finds the runs that it missed.

catchUp What starts What the history records
none Nothing Each missed time as missed
once One run, for the newest missed time The others as missed-merged
all One run for each missed time, oldest first, at most catchUpMax The times over the cap as missed-over-cap. The newest times run

The runs of a catch-up go through the overlap policy, so under wait-for-last they run one after another. They take no jitter.

Example: a schedule fires every day at 02:00. The node is off from the 13th at 20:00 to the 17th at 09:00. At the start, clockwork submits the run of the 14th. When it ends, it submits the run of the 15th, then the 16th, then the 17th. The next firing is the 18th at 02:00.

A clock that goes back fires nothing again. The next firing is the first nominal time after the last one that clockwork dealt with.

A disabled schedule counts no missed run. When it is enabled again, it starts with its next time.


The book and the lock

The book is the file clockwork.json in the base folder of the node.

  • Each write is the whole file, so a crash leaves the old file or the new one.
  • A book of a newer schema version is refused. Nothing fires until a clockwork that reads that version runs, and the file is not changed.
  • A book that does not parse is moved aside as clockwork.json.broken-<time>, and the book starts empty.

Two nodes can share one base folder. The file clockwork.lock in the folder decides which node fires. The other node shows the schedules read-only, names the node that fires them, and tries the lock again every 60 seconds. When the first node ends, the second one takes over.


The node switch

The setting clockwork.enabled, "Run stored schedules", is in the node schema. Off: no schedule fires, and the time that it is off counts no missed run.


The OPAL tasks

Task Does Grant Confirmation
clockwork.list The schedules, with the rule in words, the next time and the last outcome none no
clockwork.get id=<id> One schedule, its rule in words and its next 5 times none no
clockwork.history id=<id> [limit=50] The runs of one schedule, newest first none no
clockwork.preview rule=<rule> [zone=] [count=5] [after=] The next times of a rule in a zone, with no schedule none no
clockwork.put [id=<id>] <fields> Create a schedule, or change its fields. reset=1 brings back the values of a seeded schedule clockwork.write once per session
clockwork.pause id=<id> Pause one schedule clockwork.write no
clockwork.resume id=<id> Resume one schedule clockwork.write no
clockwork.run-now id=<id> Fire one schedule now, once clockwork.run every call
clockwork.delete id=<id> Delete one schedule. A busy job runs to its end clockwork.write every call

clockwork.put takes the fields of the schedule by name. A refused rule answers with the field and the reason of the parser:

{"task": "clockwork.put", "args": {"name": "Nightly audit", "rule": "0 2 * * *", "command": "trove.recurring.run", "arguments": {"spec": "wfc7wjvxtnw"}}}

Run outcomes

Outcome Means
submitted The batch job runs, or waits in the queue
done, failed, stopped, canceled The batch job ended so
skipped-busy Under skip, the last run was busy
coalesced Under wait-for-last, a newer firing took its place
missed Under catch-up none
missed-merged Under catch-up once, a newer missed time ran for it
missed-over-cap Under catch-up all, over catchUpMax
suspended The zone or the rule is not one that this computer can use. The schedule fires again when it is
In this section
Topics
reference clockwork schedule cron batch