Skip to content

How it works

An occurrence is a work item

Apiary polls this plugin on the source's poll_interval. Each call is a fresh process: the daemon writes one JSON request to stdin, reads one JSON response from stdout, and the process exits. Nothing survives in memory between calls.

On each poll the plugin asks, for every routine: which occurrences of this cron expression have come due since the last one I emitted? Each answer becomes a work item whose id is the routine id and the occurrence instant:

nightly-dep-audit@2026-08-27T03:00:00Z

That id is Apiary's dedup key, so it must be stable per occurrence — which is exactly what a cron instant is. It is always rendered in UTC, so changing a routine's timezone cannot make an already-emitted occurrence look new.

The item carries a routine:<id> label, so a workflow trigger can select one routine without your config repeating itself, plus metadata (routine_id, scheduled_for, schedule, timezone) readable from the task.

The cursor

Because the process holds no state, the "last occurrence emitted" cursor lives in state_file:

{
  "version": 1,
  "routines": {
    "nightly-dep-audit": {
      "last_emitted": "2026-08-27T03:00:00Z",
      "first_seen": "2026-08-20T14:22:10Z",
      "emitted_count": 7
    }
  }
}

It is written atomically (temp file plus rename) before the poll answers, so a crash mid-write cannot leave a truncated cursor. If the cursor cannot be persisted the poll fails rather than emitting: the host logs it and retries on the next interval, which is the same recovery an unreachable ticket API gets.

That ordering makes the failure mode on a crash a skipped occurrence rather than a duplicated one. This is deliberate. A skipped run is visible as a gap and re-fires next occurrence; a duplicated run leaves an operator with two results and no way to tell which to trust.

Why not the host's since watermark?

Apiary passes a since timestamp on every poll, and it would be the obvious cursor. It is not usable: the daemon keeps it in process memory and resets it to zero on restart, so the first poll after every restart asks for all of history. Trusting it would emit a backlog on each daemon restart. The plugin treats since as informational.

A new routine emits nothing

The first poll that sees a routine registers a floor (first_seen) and emits nothing. Adding a routine at noon must not immediately fire this morning's 03:00 occurrence, and catch-up must never reach back to before the routine was configured.

The first real occurrence therefore fires at the first scheduled instant after you added it.

Missed occurrences

When the daemon is down, occurrences pass unobserved. What happens on the next poll depends on catch_up:

catch_up Behaviour
false (default) The whole missed window collapses to one run — the most recent missed occurrence. Ten missed nightly audits fire one audit.
true The backlog is emitted in order, oldest first, bounded by max_per_poll — so a long outage drains over several polls instead of dispatching a week of agent runs in one tick.

The default is off because "the nightly audit did not run last night" is worth acting on once and worthless five times.

The storm cap

max_per_poll (default 5) bounds how many occurrences all routines together may emit in one poll. It mirrors the guardrail Apiary's own Prometheus source uses against alert storms. Overflow stays in the state file and surfaces on the next poll.

Daylight saving

Occurrences are computed in the routine's own location, so 0 3 * * * in America/Sao_Paulo means 03:00 there regardless of where the daemon runs.

Across a DST boundary the two awkward cases are handled by construction:

  • Spring forward, where the scheduled wall-clock time does not exist: the underlying cron implementation serves the day at the next valid instant. Occurrences stay strictly ordered, so the cursor always advances.
  • Fall back, where the wall-clock time happens twice: the two occurrences are different instants, and because occurrence ids are rendered in UTC they get different ids. Neither is swallowed as a duplicate.