Clock is how your backend **knows what time it is and when to run again**. A monthly invoice job, a 30s health ping, and a three-day trial reminder share one vocabulary: declare a named schedule, bind it with `on(clockDecl, flow("name", { do }))`, and read time only through `fx.clock`.

For developers scheduling work on okengine — one handle shape; drivers swap by environment.

<Callout title="The one rule">
  All time access and pauses go through `fx.clock`. `Date.now()`, `new Date()`, and `setTimeout`
  bypass the injectable clock and break durable sleep / time-travel tests.
</Callout>

<ClockSchedules />

## Smallest Example

<Steps>

<Step>
### Declare a named clock

```typescript title="src/clocks/digest.ts"
import { clock } from "okengine";

export const digestClock = clock.every("notes.digest", "1d");
```

</Step>

<Step>
### Bind a Flow

```typescript title="src/flows/notes/digest.ts"
import { on, flow } from "okengine";
import { digestClock } from "@/clocks/digest";
import { db, notes } from "@/schema";
import { isNull } from "drizzle-orm";

export const digest = on(
  digestClock,
  flow("notes.digest", {
    plane: "operator",
    do: async (_, fx) => {
      const rows = await fx.store(db).select().from(notes).where(isNull(notes.archivedAt));
      return { active: rows.length, at: new Date(fx.clock.now()).toISOString() };
    },
  }),
);
```

</Step>

<Step>
### See it tick

With `oke dev`, the scheduler reconciles `notes.digest` into the Store and leader-elects
before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms).
Map to ISO on the wire with `new Date(fx.clock.now()).toISOString()`.

Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting a day.

</Step>

</Steps>

