Durable sleep suspends a Flow until a future instant without holding a worker thread.
Call `await fx.clock.sleep(label, duration)` inside a `durable: true` Flow — the journal
stores `wakeAt`, releases the run lease, and any instance may resume when due.

For developers building trial reminders, delayed digests, and multi-day provisions on okengine.

<Callout title="The one rule">
  Signature is `fx.clock.sleep(label, duration)` — two arguments. A single duration string is
  treated as the **label**, not the wait. Sleep only parks when the Flow is durable.
</Callout>

<ClockSleep />

## Smallest Example

<Steps>

<Step>
### Sleep inside a durable Flow

```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 });
      });
    },
  }),
);
```

</Step>

<Step>
### Call the endpoint

```bash
curl -X POST http://localhost:6530/trials/start \
  -H "content-type: application/json" \
  -d '{"email":"ada@example.com"}'
```

Response: **`204 No Content`** (empty body). The HTTP client is done — wake continues on a
worker three days later. Wrap side effects in `fx.step` so replay does not re-send mail.

</Step>

</Steps>

<Callout title="Detailed section">
  If you only need a pause, the example above is enough. Below: park physics, wake-early,
  nested-call limits, non-durable no-ops, and how sleep fits with steps / compensation.
</Callout>

## Progressive Patterns

From a labelled pause to HTTP parking, nested-call limits, and non-durable behavior:

<Tabs items={["Label + duration", "HTTP park", "After a step", "Non-durable"]}>

<Tab value="Label + duration">

Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. Labels show up in the journal
and Console:

```typescript
await fx.clock.sleep("morning-window", "8h");
await fx.clock.sleep("verify-window", "2m");
```

**Wrong:** `fx.clock.sleep("8h")` — `"8h"` is the label; duration is missing → immediate
resolve / unexpected behavior.

</Tab>

<Tab value="HTTP park">

Sleep on an HTTP-triggered durable Flow returns success with an empty body (`204`). Return
a body **before** sleep if the caller must see an id:

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

export const create = on(
  http.post({
    in: z.object({ email: z.string().email() }),
  }),
  flow({
    durable: true,
    do: async ({ email }, fx) => {
      await fx.step("create", async () => {
        await fx.call(createWorkspace, { email });
      });
      await fx.emit(workspaceProvisioned, { email });
      await fx.clock.sleep("welcome-delay", "8h");
      await fx.step("notify", async () => {
        await fx.send(welcomeEmail, { to: email });
      });
    },
  }),
);
```

After sleep the HTTP client is already done (`204`). Need a JSON body? Return from a
Flow that does **not** sleep, and park on a Signal consumer instead. See
[Workflows · Durable Sleep](/docs/elements/flow/workflows#durable-sleep).

</Tab>

<Tab value="After a step">

Checkpoint work **before** the pause so a crash mid-provision does not re-run after wake:

```typescript
await fx.step("provision", async () => {
  await fx.call(createWorkspace, { email });
});
await fx.clock.sleep("morning-window", "8h");
await fx.step("notify", async () => {
  await fx.send(welcomeEmail, { to: email });
});
```

Do not hold `fx.using` resources across sleep — acquire again after wake.

</Tab>

<Tab value="Non-durable">

Without a journal, `fx.clock.sleep` resolves **immediately**. There is no thread sleep and
no `setTimeout`. Use durable Flows for real delays; use frozen clock + advance in tests.

</Tab>

</Tabs>

## Sleep Reference

| Call / option                     | Type        | Default | Meaning                                            |
| --------------------------------- | ----------- | ------- | -------------------------------------------------- |
| `durable: true`                   | Flow option | `false` | Journals steps and sleep; required to park         |
| `fx.clock.sleep(label, duration)` | park        | —       | Writes `wakeAt`, status `sleeping`, releases lease |
| `label`                           | `string`    | —       | Journal / Console step name                        |
| `duration`                        | `string`    | —       | `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`     |

Outcomes of a durable attempt that hits sleep: status `"sleeping"` with `wakeAt` and
`label`. After wake, prior `fx.step` bodies replay from the journal and do not re-execute.

### Duration units

Same grammar as `fx.clock.ago` / `fromNow` / `clock.every`:

| Unit | Example   | Milliseconds |
| ---- | --------- | ------------ |
| `ms` | `"200ms"` | 200          |
| `s`  | `"30s"`   | 30_000       |
| `m`  | `"2m"`    | 120_000      |
| `h`  | `"1h"`    | 3_600_000    |
| `d`  | `"7d"`    | 604_800_000  |

Integer + unit only — no weeks. `"d"` is exactly 86_400_000 ms, not a calendar day across
DST. Unknown strings parse as `0` ms (wake immediately).

## Park Physics

<Callout title="Detailed section">
  If you only need `await fx.clock.sleep("label", "8h")`, jump past this section. The park writes a
  journal entry, sets status `sleeping`, and **releases the run lease** so a parked flow does not
  hold a 30s lock for days.
</Callout>

```text
fx.clock.sleep(label, duration)
  → journal entry { kind: "sleep", label, duration, wakeAt }
  → status = sleeping, lease released
  → HTTP / caller sees success (often 204)
  → later: claim due sleep → resume → replay steps → continue after sleep
