MeridiansMeridians

Scheduler — cadence to fired run [Flow]

Source path: knowledge-base/diagrams/flows/scheduler.md

# Scheduler — cadence to fired run `[Flow]`

The headless Program scheduler is the **time-based orchestration** layer: it reads enabled schedules from the record, arms Croner timers, and on each tick runs a Research/Opinion/Merge/Position operation to completion — under concurrency, serialization, and spend gates. The pure *when-to-fire* logic is separate from the imperative shell that does the waking.

```mermaid
flowchart TB
    Rec[("record — enabled schedules<br/>(days · hours · everyNWeeks)")]
    Meta[("META — **baseline operations timezone**<br/>(aux/meta.json · one instance-wide IANA zone)")]
    Rec -->|"reconcile() · per-document fingerprint + compact projection<br/>read only the source that changed"| Arm
    Meta -->|"all cadences interpreted in it"| Arm

    subgraph Arm["arm — pure WHEN → live timers"]
        direction TB
        Cron["scheduleToCron(schedule)<br/>(future windows only — no boot replay)"]
        Timer["new Cron(pattern, tz, onFire)<br/>diff vs live timers: add new · drop stale"]
        Cron --> Timer
    end

    Timer -->|"tick"| Fire

    subgraph Fire["onFire — the gates"]
        direction TB
        Week{"weekGatePasses?<br/>(fortnightly/monthly — cron can't express)"}
        Spend{"spend cap?<br/>(provider-spending jobs only)"}
        Serial{"domain already running?<br/>(per-domain serialization)"}
        Conc{"concurrency slot?<br/>(global cap, default 3)"}
        Week -->|"no"| Skip1(["skip until due"])
        Week -->|"yes"| Spend -->|"blocked"| Skip2(["skip — cap reached"])
        Spend -->|"ok"| Serial -->|"busy"| Queue(["queue — one run per domain"])
        Serial -->|"free"| Conc -->|"full"| Wait(["wait for a slot"])
        Conc -->|"slot"| Run
    end

    Run["runOperationToEnd — the Research/Opinion/Merge/Position op<br/>(coalesced cycleIds if cadence matches)"]
    Run -->|"same op machinery"| Ops(["operation-lifecycle → commits + echo"])
```

**Invariants**

- **Croner wakes; the record decides.** The scheduler holds no schedule authority of its own — it derives timers from enabled schedules. The 30-second `reconcile()` poll checks cheap per-document sidecar/body-stat fingerprints and retains only each source's compact job projection, so one canonical write re-reads one Domain/Constellation rather than the whole record; incomplete reads are never cached. Timers fire in **future windows only**; there is no boot replay of missed runs.
- **One baseline timezone, all cadences relative.** A cadence stores only its wall-clock day/hour; the ONE instance-wide **baseline operations timezone** (persisted in the record's META, Director-set in Settings) interprets every cadence at once. Croner fires the local hour in the baseline zone → the exact UTC instant; changing the baseline re-relativizes every schedule with no per-domain rewrite. Hydration seeds it from META, else the operator's browser zone. Cadence editors and the agenda render local-primary + UTC-secondary.
- **The everyNWeeks gate is checked at fire time.** Cron can't express fortnightly/monthly, so `weekGatePasses(schedule, lastRunAt, now)` gates it in the shell. Research cycles sharing a schedule **coalesce** into one Watch pass (days/hours sorted → order-independent).
- **Three discipline gates, all independent.** Per-domain serialization (at most one mutating run per domain), a global concurrency cap (default 3), and a per-instance **weekly spend ceiling** that gates only provider-spending jobs (syncs are exempt). Failures are isolated — one domain's error never stalls another.
- **Only always-on hosts arm it.** `isAlwaysOnServer()` gates the scheduler, reconcile loop, and journal drain ([platform-model](../concepts/platform-model.md)); serverless hosts run no timers.

**Where it lives:** `src/lib/server/program/scheduler.ts` (imperative shell — timers + gates), `src/lib/core/program/schedule-cron.ts` (pure `scheduleToCron` · `weekGatePasses` · `nextFireAt`, all baseline-zone-aware), `src/lib/core/program/operations-timezone.ts` (the baseline resolver: get/set/subscribe, validated), `src/lib/server/record/instance-usage.ts` (spend estimate). The run itself: [operation-lifecycle](operation-lifecycle.md) · [maintenance-run](maintenance-run.md). Timing rules owned by `src/lib/core/program/schedule-cron.ts`.
Open on GitHub

Raw Markdown source