<Callout title="Jobs are Flows">
  Write the schedule inline or export it — [Inline or named export](#inline-or-named-export).
  Every consumer still uses `flow("name", { do })`. See
  [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
</Callout>

## Inline or named export

| Style                                                     | When                              |
| --------------------------------------------------------- | --------------------------------- |
| `on(clock.every("name", "1h"), flow("ops.ping", { do }))` | One Flow owns this schedule       |
| `export const x = clock.every("name", "1h")`              | Several Flows share this schedule |

<Callout title="Why Clock can be inline">
  `fx.clock` is now / sleep / offsets — not an emit. Signal stays an exported const so producers can
  `fx.emit(handle, payload)`. Export a Clock const only to share one schedule across named Flows.
</Callout>

Both styles pass a real `flow("name", { do })`. Nameless `flow({ do })` fails
**OKE1072** — the trigger's name is never the Flow's name.

<Tabs items={["Inline", "Named"]}>

<Tab value="Inline">

One file — declare and bind together. The scheduler fires it; no other Flow
binds this cadence:

```typescript title="src/flows/health/ping.ts"
import { on, flow, clock } from "okengine";

export const pingExternal = on(
  clock.every("health.pingExternal", "30s"),
  flow("health.pingExternal", {
    plane: "operator",
    do: async (_, fx) => {
      await fx.call(pingUpstream);
    },
  }),
);
```

</Tab>

<Tab value="Named">

Export the handle when a second `on()` must reuse the same schedule. Each Flow
keeps its own explicit name — **OKE1070** if they collide:

```typescript title="src/clocks/metrics.ts"
import { clock } from "okengine";

export const tickClock = clock.every("metrics.tick", "1h");
```

```typescript title="src/flows/ops/metrics.ts"
import { on, flow } from "okengine";
import { tickClock } from "@/clocks/metrics";

export const sweep = on(
  tickClock,
  flow("ops.sweep", {
    plane: "operator",
    do: async (_, fx) => {
      await fx.call(sweepMetrics);
    },
  }),
);

export const report = on(
  tickClock,
  flow("ops.report", {
    plane: "operator",
    do: async (_, fx) => {
      await fx.call(reportMetrics);
    },
  }),
);
```

</Tab>

</Tabs>

## Progressive Patterns

Same `clock` + `fx.clock` from a calendar cron to an interval, a durable pause, and typed offsets:

<Tabs items={["Cron", "Interval", "Sleep", "Time helpers"]}>

<Tab value="Cron">

Five-field cron plus helpers. Prefer an app-wide zone so schedules stay short:

```typescript title="src/clocks/reports.ts"
import { clock } from "okengine";

export const dailyReportClock = clock.daily("reports.daily", {
  at: "06:00",
});
```

```typescript title="src/flows/reports/daily.ts"
import { on, flow } from "okengine";
import { dailyReportClock } from "@/clocks/reports";

export const runDailyReport = on(
  dailyReportClock,
  flow("reports.runDaily", {
    plane: "operator",
    do: async (_, fx) => {
      await fx.call(buildDailyReport, { at: fx.clock.now() });
    },
  }),
);
```

Set `oke({ clock: { timezone: "Asia/Riyadh" } })` once (or the same in
`defineConfig`). Omit per-clock `timezone` unless one schedule must differ —
see [Schedules](/docs/elements/clock/schedules).

</Tab>

<Tab value="Interval">

Human durations — integer + unit, no weeks: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:

```typescript title="src/clocks/health.ts"
import { clock } from "okengine";

export const pingClock = clock.every("health.pingExternal", "30s");
```

```typescript title="src/flows/health/ping.ts"
import { on, flow } from "okengine";
import { pingClock } from "@/clocks/health";

export const pingExternal = on(
  pingClock,
  flow("health.pingExternal", {
    plane: "operator",
    do: async (_, fx) => {
      await fx.call(pingUpstream);
    },
  }),
);
```

Per-tenant expansion and duration rules: [Schedules](/docs/elements/clock/schedules).

</Tab>

<Tab value="Sleep">

`fx.clock.sleep(label, duration)` — two arguments. Needs `durable: true` to park across restarts:

```typescript title="src/flows/trials/start.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const start = on(
  http.post({
    in: z.object({ email: z.string().email() }),
  }),
  flow({
    durable: true,
    do: async ({ email }, fx) => {
      await fx.step("mark-trial", async () => {
        await fx.call(startTrial, { email });
      });
      await fx.clock.sleep("expiry-window", "3d");
      await fx.step("notify", async () => {
        await fx.send(trialExpiringEmail, { to: email });
      });
    },
  }),
);
```

Without a journal, sleep resolves immediately. Deep dive: [Durable Sleep](/docs/elements/clock/sleep).

</Tab>

<Tab value="Time helpers">

Never call `Date.now()` in a Flow — use the injectable surface:

```typescript
const now = fx.clock.now(); // epoch-ms
const cutoff = fx.clock.ago("7d"); // now − 7 days
const due = fx.clock.fromNow("14d"); // now + 14 days
const weekMs = fx.clock.duration("7d"); // span in ms
const expiresAt = createdAt + weekMs;
```

Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a
calendar day. Unknown strings parse as `0`.

</Tab>

</Tabs>

## Capability Reference

| Surface       | Signature                                                | Purpose                        | `do` input |
| ------------- | -------------------------------------------------------- | ------------------------------ | ---------- |
| Helpers       | `clock.daily` · `hourly` · `weekly` · `monthly` · `cron` | Calendar presets + field bags  | none (`_`) |
| Interval      | `clock.every(name, duration, opts?)`                     | Fixed duration loop            | none (`_`) |
| Per-tenant    | `clock.perTenant(name, opts)`                            | One Store row per tenant       | none (`_`) |
| Bare callable | `clock(name, { cron? \| every?, … })`                    | Same decl; lower-level form    | none (`_`) |
| Now / offsets | `fx.clock.now` · `ago` · `fromNow` · `duration`          | Injectable time math           | —          |
| Durable sleep | `fx.clock.sleep(label, duration)`                        | Park a durable Flow until wake | —          |

At least one of `cron` or `every` is required on every declaration.

## Per-environment drivers

Standard starters inherit `DRIVER_DEFAULTS`. Pin only when you diverge:

```typescript title="oke.config.ts"
import { defineConfig } from "okengine/config";

export default defineConfig({
  drivers: {
    // omit clock to use defaults — pin only overrides
    // clock: { dev: "postgres", test: "frozen", prod: "postgres" },
  },
});
```

| Driver     | Runs as                     | Best for                               |
| ---------- | --------------------------- | -------------------------------------- |
| `postgres` | Shared `oke_crons` + leases | Dev/prod multi-instance (default)      |
| `file`     | `.oke/crons.json`           | Single-host multi-process              |
| `memory`   | In-process map              | Single process; no cross-instance lock |
| `frozen`   | Time-travel harness         | Tests — advance instead of waiting     |

**Consequence:** three pods with `postgres` still run each tick once (30s leader lease).
`memory` does not coordinate across processes.

## The Capabilities of Clock

<Cards>
  <Card
    title="Schedules"
    description="Cron helpers, every-intervals, per-tenant rows, leader locks, and catch-up one."
    href="/docs/elements/clock/schedules"
  />
  <Card
    title="Durable Sleep"
    description="Journaled pauses that resume across deploys — label + duration, durable Flows only."
    href="/docs/elements/clock/sleep"
  />
</Cards>

## Troubleshooting

<Accordions>

<Accordion title='TypeError: clock("name"): require cron or every'>
  `clock(name)` needs `{cron}` and/or `{every}`. Empty options throw at declaration, before `on()`.
</Accordion>

<Accordion title="tz is not a clock option">
  The field is `timezone` (IANA). Prefer `oke({ clock: { timezone } })` /
  `defineConfig({ clock: { timezone } })` so schedules can omit it.
  `{ tz: "Asia/Riyadh" }` is ignored.
</Accordion>

<Accordion title="Cron fired 24 times after overnight downtime">
  Catch-up is `"one"` — one fire per overdue clock, then `nextRunAt` advances. Missed slots show as
  `missedRuns`. A burst usually means several clocks or `clock.perTenant` rows, not a replay of
  hourly slots.
</Accordion>

<Accordion title="Two pods ran the same job">
  Leader election needs a shared Store (`drivers.clock` postgres, or `file` on one host). `memory`
  does not coordinate across processes. Check that instances share `DATABASE_URL`.
</Accordion>

<Accordion title="Sleep returns immediately / wrong wait">
  Missing `durable: true`, or `fx.clock.sleep("8h")` with one argument — `"8h"` is the **label**,
  duration is missing. Use `fx.clock.sleep("label", "8h")`.
</Accordion>

<Accordion title="ScheduleNotOverridableError from Console">
  Cause: `clock "{name}" is not overridable`. Add `overridable: true` and redeploy, or edit the
  declaration in source.
</Accordion>

<Accordion title="OKE1070 — flow name defined twice">
  Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings collide. Give at least one a
  distinct name. Clock allows several consumers on one schedule — each needs its own name.
</Accordion>

<Accordion title="OKE1072 — Clock flow unnamed">
  Cause: `A clock flow on "{trigger}" has no name.`
  Fix: pass an explicit name — `on(clockDecl, flow("notes.digest", { do }))`.
  Inline `clock.every` still needs `flow("…")` — [Inline or named export](#inline-or-named-export).
</Accordion>

</Accordions>

## Learn more

- [Schedules](/docs/elements/clock/schedules) — cron, every, per-tenant, DST, catch-up, overrides
- [Durable Sleep](/docs/elements/clock/sleep) — `fx.clock.sleep(label, duration)`
- [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
- [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` around sleeps
- [fx](/docs/reference/fx) — full `fx.clock` table
- [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070 · OKE1072

## Next

<Cards>
  <Card
    title="Schedules"
    description="Cron helpers, intervals, timezones, leader locks, and catch-up one."
    href="/docs/elements/clock/schedules"
  />
  <Card
    title="Gate"
    description="Policies and rate limits on HTTP triggers."
    href="/docs/elements/gate"
  />
  <Card
    title="Consumers"
    description="Signal workers, named Clock jobs, and SQL CDC."
    href="/docs/elements/flow/consumers"
  />
</Cards>
