# Schedule

A `Schedule<Output, Input, Error, Env>` is a stateful policy that, on each step,
decides whether to recur and after what delay. The type parameters describe what
flows through it:

- `Output` — the value the schedule emits each step (often a counter or a `Duration`).
- `Input` — the value fed *into* the schedule each step. For `Effect.retry` this
  is the error; for `Effect.repeat` it is the success value.
- `Error` / `Env` — anything the schedule itself may fail with or require (most
  schedules need neither).

You rarely build a schedule from scratch. You start from a constructor and layer
on combinators with `.pipe(...)`.

```ts
import { Duration, Effect, Schedule } from "effect"

// Start from constructors — each is a plain value you can name and reuse.
const fiveTimes = Schedule.recurs(5) // recur 5 times, no delay
const every30s = Schedule.spaced("30 seconds") // fixed 30s gap between runs
const backoff = Schedule.exponential("200 millis") // 200ms, 400ms, 800ms, ...

// Compose them. This policy retries with exponential backoff, caps the delay
// at 10 seconds, adds random jitter, and stops after at most 6 attempts.
const policy = Schedule.exponential("200 millis").pipe(
  Schedule.either(Schedule.spaced("10 seconds")), // delay = min(backoff, 10s)
  Schedule.jittered, // multiply each delay by a random factor
  Schedule.both(Schedule.recurs(6)), // stop once either side stops
  Schedule.tapOutput((delay: Duration.Duration) =>
    Effect.logDebug(`next attempt in ${Duration.toMillis(delay)}ms`)
  )
)
```

## Constructors

Every constructor below returns a `Schedule` whose `Output` is shown in the
comment. `Duration.Input` accepts strings like `"200 millis"`, `"30 seconds"`, a number
of milliseconds, a `bigint` of nanoseconds, a `Duration`, or a
`[seconds, nanos]` high-resolution tuple.

```ts
import { Schedule } from "effect"

Schedule.recurs(5) //          Output: number  — recur 5 more times (0,1,2,3,4)
Schedule.forever //            Output: number  — recur forever (0,1,2,...)
Schedule.spaced("1 second") // Output: number  — fixed gap measured from the end of each run
Schedule.fixed("1 second") //  Output: number  — fixed cadence measured from the start of each run
Schedule.exponential("100 millis") // Output: Duration — 100, 200, 400, 800, ...
Schedule.exponential("100 millis", 3) // grow by a custom factor (×3) instead of ×2
Schedule.fibonacci("100 millis") //   Output: Duration — 100, 100, 200, 300, 500, ...
Schedule.windowed("5 seconds") //     Output: number  — align recurrences to fixed wall-clock windows
Schedule.duration("250 millis") //    Output: Duration — recur once after a single fixed delay
```

`spaced` vs `fixed` is a common point of confusion. `spaced` waits the given
duration *after the previous run finishes*, so a slow effect pushes the next run
later. `fixed` recurs on a steady cadence regardless of how long each run takes —
if a run overruns the window, the next fires immediately.
**The initial run is free:** Schedules describe *additional* recurrences. When a schedule drives an effect
  via `Effect.repeat` or `Effect.retry`, the effect always runs once first, then
  the schedule decides whether to run it again. `Schedule.recurs(5)` therefore
  allows up to six total executions: one initial plus five recurrences.

## Combining schedules

`both` and `either` merge two schedules by combining their *continue* decisions
and combining their delays:

- `Schedule.both(a, b)` continues only while **both** continue, and uses the
  **maximum** of the two delays — use it to layer a stop condition (like
  `recurs`) onto a delay pattern.
- `Schedule.either(a, b)` continues while **either** continues, and uses the
  **minimum** of the two delays — use it as a fallback, or, as above, to cap a
  growing backoff against a constant ceiling.

```ts
import { Schedule } from "effect"

// Exponential backoff, but never more than 6 attempts.
const cappedAttempts = Schedule.both(
  Schedule.exponential("250 millis"),
  Schedule.recurs(6)
)

// Keep retrying until BOTH the spacing and the count schedules give up.
const keepTrying = Schedule.either(
  Schedule.spaced("2 seconds"),
  Schedule.recurs(3)
)
```

