Elements

Clock

Time — recurring schedules, intervals, and durable sleeps as first-class declarations, not a bolted-on cron library.

Clock is how your app deals with time: cleanup jobs that run every hour, reports due at 9am Riyadh time, a flow that pauses for seven days and wakes up even after a deploy. There is no separate scheduler to install — a schedule is a trigger, and the flow it fires is the same species as every other flow.

The one rule

Flow code never calls Date.now() — it asks fx.clock.now(). Time is injected, which makes it deterministic in tests and auditable in traces.

Quick start

Fire a flow on an interval

The simplest schedule is an anonymous interval trigger:

src/flows/links/purge.ts
import { on, flow, every } from "okengine";

export const purgeOld = on(
  every("1h"),
  flow({
    do: async (_, fx) => {
      const cutoff = fx.clock.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
      await fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
    },
  }),
);

Or declare a named schedule

A named clock() shows up in the Console, supports cron expressions and timezones, and can be paused or retuned at runtime:

src/clocks.ts
import { clock } from "okengine";

export const dailyReport = clock("daily-report", {
  cron: "0 9 * * *", // 09:00
  timezone: "Asia/Riyadh",
  overridable: true, // the Console may edit this schedule
});
src/flows/reports/daily.ts
export const sendDaily = on(
  dailyReport,
  flow({
    do: async (_, fx) => {
      /* … */
    },
  }),
);

Ask for time inside flows

fx.clock is the only clock a flow knows:

do: async (input, fx) => {
  const now = fx.clock.now(); // epoch-ms, injectable
  await fx.clock.sleep("wait-for-payment", "7d"); // durable — survives restarts
};

Two kinds of schedules

DeclarationShows in ConsoleCron + timezoneRuntime-editableUse for
every("1h")nononoSimple fixed intervals
clock(name, opts)yesyeswhen overridableBusiness schedules operators tune

Both are triggers consumed with the same on(trigger, flow) — the flow underneath does not know the difference.

clock() options

OptionTypeDefaultMeaning
cronstringCron expression m h dom mon dow (this or every required)
everystringFixed interval: "30s" · "10m" · "1h" · "7d"
timezonestring"UTC"IANA timezone for cron evaluation
overridablebooleanfalseAllow the Console to edit / pause the schedule

Sleeping inside a flow

fx.clock.sleep(label, duration) is a durable sleep: in a durable: true flow the wake time is journaled, so the flow resumes after restarts and deploys instead of losing its place. The label names the step in the journal — it is what the Console shows when you inspect a sleeping run.

In a non-durable flow the same call resolves immediately, so code reads identically in tests.

What the runtime guarantees

GuaranteeWhat it means
Leader electionWith several replicas, only one instance fires each cron tick
Catch-up "one"A schedule missed during downtime fires once, not once per missed tick
Reconciled at bootNamed clocks are written into the store (oke_crons) — the scheduler reads state, not code
DST-awareAmbiguous local times around DST transitions are detected and handled

Per-environment drivers

oke.config.ts
drivers: {
  clock: { local: "memory", docker: "postgres", test: "frozen", prod: "postgres" },
},
DriverBehavior
memoryIn-process timers — fast local loop, lost on exit
postgresSchedules and wakes persisted in your Postgres — survives restarts
frozenDeterministic test clock — time advances only when the test says so

frozen is why the no-Date.now() rule pays off: tests inject time travel through fx.clock and every flow obeys it automatically.

Operating schedules from the Console

The Console (:6533 → Clock) lists every named clock with a four-number health view — drift, past-due, next run, and which replica holds the leader lease. From there you can pause, edit the schedule, or wake early — but only for clocks declared with overridable: true. A non-overridable clock rejects edits with ScheduleNotOverridableError, so a mistyped production change cannot silently reshape your cadence.

Troubleshooting

Learn more

  • Flowon(trigger, flow) and the fx surface
  • Console · Clock — health numbers, pause / edit / wake early
  • Signal — reacting to events instead of time

Next

On this page