```

Any instance may claim the row when `wakeAt` is due (shared journal + lease). Resume replays
completed `fx.step` values from the journal, then continues past the sleep entry.

| Phase          | Status                 | Lease        | What happens                                  |
| -------------- | ---------------------- | ------------ | --------------------------------------------- |
| Before sleep   | `running`              | Held         | Steps append; side effects execute once       |
| At sleep       | `sleeping`             | **Released** | `wakeAt` stored; worker free                  |
| After `wakeAt` | claim → `running`      | Re-acquired  | Replay steps; sleep entry is a no-op past due |
| Done           | `completed` / `failed` | Released     | Terminal — resume refused                     |

**Consequence:** multi-day sleeps are safe across deploys — the journal row is the schedule,
not an in-memory timer.

## With Steps

Each verb of durable work binds the same way — checkpoint, park, then more checkpoints:

<Tabs items={["Checkpoint sandwich", "HTTP body + sleep", "Signal consumer"]}>

<Tab value="Checkpoint sandwich">

Wrap every side effect in a uniquely named `fx.step`. Sleep sits **between** steps:

```typescript title="src/flows/onboarding/welcome.ts"
import { on, flow } from "okengine";
import { userSignedUp } from "@/signals";
import { welcomeEmail } from "@/channels/welcome";

export const sendWelcome = on(
  userSignedUp,
  flow("onboarding.welcome", {
    durable: true,
    do: async ({ email }, fx) => {
      await fx.step("provision", async () => {
        await fx.call(createWorkspace, { email });
      });
      await fx.clock.sleep("morning-window", "8h");
      await fx.step("notify", async () => {
        await fx.send(welcomeEmail, { to: email });
      });
    },
  }),
);
```

**Consequence:** after wake, `provision` returns the journaled value and never re-runs;
`notify` runs for the first time.

</Tab>

<Tab value="HTTP body + sleep">

Parking an HTTP Flow always answers **`204`** (suspend returns `undefined`). When the
caller must see an id, **do not sleep on that route** — return the body, emit, and park
on a consumer:

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

export const create = on(
  http.post({
    in: z.object({ email: z.string().email() }),
    out: z.object({ trialId: z.string() }),
  }),
  flow({
    durable: true,
    do: async ({ email }, fx) => {
      const trialId = await fx.step("create", async () => {
        return await fx.call(createTrial, { email });
      });
      await fx.emit(trialStarted, { trialId, email });
      return { trialId };
    },
  }),
);
```

</Tab>

<Tab value="Signal consumer">

The consumer owns the multi-day wait (paired with the HTTP Flow above):

```typescript title="src/flows/trials/remind.ts"
import { on, flow } from "okengine";
import { trialStarted } from "@/signals";
import { trialExpiringEmail } from "@/channels/trials";

export const remind = on(
  trialStarted,
  flow("trials.remind", {
    durable: true,
    do: async ({ trialId, email }, fx) => {
      await fx.clock.sleep("expiry-window", "3d");
      await fx.step("notify", async () => {
        await fx.send(trialExpiringEmail, { to: email, trialId });
      });
    },
  }),
);
```

</Tab>

</Tabs>

## Nested Calls

<Callout title="Detailed section">
  If you sleep only on the root durable Flow, skip this section. `fx.call` waits for the callee to
  return — a sleeping callee does not keep the caller parked.
</Callout>

`fx.call` of a durable sleeper: the caller receives `undefined` and continues; the child
wakes later as its own run.

| Pattern                                       | Safe? | Why                                        |
| --------------------------------------------- | ----- | ------------------------------------------ |
| Sleep on the root durable Flow                | Yes   | One journal row, clear park                |
| `fx.emit` → durable consumer that sleeps      | Yes   | Separate run owns the wait                 |
| `fx.call(sleeperFlow, …)` then use the result | No    | Caller gets `undefined`; child parks alone |

**Consequence:** sleep on the **root** durable Flow, or split with `fx.emit` to a consumer —
do not nest sleep behind `fx.call`.

## Wake Early

Operators can advance `wakeAt` to **now** from Console (waiting-on / wake-early). With a
flow resolver, the run resumes immediately; without it, only the wake time moves forward.