`Schedule.andThen(first, second)` runs `first` to completion, then switches to
`second` — handy for "retry fast a few times, then back off slowly":

```ts
import { Schedule } from "effect"

const fastThenSlow = Schedule.andThen(
  Schedule.exponential("100 millis").pipe(Schedule.take(3)),
  Schedule.spaced("5 seconds")
)
```

## Transforming delays and outputs

```ts
import { Duration, Effect, Schedule } from "effect"

const tuned = Schedule.exponential("100 millis").pipe(
  // Add extra fixed delay on top of whatever the schedule computed.
  Schedule.addDelay(() => Effect.succeed("50 millis")),
  // Replace the delay entirely. The callback returns an Effect<Duration.Input>,
  // so you can compute the new delay (here, capped at 5 seconds).
  Schedule.modifyDelay((_output, delay) =>
    Effect.succeed(Duration.min(delay, Duration.seconds(5)))
  ),
  // Apply random jitter so concurrent clients don't retry in lockstep.
  Schedule.jittered,
  // Keep only the first 8 recurrences.
  Schedule.take(8)
)
```

- `addDelay` adds to the computed delay; `modifyDelay` replaces it.
- `jittered` multiplies each delay by a random factor (between 0.8× and 1.2×) to avoid
  the *thundering herd* problem where many clients retry at the same instant.
- `take(n)` stops the schedule after `n` outputs.

## Stopping and inspecting

`Schedule.while` continues only while a predicate holds. The predicate receives
the full step `Metadata` — including the `input`, `output`, `attempt` count, and
`elapsed` time — and may return a `boolean` or an `Effect<boolean>`. To type the
`input` (for example, an error you want to inspect), declare it with
`setInputType`:

```ts
import { Schema, Schedule } from "effect"

class HttpError extends Schema.TaggedErrorClass<HttpError>()("HttpError", {
  message: Schema.String,
  status: Schema.Number,
  retryable: Schema.Boolean
}) {}

// Retry with backoff, but only while the failure is marked retryable, and only
// for the first 30 seconds of attempts.
const retryRetryable = Schedule.exponential("200 millis").pipe(
  Schedule.setInputType<HttpError>(),
  Schedule.while(({ input, elapsed }) => input.retryable && elapsed < 30_000)
)
```

For observability, `tapInput` and `tapOutput` run an effect on each step's input
or output without changing it — ideal for logging or metrics:

```ts
import { Duration, Effect, Schema, Schedule } from "effect"

class HttpError extends Schema.TaggedErrorClass<HttpError>()("HttpError", {
  message: Schema.String,
  status: Schema.Number
}) {}

const instrumented = Schedule.exponential("200 millis").pipe(
  Schedule.setInputType<HttpError>(),
  Schedule.tapInput((error) =>
    Effect.logDebug(`retrying after ${error.status}: ${error.message}`)
  ),
  Schedule.tapOutput((delay: Duration.Duration) =>
    Effect.logDebug(`next retry in ${Duration.toMillis(delay)}ms`)
  )
)
```