| Outcome                      | Meaning                                                          |
| ---------------------------- | ---------------------------------------------------------------- |
| Resumed                      | `wakeAt` set to now and `runDurable` continued past the sleep    |
| Wake time only               | `wakeAt` advanced; scheduler / next claim picks it up            |
| `ClockResourceNotFoundError` | `run "{id}" not found` — missing id, or status is not `sleeping` |

Missing or non-sleeping run id → `ClockResourceNotFoundError` — `run "{id}" not found`.

## Non-durable & Tests

Without a journal session, `fx.clock.sleep` resolves immediately (tests and sync Flows).
There is no thread sleep and no `setTimeout`.

For deterministic waits in tests, use a frozen clock and advance:

```typescript
import { createTimeTravel } from "okengine";

const t = createTimeTravel(0);
// Park a durable run with now: () => t.now()
t.advance("7d");
// Resume — sleep entry is past due and continues
```

Pin journal for the env you need:

| Driver     | Default env    | Best for                                             |
| ---------- | -------------- | ---------------------------------------------------- |
| `postgres` | `dev`, `prod`  | Shared durable runs across replicas (`DATABASE_URL`) |
| `memory`   | `test`         | Process-local; lost on exit                          |
| `file`     | pin explicitly | One machine — `.oke/journal.json`                    |

Unknown ids throw `oke boot: unknown journal driver "…" (expected memory · file · postgres)`.
Postgres without a URL throws `oke boot: journal driver "postgres" needs DATABASE_URL`.

Full lease / orphan / status tables live on [Workflows · Journal](/docs/elements/flow/workflows#journal).

## Troubleshooting

<Accordions>

<Accordion title="Sleep returns immediately / work runs twice after wait">
  Missing `durable: true`, or `fx.clock.sleep("8h")` with one argument. Use `fx.clock.sleep("label",
  "8h")`. Non-durable sleep is a no-op.
</Accordion>

<Accordion title="HTTP 204 with empty body after POST">
  The Flow parked on `fx.clock.sleep` — success, not a missing handler. The original client is done;
  wake continues on a worker. Return a body before sleep if needed, or emit to a Signal consumer for
  the long wait.
</Accordion>

<Accordion title="Mail / charge ran again after resume">
  The side effect was outside `fx.step`. Wrap provider calls in uniquely named steps so replay
  returns the journaled value.
</Accordion>

<Accordion title="Connection held across sleep">
  `fx.using` is same-attempt cleanup — do not hold handles across `fx.clock.sleep`. Acquire again
  after wake.
</Accordion>

<Accordion title="ClockResourceNotFoundError on wake">
  Cause: `run "{id}" not found`. The wake-early target is missing or not `sleeping` — check the run
  id from Console / durable result.
</Accordion>

<Accordion title="fx.call of a durable sleeper returned undefined">
  The callee parked. Sleep on the root Flow, or emit to a durable consumer instead of calling a
  sleeper inline.
</Accordion>

<Accordion title="Unknown duration wakes immediately">
  Strings that do not match `integer + ms|s|m|h|d` parse as `0` ms. Use `"8h"`, not `"8 hours"` or
  `"1w"`.
</Accordion>

<Accordion title='journal: run "…" is leased by another instance'>
  Resume lost the lease race (`JournalLeaseBusy`). The other holder continues; this instance skips.
  Shared `postgres` journal + default **30s** lease — same physics as Signal claims.
</Accordion>

<Accordion title="GET /_/ready is 503 reason orphan_scan">
  Boot is still reclaiming `running` / `sleeping` / `compensating` rows with no live lease. Wait for
  the orphan scan; future sleeps stay scheduled until `wakeAt`.
</Accordion>

</Accordions>

## Learn more

- [Workflows](/docs/elements/flow/workflows) — `durable`, `fx.step`, compensate, full sleep section
- [HTTP](/docs/elements/flow/http) — request envelope; `204` from park / `undefined`
- [Clock overview](/docs/elements/clock) — schedules and `fx.clock` helpers
- [Schedules](/docs/elements/clock/schedules) — cron / every (calendar time, not durable pause)
- [fx](/docs/reference/fx) — `fx.clock.sleep`, `fx.step`, `fx.using`
- [Configuration](/docs/reference/configuration) — `drivers.journal`
- [Errors](/docs/reference/errors) — `ClockResourceNotFoundError`

## Next

<Cards>
  <Card
    title="Durable Workflows"
    description="Step journaling, undo, and multi-day runs."
    href="/docs/elements/flow/workflows"
  />
  <Card
    title="Schedules"
    description="Cron helpers, intervals, and IANA timezones."
    href="/docs/elements/clock/schedules"
  />
  <Card
    title="Clock Overview"
    description="Return to the Clock element overview."
    href="/docs/elements/clock"
  />
</Cards>