These schedules are just values — pass them to `Effect.retry` or `Effect.repeat`
as shown in [Repetition & Retry](https://effect.plants.sh/scheduling/repetition-and-retry/).

## Reference

The reference below covers every notable public export of the `Schedule` module.
A few examples drive a schedule manually with `Schedule.toStepWithSleep` so you
can see the emitted outputs; in practice you hand a schedule to
`Effect.retry`/`Effect.repeat` rather than stepping it yourself.

### Constructors

#### recurs

Recurs the given number of times with no delay, emitting a 0-based counter. With
`retry`/`repeat` this allows one initial run plus `times` recurrences.

```ts
import { Schedule } from "effect"

Schedule.recurs(3) // Output: number — emits 0, 1, 2 then stops (delay 0)
```

#### forever

Recurs forever with no delay, emitting an incrementing counter. It is exactly
`spaced(Duration.zero)`.

```ts
import { Schedule } from "effect"

Schedule.forever // Output: number — emits 0, 1, 2, 3, ... never stops
```

#### spaced

Recurs continuously, waiting `duration` *after the previous run completes*.
Emits a 0-based counter. A slow effect pushes the next run later.

```ts
import { Schedule } from "effect"

Schedule.spaced("1 second") // Output: number — wait 1s after each run; emits 0, 1, 2, ...
```

#### fixed

Recurs on a steady cadence of `interval`, measured from the start of each run.
If a run overruns the window the next fires immediately; missed windows are not
replayed. Emits a 0-based counter.

```ts
import { Schedule } from "effect"

Schedule.fixed("1 second") // Output: number — fire every 1s regardless of run duration
```

#### exponential

Recurs forever, growing the delay as `base * factor ** n` (`factor` defaults to
`2`). Emits the current delay as a `Duration`.

```ts
import { Effect, Schedule } from "effect"

const program = Effect.gen(function* () {
  const step = yield* Schedule.toStepWithSleep(
    Schedule.exponential("100 millis").pipe(Schedule.take(4))
  )
  yield* step(undefined) // sleeps 100 millis
  yield* step(undefined) // sleeps 200 millis
  yield* step(undefined) // sleeps 400 millis
  yield* step(undefined) // sleeps 800 millis
})
```

#### fibonacci

Recurs forever, increasing each delay by summing the previous two delays. Emits
the current delay as a `Duration`.

```ts
import { Schedule } from "effect"

Schedule.fibonacci("100 millis") // Output: Duration — 100, 100, 200, 300, 500, 800, ...
```

#### windowed

Divides the timeline into `interval`-long windows and sleeps until the next
window boundary on each recurrence (boundaries are relative to when the schedule
started). Emits a 0-based counter.

```ts
import { Schedule } from "effect"

Schedule.windowed("5 seconds") // Output: number — recurrences snap to 5s window boundaries
```

#### duration

Recurs exactly **once** after a single fixed delay, then stops. Emits the
configured `Duration`. Use `during` to keep recurring until a duration elapses.

```ts
import { Console, Effect, Schedule } from "effect"

// Runs the effect once, then again after 1 second, then stops.
Effect.repeat(Console.log("again"), Schedule.duration("1 second"))
```

#### during

Always recurs, but only while the **total elapsed** time is within `duration`.
Emits the elapsed time as a `Duration`. Use `duration` for a single delayed
recurrence.

```ts
import { Schedule } from "effect"

Schedule.during("5 seconds") // Output: Duration — keep recurring for up to 5 seconds total
```

#### elapsed

Always recurs with **zero delay**, emitting the total elapsed time since the
first step as a `Duration`. Combine it with another schedule (via `both`) to
attach elapsed time to that schedule's pacing.

```ts
import { Schedule } from "effect"

// Pair a 1s cadence with the elapsed-time output.
const withElapsed = Schedule.spaced("1 second").pipe(Schedule.both(Schedule.elapsed))
// Output: [number, Duration] — e.g. [0, 0 millis], [1, 1000 millis], [2, 2000 millis]
```

#### cron

Recurs on a cron `expression` (or a parsed `Cron`), optionally in a `timezone`.
Emits the `Duration` until the next occurrence. May fail with `CronParseError`
when given an invalid expression. See [Cron](https://effect.plants.sh/scheduling/cron/) for the cron
syntax.

```ts
import { Schedule } from "effect"

Schedule.cron("30 2 * * *") // every day at 02:30 — Output: Duration (time until next run)
Schedule.cron("0 9 * * 1", "America/New_York") // Mondays 09:00 in a timezone
```

#### identity

Always recurs with zero delay, passing each **input straight through as the
output**. Useful as a base for combinators that inspect inputs.

```ts
import { Schedule } from "effect"

Schedule.identity<string>() // Schedule<string, string> — echoes each input as its output
```

#### unfold

Builds a custom schedule by unfolding state: it emits the current state and
computes the next state via an effectful `next` function. Recurs with zero delay
until limited (e.g. with `take`).

```ts
import { Effect, Schedule } from "effect"

Schedule.unfold(0, (n) => Effect.succeed(n + 1)) // Output: number — emits 0, 1, 2, 3, ...
```

#### fromStep / fromStepWithMetadata

Low-level escape hatch: build a schedule directly from a step function. The step
returns `[output, delay]` to continue, or `Cause.done(finalOutput)` to stop.
`fromStepWithMetadata` hands the step the full `InputMetadata` (attempt, elapsed,
input, ...) instead of just `now` and `input`. Prefer the higher-level
constructors above unless you need fully custom control flow.

```ts
import { Cause, Duration, Effect, Schedule } from "effect"

const threeQuick = Schedule.fromStep(
  Effect.sync(() => {
    let count = 0
    return (_now: number, _input: unknown) =>
      count >= 3
        ? Cause.done(count) // stop after 3 steps
        : Effect.succeed([count++, Duration.millis(100)] as [number, Duration.Duration])
  })
)
```

#### toStep / toStepWithMetadata / toStepWithSleep

Low-level escape hatch in the other direction: turn a schedule into its step
function so you can drive it manually. `toStep` returns a `(now, input) =>` step
where you supply the timestamp and handle the returned delay yourself;
`toStepWithMetadata` reads the `Clock`, sleeps for each delay, and yields full
`Metadata`; `toStepWithSleep` does the same but yields only the output.

```ts
import { Effect, Schedule } from "effect"

const program = Effect.gen(function* () {
  // Automatically sleeps for each computed delay; yields the output.
  const step = yield* Schedule.toStepWithSleep(
    Schedule.spaced("1 second").pipe(Schedule.take(2))
  )
  const a = yield* step("first") // => 0   (after sleeping 1s)
  const b = yield* step("second") // => 1  (after sleeping 1s)
})
```

### Combining schedules

#### both

Continues only while **both** schedules continue, using the **maximum** of the
two delays, and emits a tuple `[leftOutput, rightOutput]`.

```ts
import { Schedule } from "effect"

Schedule.both(Schedule.exponential("250 millis"), Schedule.recurs(6))
// Output: [Duration, number] — exponential backoff, capped at 6 recurrences
```

#### either

Continues while **either** schedule continues, using the **minimum** of the two
delays, and emits a tuple `[leftOutput, rightOutput]`.

```ts
import { Schedule } from "effect"

Schedule.either(Schedule.exponential("200 millis"), Schedule.spaced("10 seconds"))
// Output: [Duration, number] — growing backoff, but each delay capped at 10s
```

#### bothWith / eitherWith

Like `both`/`either`, but combine the two outputs with a custom function instead
of producing a tuple.

```ts
import { Schedule } from "effect"

Schedule.bothWith(
  Schedule.recurs(3),
  Schedule.recurs(5),
  (left, right) => left + right
) // Output: number — sum of the two counters; stops when either side stops
```

#### bothLeft / bothRight

`both`, keeping only the left (`self`) output or only the right (`other`) output.

```ts
import { Schedule } from "effect"

// Pace with exponential backoff but limit attempts; keep the backoff Duration.
Schedule.bothLeft(Schedule.exponential("100 millis"), Schedule.recurs(5))
// Output: Duration

Schedule.bothRight(Schedule.recurs(5), Schedule.exponential("100 millis"))
// Output: Duration — right output retained instead
```

#### eitherLeft / eitherRight

`either`, keeping only the left (`self`) output or only the right (`other`)
output.

```ts
import { Schedule } from "effect"

Schedule.eitherLeft(Schedule.exponential("100 millis"), Schedule.spaced("1 second"))
// Output: Duration — continues while either side does; keeps the left output

Schedule.eitherRight(Schedule.exponential("100 millis"), Schedule.spaced("1 second"))
// Output: number — keeps the right output
```

#### andThen

Runs `self` to completion, then switches to `other`. The output is the union of
both outputs (`Output | Output2`); inputs are intersected.

```ts
import { Schedule } from "effect"

Schedule.andThen(
  Schedule.exponential("100 millis").pipe(Schedule.take(3)), // Output: Duration
  Schedule.spaced("5 seconds") // Output: number
) // Output: Duration | number — fast backoff first, then steady spacing
```

#### andThenResult

Like `andThen`, but tags which phase produced each output via a `Result`:
outputs from `self` are emitted as `Result.fail`, and outputs from `other` as
`Result.succeed`. The output type is `Result<Output2, Output>`.

```ts
import { Result, Schedule } from "effect"

const phased = Schedule.andThenResult(
  Schedule.recurs(2), // phase 1 — emitted as Result.fail(number)
  Schedule.spaced("1 second") // phase 2 — emitted as Result.succeed(number)
)
// match with: Result.match(output, { onFailure, onSuccess })
```

### Transforming delays & outputs

#### addDelay

Adds the `Duration` computed by an effectful function to the schedule's existing
delay (it is summed, not replaced).

```ts
import { Effect, Schedule } from "effect"

Schedule.exponential("100 millis").pipe(
  Schedule.addDelay(() => Effect.succeed("50 millis"))
) // each delay becomes the exponential value + 50ms
```

#### modifyDelay

Replaces the delay with the `Duration` computed by an effectful function, which
receives both the output and the current delay.

```ts
import { Duration, Effect, Schedule } from "effect"

Schedule.exponential("100 millis").pipe(
  Schedule.modifyDelay((_output, delay) =>
    Effect.succeed(Duration.min(delay, Duration.seconds(5))) // cap at 5s
  )
)
```

#### jittered

Scales each delay by a random factor between `0.8×` and `1.2×`, preserving the
schedule's outputs and stop behavior. Spreads out concurrent retries.

```ts
import { Schedule } from "effect"

Schedule.exponential("100 millis").pipe(Schedule.jittered)
// e.g. ~80–120ms, ~160–240ms, ~320–480ms, ...
```

#### map

Transforms each output value. The function may return a plain value or an
`Effect`.

```ts
import { Effect, Schedule } from "effect"

Schedule.recurs(3).pipe(
  Schedule.map((count) => Effect.succeed(`run #${count + 1}`))
) // Output: string — "run #1", "run #2", "run #3"
```

#### delays

Returns a schedule that emits the **delay** of each step (a `Duration`) as its
output, preserving the original stopping behavior.

```ts
import { Schedule } from "effect"

Schedule.delays(Schedule.exponential("100 millis").pipe(Schedule.take(3)))
// Output: Duration — 100 millis, 200 millis, 400 millis
```

#### passthrough

Returns a schedule that emits each **input** (instead of the original output),
keeping the original delays and stop behavior.

```ts
import { Schedule } from "effect"

Schedule.passthrough(Schedule.exponential("100 millis").pipe(Schedule.take(3)))
// Output: the input value seen at each step (Input type), with exponential delays
```

#### reduce

Accumulates outputs into a running state with an effectful `combine` function,
emitting the running state each step.

```ts
import { Effect, Schedule } from "effect"

Schedule.reduce(
  Schedule.recurs(5),
  () => 0,
  (sum, count) => Effect.succeed(sum + count)
) // Output: number — running sum of the recurs counter: 0, 1, 3, 6, 10
```

#### collectInputs

Follows `self` but emits all **inputs** seen so far as an array. Stops when
`self` stops.

```ts
import { Schedule } from "effect"

Schedule.collectInputs(Schedule.spaced("100 millis"))
// Output: Array<Input> — grows by one input each step
```

#### collectOutputs

Follows `self` but emits all **outputs** seen so far as an array. Stops when
`self` stops.

```ts
import { Schedule } from "effect"

Schedule.collectOutputs(Schedule.recurs(4))
// Output: Array<number> — [0], [0,1], [0,1,2], [0,1,2,3]
```

#### collectWhile

Collects outputs into an array while the `Metadata` predicate holds (and stops
otherwise). Combines `while` and `collectOutputs`.

```ts
import { Effect, Schedule } from "effect"

Schedule.collectWhile(
  Schedule.exponential("100 millis"),
  (meta) => Effect.succeed(meta.attempt <= 5 && meta.elapsed < 2000)
) // Output: Array<Duration> — collected delays while within 5 attempts / 2s
```

### Stopping & filtering

#### while

Continues only while the `Metadata` predicate returns `true` (it may return a
`boolean` or an `Effect<boolean>`); stops on the first `false`. The predicate
sees `input`, `output`, `attempt`, `elapsed`, `duration`, and the timing fields.

```ts
import { Schedule } from "effect"

Schedule.spaced("1 second").pipe(
  Schedule.while(({ attempt, elapsed }) => attempt <= 10 && elapsed < 30_000)
) // continue while under 10 attempts AND under 30s elapsed
```

#### take

Stops the schedule after `n` outputs, preserving the original outputs and
delays. Use `recurs` instead when you only need a bare counter with no delay.

```ts
import { Schedule } from "effect"

Schedule.exponential("100 millis").pipe(Schedule.take(3))
// at most 3 outputs: 100ms, 200ms, 400ms — then stops
```

Other constructors also embed stop conditions: `recurs` stops after a count,
`during` stops after a total elapsed duration, `duration` stops after one
recurrence, and `both`/`either` stop when their combined continue rule says so.

### Observability

#### tap

Runs an effect on the full `Metadata` of each step without changing the
schedule's input or output.

```ts
import { Console, Schedule } from "effect"

Schedule.exponential("100 millis").pipe(
  Schedule.tap((meta) =>
    Console.log(`attempt ${meta.attempt}, next delay ${meta.duration}`)
  )
)
```

#### tapInput

Runs an effect on each step's **input** without changing it — ideal for logging
the error being retried.

```ts
import { Console, Schedule } from "effect"

Schedule.exponential("100 millis").pipe(
  Schedule.tapInput((error: unknown) => Console.log(`retrying after: ${String(error)}`))
)
```

#### tapOutput

Runs an effect on each step's **output** without changing it.

```ts
import { Console, Schedule } from "effect"

Schedule.exponential("100 millis").pipe(
  Schedule.tapOutput((delay) => Console.log(`next delay: ${delay}`))
)
```

### Typing & introspection

#### setInputType

Declares the schedule's `Input` type (without changing runtime behavior) so that
`while`/`tap` predicates can infer the input — typically the error type for a
retry policy.

```ts
import { Schema, Schedule } from "effect"

class HttpError extends Schema.TaggedErrorClass<HttpError>()("HttpError", {
  retryable: Schema.Boolean
}) {}

Schedule.recurs(5).pipe(
  Schedule.setInputType<HttpError>(),
  Schedule.while(({ input }) => input.retryable) // `input` is HttpError
)
```

#### satisfiesInputType / satisfiesOutputType / satisfiesErrorType / satisfiesServicesType

Compile-time assertions that a schedule's `Input`, `Output`, `Error`, or `Env`
type parameter extends a given type. They return the schedule unchanged and emit
a type error otherwise — useful as a guard in library code.

```ts
import { Schedule } from "effect"

declare const policy: Schedule.Schedule<number, string>

const checkInput = Schedule.satisfiesInputType<string>()
const checked = checkInput(policy) // OK — Input is string
// checkInput(Schedule.recurs(3))  // type error — Input is not string
```

#### isSchedule

Type guard that returns `true` if a value is a `Schedule`.

```ts
import { Schedule } from "effect"

Schedule.isSchedule(Schedule.recurs(3)) // => true
Schedule.isSchedule({ foo: "bar" }) // => false
```

#### CurrentMetadata, Metadata & InputMetadata

`Schedule.CurrentMetadata` is a `Context.Reference` holding the current step's
`Metadata`, provided to effects run between schedule steps by `repeat`, `retry`,
and the streaming scheduling APIs. The metadata shapes are:

`InputMetadata<Input>`:

- `input` — the value fed into this step.
- `attempt` — 1-based step count.
- `start` — timestamp (ms) of the first step.
- `now` — timestamp (ms) of this step.
- `elapsed` — ms elapsed since the first step.
- `elapsedSincePrevious` — ms elapsed since the previous step.

`Metadata<Output, Input>` extends `InputMetadata` with:

- `output` — the value emitted this step.
- `duration` — the computed delay before the next step.

These are the fields available to `while`, `collectWhile`, and `tap` predicates.

```ts
import { Effect, Schedule } from "effect"

// Read the current step metadata inside a repeated effect.
const program = Effect.gen(function* () {
  const meta = yield* Schedule.CurrentMetadata
  yield* Effect.log(`attempt ${meta.attempt}, elapsed ${meta.elapsed}ms`)
}).pipe(Effect.repeat(Schedule.recurs(3)))
```

---

For driving these policies with effects, see
[Repetition & Retry](https://effect.plants.sh/scheduling/repetition-and-retry/). For calendar-driven
recurrence and the cron expression syntax, see [Cron](https://effect.plants.sh/scheduling/cron/).