Skip to content

Schedule.ts

Describes policies for retrying, repeating, and pacing Effect programs.

A Schedule<Output, Input, Error, Env> is stepped with an input value. Each step either stops or produces an output together with the delay before the next step. Schedules are used by retry, repeat, stream, and channel APIs to decide when work should continue, how long to wait, and when to stop.

Since v2.0.0



Returns a new Schedule that follows self and outputs the inputs seen so far as an array.

Details

This does not make the schedule run forever. The collected schedule stops when self stops and fails when self fails.

Example (Collecting schedule inputs)

import { Console, Effect, Schedule } from "effect"
// Collect all inputs passed to the schedule
const inputCollector = Schedule.collectInputs(Schedule.spaced("100 millis"))
const program = Effect.gen(function* () {
let counter = 0
yield* Effect.repeat(
Effect.gen(function* () {
counter++
yield* Console.log(`Iteration ${counter}`)
return `result-${counter}`
}),
inputCollector.pipe(Schedule.take(4))
)
})

Signature

declare const collectInputs: <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Array<Input>, Input, Error, Env>

Source

Since v4.0.0

Returns a new Schedule that follows self and outputs the schedule outputs seen so far as an array.

Details

This does not make the schedule run forever. The collected schedule stops when self stops and fails when self fails.

Example (Collecting schedule outputs)

import { Console, Effect, Schedule } from "effect"
// Collect all outputs from the schedule
const outputCollector = Schedule.collectOutputs(Schedule.recurs(4))
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-result"
}),
outputCollector.pipe(Schedule.take(4))
)
})

Signature

declare const collectOutputs: <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Array<Output>, Input, Error, Env>

Source

Since v4.0.0

Returns a new Schedule that recurs as long as the specified predicate returns true, collecting all outputs of the schedule into an array.

Example (Collecting outputs while a condition holds)

import { Console, Effect, Schedule } from "effect"
// Collect outputs while condition is met
const collectWhileSmall = Schedule.collectWhile(Schedule.exponential("100 millis"), (metadata) =>
Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
)
const conditionalProgram = Effect.gen(function* () {
let attempt = 0
const attempts = yield* Effect.repeat(
Effect.gen(function* () {
attempt++
yield* Console.log(`Retry attempt ${attempt}`)
return `attempt-${attempt}`
}),
collectWhileSmall
)
yield* Console.log(`Collected attempts: [${attempts.join(", ")}]`)
})
// Collect with effectful predicate
const collectWithCheck = Schedule.collectWhile(Schedule.fixed("1 second"), (metadata) =>
Effect.gen(function* () {
const shouldContinue = metadata.attempt < 5
yield* Console.log(`Check ${metadata.attempt}: continue = ${shouldContinue}`)
return shouldContinue
})
)
const effectfulProgram = Effect.gen(function* () {
const results = yield* Effect.repeat(Effect.succeed("checked"), collectWithCheck)
yield* Console.log(`Final collection: ${results.length} items`)
})
// Collect samples with condition
const samples = [12, 18, 24, 30, 36]
const collectSamples = Schedule.collectWhile(Schedule.spaced("200 millis"), (metadata) =>
Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
)
const samplingProgram = Effect.gen(function* () {
let index = 0
const collected = yield* Effect.repeat(
Effect.gen(function* () {
const sample = samples[index++]
yield* Console.log(`Sample: ${sample}`)
return sample
}),
collectSamples
)
const average = collected.reduce((sum, s) => sum + s, 0) / collected.length
yield* Console.log(`Collected ${collected.length} samples, average: ${average.toFixed(1)}`)
})

Signature

declare const collectWhile: {
<Input, Output, Error2 = never, Env2 = never>(
predicate: (metadata: Metadata<Output, Input>) => boolean | Effect<boolean, Error2, Env2>
): <Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Array<Output>, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
predicate: (metadata: Metadata<Output, Input>) => boolean | Effect<boolean, Error2, Env2>
): Schedule<Array<Output>, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if both of the two schedules want to recur, using the maximum of the two durations between recurrences and outputting a tuple of the outputs of both schedules.

When to use

Use when the combined schedule should continue only while both schedules still recur.

Example (Combining time and attempt limits)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Both schedules must want to continue for the combined schedule to continue
const timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 times
const attemptLimit = Schedule.recurs(3) // max 3 attempts
// Continues only while BOTH schedules want to continue (intersection/AND logic)
const bothSchedule = Schedule.both(timeLimit, attemptLimit)
// Outputs: [time_result, attempt_count] tuple
const program = Effect.gen(function* () {
const results = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task completed"
}),
bothSchedule.pipe(
Schedule.tapOutput(([timeResult, attemptResult]) =>
Console.log(`Time: ${timeResult}, Attempts: ${attemptResult}`)
)
)
)
yield* Console.log("Completed all executions")
})
// Both with different delay strategies - uses maximum delay
const fastSchedule = Schedule.fixed("500 millis").pipe(Schedule.take(4))
const slowSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(6))
// Will use the slower (maximum) delay and stop when first schedule exhausts
const conservativeSchedule = Schedule.both(fastSchedule, slowSchedule)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Retry attempt ${attempt}`)
if (attempt < 3) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
conservativeSchedule
)
yield* Console.log(`Final result: ${result}`)
})
// Both provides intersection semantics (AND logic)
// Compare with either which provides union semantics (OR logic)

See

  • either for continuing while either schedule still recurs

Signature

declare const both: {
<Output2, Input2, Error2, Env2, Output>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if both of the two schedules want to recur, using the maximum of the two durations between recurrences and outputting the result of the left schedule (i.e. self).

When to use

Use when two schedules must both allow recurrence and only the left schedule’s output is needed.

Example (Combining schedules and keeping the left output)

import { Console, Effect, Schedule } from "effect"
// Combine two schedules, keeping left output
const leftSchedule = Schedule.exponential("100 millis").pipe(Schedule.map(() => Effect.succeed("left-result")))
const rightSchedule = Schedule.spaced("50 millis")
const combined = Schedule.bothLeft(leftSchedule, rightSchedule)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-done"
}),
combined.pipe(Schedule.take(3))
)
})

Signature

declare const bothLeft: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if both of the two schedules want to recur, using the maximum of the two durations between recurrences and outputting the result of the right schedule (i.e. other).

When to use

Use when two schedules must both allow recurrence and only the right schedule’s output is needed.

Example (Combining schedules and keeping the right output)

import { Console, Effect, Schedule } from "effect"
// Combine two schedules, keeping right output
const leftSchedule = Schedule.exponential("100 millis").pipe(Schedule.map(() => Effect.succeed("left-result")))
const rightSchedule = Schedule.spaced("50 millis").pipe(Schedule.map(() => Effect.succeed("right-result")))
const combined = Schedule.bothRight(leftSchedule, rightSchedule)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-done"
}),
combined.pipe(Schedule.take(3))
)
})

Signature

declare const bothRight: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if both of the two schedules want to recur, using the maximum of the two durations between recurrences and outputting the result of the combination of both schedule outputs using the specified combine function.

When to use

Use when two schedules must both allow recurrence and their outputs should be combined into a custom value.

Example (Combining schedule outputs)

import { Console, Effect, Schedule } from "effect"
// Combine two schedules with custom output combination
const leftSchedule = Schedule.exponential("100 millis").pipe(Schedule.map(() => Effect.succeed("left")))
const rightSchedule = Schedule.spaced("50 millis").pipe(Schedule.map(() => Effect.succeed("right")))
const combined = Schedule.bothWith(leftSchedule, rightSchedule, (left, right) => `${left}-${right}`)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-result"
}),
combined.pipe(Schedule.take(3))
)
})

Signature

declare const bothWith: {
<Output2, Input2, Error2, Env2, Output, Output3>(
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if either of the two schedules wants to recur, using the minimum of the two durations between recurrences and outputting a tuple of the outputs of both schedules.

When to use

Use when the combined schedule should continue while at least one schedule still recurs.

Example (Combining schedules with either semantics)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Either continues as long as at least one schedule wants to continue
const timeBasedSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(3))
const countBasedSchedule = Schedule.recurs(5)
// Continues until both schedules are exhausted (either still wants to recur)
const eitherSchedule = Schedule.either(timeBasedSchedule, countBasedSchedule)
// Outputs: [time_result, count_result] tuple
const program = Effect.gen(function* () {
const results = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task completed"
}),
eitherSchedule.pipe(
Schedule.tapOutput(([timeResult, countResult]) => Console.log(`Time: ${timeResult}, Count: ${countResult}`))
)
)
yield* Console.log(`Total executions: ${results.length}`)
})
// Either with different delay strategies
const aggressiveRetry = Schedule.exponential("100 millis").pipe(Schedule.take(3))
const fallbackRetry = Schedule.fixed("5 seconds").pipe(Schedule.take(2))
// Will use the more aggressive retry until it's exhausted, then fallback
const combinedRetry = Schedule.either(aggressiveRetry, fallbackRetry)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Retry attempt ${attempt}`)
if (attempt < 6) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
combinedRetry
)
yield* Console.log(`Final result: ${result}`)
})
// Either provides union semantics (OR logic)
// Compare with both, which provides intersection semantics (AND logic)

See

  • both for continuing only while both schedules still recur

Signature

declare const either: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Combines two Schedules by recurring if either of the two schedules wants to recur, using the minimum of the two durations between recurrences and outputting the result of the left schedule (i.e. self).

When to use

Use when either schedule may keep recurrence going and only the left schedule’s output is needed.

Example (Combining either schedules and keeping the left output)

import { Console, Effect, Schedule } from "effect"
// Combine two schedules with either semantics, keeping left output
const primarySchedule = Schedule.exponential("100 millis").pipe(
Schedule.map(() => Effect.succeed("primary-result")),
Schedule.take(2)
)
const backupSchedule = Schedule.spaced("500 millis").pipe(Schedule.map(() => Effect.succeed("backup-result")))
const combined = Schedule.eitherLeft(primarySchedule, backupSchedule)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-done"
}),
combined.pipe(Schedule.take(5))
)
})

Signature

declare const eitherLeft: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v4.0.0

Combines two Schedules by recurring if either of the two schedules wants to recur, using the minimum of the two durations between recurrences and outputting the result of the right schedule (i.e. other).

When to use

Use when either schedule may keep recurrence going and only the right schedule’s output is needed.

Example (Combining either schedules and keeping the right output)

import { Console, Effect, Schedule } from "effect"
// Combine two schedules with either semantics, keeping right output
const primarySchedule = Schedule.exponential("100 millis").pipe(
Schedule.map(() => Effect.succeed("primary-result")),
Schedule.take(2)
)
const backupSchedule = Schedule.spaced("500 millis").pipe(Schedule.map(() => Effect.succeed("backup-result")))
const combined = Schedule.eitherRight(primarySchedule, backupSchedule)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-done"
}),
combined.pipe(Schedule.take(5))
)
})

Signature

declare const eitherRight: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output2, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output2, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v4.0.0

Combines two Schedules by recurring if either of the two schedules wants to recur, using the minimum of the two durations between recurrences and outputting the result of the combination of both schedule outputs using the specified combine function.

When to use

Use when either schedule may keep recurrence going and their outputs should be combined into a custom value.

Example (Combining either schedule outputs)

import { Console, Effect, Schedule } from "effect"
// Combine schedules with either semantics and custom combination
const primarySchedule = Schedule.exponential("100 millis").pipe(
Schedule.map(() => Effect.succeed("primary")),
Schedule.take(2)
)
const fallbackSchedule = Schedule.spaced("500 millis").pipe(Schedule.map(() => Effect.succeed("fallback")))
const combined = Schedule.eitherWith(primarySchedule, fallbackSchedule, (primary, fallback) => `${primary}+${fallback}`)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-result"
}),
combined.pipe(Schedule.take(5))
)
})

Signature

declare const eitherWith: {
<Output2, Input2, Error2, Env2, Output, Output3>(
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a new Schedule that recurs on the specified Cron schedule and outputs the duration between recurrences.

Example (Scheduling work with cron expressions)

import { Console, Data, Effect, Schedule } from "effect"
class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {}
// Run every minute
const everyMinute = Schedule.cron("* * * * *")
const minutelyProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Running minutely task")
return "minute"
}),
everyMinute.pipe(
Schedule.take(3), // Run only 3 times for demo
Schedule.tapOutput((duration) => Console.log(`Next execution in: ${duration}`))
)
)
})
// Run every day at 2:30 AM
const dailyBackup = Schedule.cron("30 2 * * *")
const backupProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Running daily backup...")
// Simulate backup process
yield* Effect.sleep("2 seconds")
yield* Console.log("Backup completed")
return "backup-done"
}),
dailyBackup.pipe(
Schedule.take(2) // Run 2 times for demo
)
)
})
// Run every Monday at 9:00 AM with timezone
const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York")
const reportProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Generating weekly report...")
const report = {
week: 42,
status: "ready" as const
}
yield* Console.log(`Report generated: ${JSON.stringify(report)}`)
return report
}),
weeklyReport.pipe(Schedule.take(1))
)
})
// Run every 15 minutes during business hours (9 AM - 5 PM)
const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5")
const businessProgram = Effect.gen(function* () {
const statuses = ["healthy", "healthy", "degraded", "healthy"] as const
let index = 0
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Business hours health check...")
const status = statuses[index++]
yield* Console.log(`System status: ${status}`)
return status
}),
businessHoursCheck.pipe(
Schedule.take(4) // Demo with 4 checks
)
)
})
// Run on specific days of the month
const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM
const invoiceProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Processing monthly invoices...")
const invoiceCount = 72
yield* Console.log(`Processed ${invoiceCount} invoices`)
return { count: invoiceCount, batch: "2024-01-a" }
}),
monthlyInvoice.pipe(Schedule.take(1))
)
})
// Complex cron with error handling
const complexCron = Schedule.cron("0 2,4,6 * * *").pipe(
Schedule.tapOutput((duration) => Console.log(`Scheduled to run again in ${duration}`))
)
const robustProgram = Effect.gen(function* () {
let attempt = 0
yield* Effect.repeat(
Effect.gen(function* () {
attempt++
yield* Console.log("Complex scheduled task...")
if (attempt === 1) {
return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" }))
}
return "success"
}),
complexCron.pipe(Schedule.take(3))
).pipe(Effect.catch((error: unknown) => Console.log(`Cron task error: ${String(error)}`)))
})

Signature

declare const cron: {
(expression: Cron.Cron): Schedule<Duration.Duration, unknown, Cron.CronParseError>
(expression: string, tz?: string | DateTime.TimeZone): Schedule<Duration.Duration, unknown, Cron.CronParseError>
}

Source

Since v2.0.0

Returns a new schedule that outputs the delay between each occurrence.

Example (Extracting schedule delays)

import { Console, Effect, Schedule } from "effect"
// Extract delays from an exponential backoff schedule
const exponentialDelays = Schedule.delays(Schedule.exponential("100 millis").pipe(Schedule.take(5)))
const delayProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task result"
}),
exponentialDelays.pipe(Schedule.tapOutput((delay) => Console.log(`Waiting ${delay} before next execution`)))
)
})
// Monitor delays from a Fibonacci schedule
const fibonacciDelays = Schedule.delays(Schedule.fibonacci("200 millis").pipe(Schedule.take(8)))
const fibDelayProgram = Effect.gen(function* () {
yield* Effect.repeat(
Console.log("Fibonacci task"),
fibonacciDelays.pipe(Schedule.tapOutput((delay) => Console.log(`Fibonacci delay: ${delay}`)))
)
})
// Extract delays for analysis or logging
const analyzeDelays = Schedule.delays(Schedule.spaced("1 second").pipe(Schedule.take(3))).pipe(
Schedule.tapOutput((delay) =>
Effect.gen(function* () {
yield* Console.log(`Recorded delay: ${delay}`)
// In real applications, might send to metrics system
})
)
)
// Combine delays with other schedules for complex timing
const adaptiveSchedule = Schedule.unfold(100, (delay) => Effect.succeed(delay * 1.5)).pipe(Schedule.take(6))
const adaptiveDelays = Schedule.delays(adaptiveSchedule)
const adaptiveProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Adaptive task execution")
return "completed"
}),
adaptiveDelays.pipe(Schedule.tapOutput((delay) => Console.log(`Adaptive delay: ${delay}`)))
)
})
// Use delays to implement custom timing logic
const customTimingSchedule = Schedule.delays(Schedule.exponential("50 millis").pipe(Schedule.take(4))).pipe(
Schedule.map((delay) => Effect.succeed(`Next execution in ${delay}`)),
Schedule.tapOutput((message) => Console.log(message))
)

Signature

declare const delays: <Out, In, E, R>(self: Schedule<Out, In, E, R>) => Schedule<Duration.Duration, In, E, R>

Source

Since v2.0.0

Returns a schedule that recurs once after the specified duration.

When to use

Use when you need a schedule that recurs once after a fixed delay.

Details

The schedule outputs the configured duration for its first recurrence and then completes.

Example (Recurring once after a duration)

import { Console, Effect, Schedule } from "effect"
const program = Effect.repeat(Console.log("runs again after one second"), Schedule.duration("1 second"))

See

  • during for recurring until a duration has elapsed

Signature

declare const duration: (durationInput: Duration.Input) => Schedule<Duration.Duration>

Source

Since v2.0.0

Returns a new Schedule that will always recur, but only during the specified duration of time.

When to use

Use to bound a repeating or retrying schedule by elapsed time.

Example (Repeating work during a duration)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Run a task for exactly 5 seconds, regardless of how many iterations
const fiveSecondSchedule = Schedule.during("5 seconds")
const timedProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed inside the time window")
yield* Effect.sleep("500 millis") // Each task takes 500ms
return "task done"
}),
fiveSecondSchedule.pipe(Schedule.tapOutput((elapsedDuration) => Console.log(`Total elapsed: ${elapsedDuration}`)))
)
yield* Console.log("Time limit reached!")
})
// Combine with other schedules for time-bounded execution
const timeAndCountLimited = Schedule.spaced("1 second").pipe(
Schedule.both(Schedule.during("10 seconds")), // Stop after 10 seconds OR
Schedule.both(Schedule.recurs(15)) // 15 attempts, whichever comes first
)
// Burst execution within time window
const burstWindow = Schedule.during("3 seconds")
const burstProgram = Effect.gen(function* () {
yield* Console.log("Starting burst execution...")
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Burst task")
return "burst"
}),
burstWindow
)
yield* Console.log("Burst window completed")
})
// Timed retry window - retry for up to 30 seconds
const timedRetry = Schedule.exponential("200 millis").pipe(Schedule.both(Schedule.during("30 seconds")))
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Retry attempt ${attempt}`)
if (attempt < 4) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
timedRetry
)
yield* Console.log(`Result: ${result}`)
}).pipe(Effect.catch((error: unknown) => Console.log(`Timed out: ${String(error)}`)))

See

  • duration for one delayed recurrence

Signature

declare const during: (duration: Duration.Input) => Schedule<Duration.Duration>

Source

Since v4.0.0

Schedule that always recurs and returns the total elapsed duration since the first recurrence.

Details

This schedule never stops and outputs the cumulative time that has passed since the schedule started executing. Useful for tracking execution time or implementing time-based logic.

Example (Measuring elapsed schedule time)

import { Console, Duration, Effect, Schedule } from "effect"
const program = Effect.gen(function* () {
yield* Effect.repeat(
Console.log("Running task..."),
Schedule.spaced("1 second").pipe(
Schedule.both(Schedule.elapsed),
Schedule.tapOutput(([count, duration]) => Console.log(`Run ${count}, elapsed: ${Duration.toMillis(duration)}ms`)),
Schedule.take(5)
)
)
})

Signature

declare const elapsed: Schedule<Duration.Duration, unknown, never, never>

Source

Since v2.0.0

Schedule that always recurs, but will wait a certain amount between repetitions, given by base * factor.pow(n), where n is the number of repetitions so far. Returns the current duration between recurrences.

Example (Retrying with exponential backoff)

import { Console, Data, Effect, Schedule } from "effect"
class RetryFailure extends Data.TaggedError("RetryFailure")<{ readonly message: string }> {}
// Basic exponential backoff with default factor of 2
const basicExponential = Schedule.exponential("100 millis")
// Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ...
// Custom exponential backoff with factor 1.5
const gentleExponential = Schedule.exponential("200 millis", 1.5)
// Delays: 200ms, 300ms, 450ms, 675ms, 1012ms, ...
// Retry with exponential backoff (limited to 5 attempts)
const retryPolicy = Schedule.exponential("50 millis").pipe(Schedule.both(Schedule.recurs(5)))
const program = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
if (attempt < 4) {
yield* Console.log(`Attempt ${attempt} failed, retrying...`)
return yield* Effect.fail(new RetryFailure({ message: `Failure ${attempt}` }))
}
return `Success on attempt ${attempt}`
}),
retryPolicy
)
yield* Console.log(`Final result: ${result}`)
})
// Will retry with delays: 50ms, 100ms, 200ms before success

Signature

declare const exponential: (base: Duration.Input, factor?: number) => Schedule<Duration.Duration>

Source

Since v2.0.0

Schedule that always recurs, increasing delays by summing the preceding two delays (similar to the Fibonacci sequence). Returns the current duration between recurrences.

Example (Retrying with Fibonacci backoff)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Basic Fibonacci schedule starting with 100ms
const fibSchedule = Schedule.fibonacci("100 millis")
// Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, ...
// Retry with Fibonacci backoff for gradual increase
const retryWithFib = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Attempt ${attempt}`)
if (attempt < 5) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
Schedule.fibonacci("50 millis").pipe(
Schedule.both(Schedule.recurs(6)), // Maximum 6 retries
Schedule.tapOutput((delay) => Console.log(`Next retry in ${delay}`))
)
)
yield* Console.log(`Final result: ${result}`)
})
// Heartbeat with Fibonacci intervals (starts fast, gets slower)
const adaptiveHeartbeat = Effect.gen(function* () {
yield* Console.log("Heartbeat")
return "pulse"
}).pipe(
Effect.repeat(
Schedule.fibonacci("200 millis").pipe(
Schedule.take(8) // First 8 heartbeats
)
)
)
// Fibonacci vs exponential comparison
const compareSchedules = Effect.gen(function* () {
yield* Console.log("=== Fibonacci Delays ===")
// 100ms, 100ms, 200ms, 300ms, 500ms, 800ms
yield* Console.log("=== Exponential Delays ===")
// 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms
// Fibonacci grows more slowly than exponential
})

Signature

declare const fibonacci: (one: Duration.Input) => Schedule<Duration.Duration>

Source

Since v2.0.0

Returns a Schedule that recurs on the specified fixed interval and outputs the number of repetitions of the schedule so far.

When to use

Use when recurrences should stay aligned to a regular cadence.

Gotchas

If the action run between recurrences takes longer than the interval, the next recurrence happens immediately, but missed intervals are not replayed.

|-----interval-----|-----interval-----|-----interval-----|
|---------action--------||action|-----|action|-----------|

Example (Repeating on fixed intervals)

import { Console, Effect, Schedule } from "effect"
// Fixed interval schedule - recurs on a one-second cadence
const everySecond = Schedule.fixed("1 second")
// Health check that runs at fixed intervals
const healthCheck = Effect.gen(function* () {
yield* Console.log("Health check")
yield* Effect.sleep("200 millis") // simulate health check work
return "healthy"
}).pipe(Effect.repeat(Schedule.fixed("2 seconds").pipe(Schedule.take(5))))
// Difference between fixed and spaced:
// - fixed: maintains constant rate regardless of action duration
// - spaced: waits for the duration AFTER each action completes
const longRunningTask = Effect.gen(function* () {
yield* Console.log("Task started")
yield* Effect.sleep("1.5 seconds") // Longer than interval
yield* Console.log("Task completed")
return "done"
})
// Fixed schedule: if task takes 1.5s but interval is 1s,
// next execution happens immediately (no pile-up)
const fixedSchedule = longRunningTask.pipe(Effect.repeat(Schedule.fixed("1 second").pipe(Schedule.take(3))))
// Comparing with spaced (waits 1s AFTER each task)
const spacedSchedule = longRunningTask.pipe(Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.take(3))))
const program = Effect.gen(function* () {
yield* Console.log("=== Fixed Schedule Demo ===")
yield* fixedSchedule
yield* Console.log("=== Spaced Schedule Demo ===")
yield* spacedSchedule
})

See

  • spaced for delaying after each action completes

Signature

declare const fixed: (interval: Duration.Input) => Schedule<number>

Source

Since v2.0.0

Returns a new Schedule that will recur forever.

Details

The output of the schedule is the current count of its repetitions thus far (i.e. 0, 1, 2, ...).

Example (Repeating forever)

import { Console, Effect, Schedule } from "effect"
// A schedule that runs forever with no delay
const infiniteSchedule = Schedule.forever
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Running forever...")
return "continuous-task"
}),
infiniteSchedule.pipe(Schedule.take(5)) // Limit for demo
)
})

Signature

declare const forever: Schedule<number, unknown, never, never>

Source

Since v2.0.0

Creates a Schedule from a step function that returns a Pull.

Example (Creating a custom schedule from a step function)

import { Cause, Duration, Effect, Schedule } from "effect"
const schedule = Schedule.fromStep(
Effect.sync(() => {
let count = 0
return (_now: number, _input: string) => {
if (count >= 3) {
return Cause.done(count)
}
return Effect.succeed([count++, Duration.millis(100)] as [number, Duration.Duration])
}
})
)

Signature

declare const fromStep: <Input, Output, EnvX, Error, ErrorX, Env>(
step: Effect<(now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>, Error, Env>
) => Schedule<Output, Input, Error | Pull.ExcludeDone<ErrorX>, Env | EnvX>

Source

Since v4.0.0

Creates a Schedule from a step function that receives metadata about the schedule’s execution.

Example (Creating a metadata-aware schedule)

import { Cause, Duration, Effect, Schedule } from "effect"
const firstThreeInputs = Schedule.fromStepWithMetadata(
Effect.succeed((metadata: Schedule.InputMetadata<string>) => {
if (metadata.attempt > 3) {
return Cause.done("finished")
}
return Effect.succeed([`attempt ${metadata.attempt}: ${metadata.input}`, Duration.millis(250)] as [
string,
Duration.Duration
])
})
)

Signature

declare const fromStepWithMetadata: <Input, Output, EnvX, ErrorX, Error, Env>(
step: Effect<
(options: InputMetadata<Input>) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>,
Error,
Env
>
) => Schedule<Output, Input, Error | Pull.ExcludeDone<ErrorX>, Env | EnvX>

Source

Since v4.0.0

Creates a schedule that always recurs, passing inputs directly as outputs.

When to use

Use when you need an infinite schedule that preserves input values as outputs.

Details

This schedule runs indefinitely, returning each input value as its output without modification. It effectively acts as a pass-through that simply echoes its input values at each step.

See

  • forever for an infinite schedule that returns incrementing step counts

Signature

declare const identity: <A>() => Schedule<A, A>

Source

Since v2.0.0

Returns a Schedule which can only be stepped the specified number of times before it terminates.

When to use

Use when you need a counter schedule with no additional delay.

Gotchas

recurs(n) counts schedule recurrences, not the first evaluation of the effect being repeated or retried. For retrying, this means one initial attempt plus at most n retries.

Example (Limiting recurrences)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Basic recurs - retry at most 3 times
const maxThreeAttempts = Schedule.recurs(3)
// Retry a failing operation at most 5 times
const program = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Attempt ${attempt}`)
if (attempt < 4) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
Schedule.recurs(5) // Will retry up to 5 times
)
yield* Console.log(`Final result: ${result}`)
})
// Combining recurs with other schedules for sophisticated retry logic
const complexRetry = Schedule.exponential("100 millis").pipe(
Schedule.both(Schedule.recurs(3)) // At most 3 retries
)
// Allow ten recurrences after the initial run
const tenRecurrences = Effect.gen(function* () {
yield* Console.log("Executing task...")
return "completed"
}).pipe(Effect.repeat(Schedule.recurs(10)))
// The schedule outputs the current recurrence count (0-based)
const countingSchedule = Schedule.recurs(3).pipe(Schedule.tapOutput((count) => Console.log(`Execution #${count + 1}`)))

See

  • take for limiting an existing schedule

Signature

declare const recurs: (times: number) => Schedule<number>

Source

Since v2.0.0

Returns a schedule that recurs continuously, each repetition spaced the specified duration from the last run.

When to use

Use when each delay should start after the previous action completes.

Example (Repeating with fixed spacing)

import { Console, Effect, Schedule } from "effect"
// Basic spaced schedule - runs every 2 seconds
const everyTwoSeconds = Schedule.spaced("2 seconds")
// Heartbeat that runs indefinitely with fixed spacing
const heartbeat = Effect.gen(function* () {
yield* Console.log("Heartbeat")
}).pipe(Effect.repeat(everyTwoSeconds))
// Limited repeat - run only 5 times with 1-second spacing
const limitedTask = Effect.gen(function* () {
yield* Console.log("Executing scheduled task...")
yield* Effect.sleep("500 millis") // simulate work
return "Task completed"
}).pipe(Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.take(5))))
// Simple spaced schedule with limited repetitions
const limitedSpaced = Schedule.spaced("100 millis").pipe(
Schedule.both(Schedule.recurs(5)) // at most 5 times
)
const program = Effect.gen(function* () {
yield* Console.log("Starting spaced execution...")
yield* Effect.repeat(Effect.succeed("work item"), limitedSpaced)
yield* Console.log("Completed executions")
})

See

  • fixed for recurrence aligned to a regular cadence

Signature

declare const spaced: (duration: Duration.Input) => Schedule<number>

Source

Since v2.0.0

Creates a schedule that unfolds a state by repeatedly applying a function, outputting the current state and computing the next state.

Example (Unfolding schedule state)

import { Console, Effect, Schedule } from "effect"
// Counter schedule that increments by 1 each time
const counterSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1))
// Outputs: 0, 1, 2, 3, 4, 5, ...
const countingProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "done"
}),
counterSchedule.pipe(
Schedule.take(5),
Schedule.tapOutput((count) => Console.log(`Count: ${count}`))
)
)
})
// Fibonacci sequence schedule
const fibonacciSchedule = Schedule.unfold([0, 1] as [number, number], ([a, b]) =>
Effect.succeed([b, a + b] as [number, number])
)
// Outputs: [0,1], [1,1], [1,2], [2,3], [3,5], [5,8], ...
const fibProgram = Effect.gen(function* () {
yield* Effect.repeat(
Console.log("Fibonacci step"),
fibonacciSchedule.pipe(
Schedule.take(8),
Schedule.tapOutput(([a, b]) => Console.log(`Fib: ${a}, next: ${b}`))
)
)
})
// Effectful unfold - exponential backoff with state
const exponentialState = Schedule.unfold(100, (delayMs) =>
Effect.gen(function* () {
yield* Console.log(`Current delay: ${delayMs}ms`)
return Math.min(delayMs * 2, 5000) // Cap at 5 seconds
})
)
// Deterministic delay adjustment schedule
const adjustedDelaySchedule = Schedule.unfold({ delay: 1000, adjustment: 100 }, ({ delay, adjustment }) =>
Effect.gen(function* () {
const nextDelay = Math.max(100, delay + adjustment)
yield* Console.log(`Adjusted delay: ${nextDelay}ms`)
return { delay: nextDelay, adjustment: adjustment * -1 }
})
)
// State machine schedule
type State = "init" | "warming" | "active" | "cooling"
const stateMachineSchedule = Schedule.unfold("init" as State, (state) => {
switch (state) {
case "init":
return Effect.succeed("warming" as State)
case "warming":
return Effect.succeed("active" as State)
case "active":
return Effect.succeed("cooling" as State)
case "cooling":
return Effect.succeed("active" as State)
}
})
const stateMachineProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("State machine step")
return "step"
}),
stateMachineSchedule.pipe(
Schedule.take(10),
Schedule.tapOutput((state) => Console.log(`State: ${state}`))
)
)
})

Signature

declare const unfold: <State, Error = never, Env = never>(
initial: State,
next: (state: State) => Effect<State, Error, Env>
) => Schedule<State, unknown, Error, Env>

Source

Since v2.0.0

Schedule that divides the timeline to interval-long windows, and sleeps until the nearest window boundary every time it recurs.

Details

For example, Schedule.windowed("10 seconds") would produce a schedule as follows:

10s 10s 10s 10s
|----------|----------|----------|----------|
|action------|sleep---|act|-sleep|action----|

Example (Repeating on aligned windows)

import { Console, Effect, Schedule } from "effect"
// Execute tasks at regular intervals aligned to window boundaries
const windowSchedule = Schedule.windowed("5 seconds")
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Window task executed")
return "window-task"
}),
windowSchedule.pipe(Schedule.take(4))
)
})

Signature

declare const windowed: (interval: Duration.Input) => Schedule<number>

Source

Since v2.0.0

Returns a new Schedule that adds the delay computed by the specified effectful function to the next recurrence of the schedule.

Example (Adding extra delay to a schedule)

import { Console, Data, Duration, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Add a deterministic extra delay based on the schedule output
const delayedSchedule = Schedule.addDelay(Schedule.exponential("100 millis").pipe(Schedule.take(5)), (output) =>
Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25))
)
const repeatProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.succeed("delayed task"),
delayedSchedule.pipe(Schedule.tapOutput((delay) => Console.log(`Base delay: ${delay}`)))
)
})
// Add adaptive delay based on execution count
const adaptiveSchedule = Schedule.addDelay(Schedule.recurs(6), (executionCount) =>
// Increase delay as execution count grows
Effect.succeed(Duration.millis(executionCount * 200))
)
const adaptiveProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Adaptive delay task")
return "adaptive"
}),
adaptiveSchedule.pipe(Schedule.tapOutput((count) => Console.log(`Execution ${count + 1} with adaptive delay`)))
)
})
// Add effectful delay computation from deterministic service data
const loadByExecution = [1, 3, 2, 4] as const
const dynamicSchedule = Schedule.addDelay(Schedule.spaced("1 second").pipe(Schedule.take(4)), (executionNumber) => {
const load = loadByExecution[executionNumber] ?? 1
return Effect.succeed(Duration.millis(load * 100))
})
const dynamicProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Dynamic delay task")
return "dynamic"
}),
dynamicSchedule
)
})
// Combine with retry for progressive backoff
const progressiveRetrySchedule = Schedule.addDelay(
Schedule.exponential("50 millis").pipe(Schedule.take(4)),
() => Effect.succeed(Duration.millis(100)) // Fixed additional delay
)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
if (attempt < 5) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
progressiveRetrySchedule
)
yield* Console.log(`Final result: ${result}`)
})

Signature

declare const addDelay: {
<Output, Error2 = never, Env2 = never>(
f: (output: Output) => Effect<Duration.Input, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a new Schedule that randomly adjusts each recurrence delay.

When to use

Use to add random variation to an existing schedule’s recurrence delays while preserving its output and completion behavior.

Details

Each recurrence delay is scaled by a random factor between 0.8 and 1.2.

See

  • modifyDelay for replacing recurrence delays with a custom effectful transformation

Signature

declare const jittered: <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>

Source

Since v2.0.0

Returns a new Schedule that modifies the delay of the next recurrence of the schedule using the specified effectful function.

Example (Modifying delays from schedule output)

import { Console, Duration, Effect, Schedule } from "effect"
// Modify delays based on output - increase delay on high iteration counts
const adaptiveDelay = Schedule.recurs(10).pipe(
Schedule.modifyDelay((output, delay) => {
// Double the delay if we're seeing high iteration counts
return Effect.succeed(output > 5 ? Duration.times(delay, 2) : delay)
})
)
const program = Effect.gen(function* () {
let counter = 0
yield* Effect.repeat(
Effect.gen(function* () {
counter++
yield* Console.log(`Attempt ${counter}`)
return counter
}),
adaptiveDelay.pipe(Schedule.take(8))
)
})

Signature

declare const modifyDelay: {
<Output, Error2 = never, Env2 = never>(
f: (output: Output, delay: Duration.Duration) => Effect<Duration.Input, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output, delay: Duration.Input) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Extracts the step function from a Schedule.

Example (Extracting a schedule step function)

import { Effect, Schedule } from "effect"
// Extract step function from an existing schedule
const schedule = Schedule.exponential("100 millis").pipe(Schedule.take(3))
const program = Effect.gen(function* () {
const stepFn = yield* Schedule.toStep(schedule)
// Use the step function directly for custom logic. The timestamp is
// supplied by the caller, so tests can pass a deterministic value.
const now = 0
const result = yield* stepFn(now, "input")
console.log(`Step result: ${result}`)
})

Signature

declare const toStep: <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
) => Effect<(now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], Error, Output, Env>, never, Env>

Source

Since v4.0.0

Extracts a step function from a Schedule that sleeps for each computed delay and returns metadata for the completed step.

When to use

Use to drive a schedule manually while preserving the computed output, delay, input, attempt, and elapsed timing metadata for each step.

Details

The returned step reads the current time from Clock when invoked, calls the schedule step with that timestamp and input, sleeps for the returned duration, and then yields Metadata.

See

  • toStep for manually supplying the timestamp and handling the returned delay yourself
  • toStepWithSleep for the same automatic sleeping behavior when only the schedule output is needed

Signature

declare const toStepWithMetadata: <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
) => Effect<(input: Input) => Pull.Pull<Metadata<Output, Input>, Error, Output, Env>, never, Env>

Source

Since v4.0.0

Extracts a step function from a Schedule that automatically handles sleep delays.

Example (Extracting a sleeping step function)

import { Effect, Schedule } from "effect"
// Convert schedule to step function with automatic sleeping
const schedule = Schedule.spaced("1 second").pipe(Schedule.take(3))
const program = Effect.gen(function* () {
const stepWithSleep = yield* Schedule.toStepWithSleep(schedule)
// Each call will automatically sleep for the scheduled delay
console.log("Starting...")
const result1 = yield* stepWithSleep("first")
console.log(`First result: ${result1}`)
const result2 = yield* stepWithSleep("second")
console.log(`Second result: ${result2}`)
const result3 = yield* stepWithSleep("third")
console.log(`Third result: ${result3}`)
})

Signature

declare const toStepWithSleep: <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
) => Effect<(input: Input) => Pull.Pull<Output, Error, Output, Env>, never, Env>

Source

Since v4.0.0

Returns a new schedule that continues while the predicate returns true.

When to use

Use to stop an existing schedule based on its full metadata, such as the current input, output, attempt, delay, or elapsed time.

Details

The predicate receives Metadata, may return boolean or an Effect<boolean, ...>, preserves the output and delay when it returns true, and stops the schedule when it returns false.

See

  • collectWhile for collecting outputs while using the same predicate
  • take for stopping after a fixed number of schedule outputs

Signature

declare const while: { <Input, Output, Error2 = never, Env2 = never>(predicate: (metadata: Metadata<Output, Input>) => boolean | Effect<boolean, Error2, Env2>): <Error, Env>(self: Schedule<Output, Input, Error, Env>) => Schedule<Output, Input, Error | Error2, Env | Env2>; <Output, Input, Error, Env, Error2 = never, Env2 = never>(self: Schedule<Output, Input, Error, Env>, predicate: (metadata: Metadata<Output, Input>) => boolean | Effect<boolean, Error2, Env2>): Schedule<Output, Input, Error | Error2, Env | Env2>; }

Source

Since v4.0.0

Returns a new Schedule that combines the outputs of the provided schedule using the specified effectful combine function and starting from the specified initial state.

Example (Reducing schedule outputs)

import { Console, Effect, Schedule } from "effect"
// Sum up execution counts from a counter schedule
const sumSchedule = Schedule.reduce(
Schedule.recurs(5),
() => 0, // Initial sum
(sum, count) => Effect.succeed(sum + count) // Add each count to the sum
)
const sumProgram = Effect.gen(function* () {
const finalSum = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task"
}),
sumSchedule.pipe(Schedule.tapOutput((sum) => Console.log(`Running sum: ${sum}`)))
)
yield* Console.log(`Final sum: ${finalSum}`)
})
// Build a history of execution counts
const historySchedule = Schedule.reduce(
Schedule.spaced("1 second").pipe(Schedule.take(4)),
() => [] as Array<number>, // Initial empty array
(history, executionNumber) => Effect.succeed([...history, executionNumber])
)
const historyProgram = Effect.gen(function* () {
const timeline = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Recording execution...")
return "recorded"
}),
historySchedule
)
yield* Console.log(`Execution timeline: ${timeline.join(", ")}`)
})
// Accumulate metrics with effectful combination
const metricsAccumulator = Schedule.reduce(
Schedule.recurs(6),
() => ({ total: 0, count: 0, max: 0 }),
(metrics, executionCount) =>
Effect.succeed({
total: metrics.total + executionCount + 1,
count: metrics.count + 1,
max: Math.max(metrics.max, executionCount + 1)
})
)
const metricsProgram = Effect.gen(function* () {
const finalMetrics = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Processing...")
return "processed"
}),
metricsAccumulator
)
const average = finalMetrics.total / finalMetrics.count
yield* Console.log(`Final metrics: ${finalMetrics.count} executions`)
yield* Console.log(`Average delay: ${average.toFixed(1)}ms, Max delay: ${finalMetrics.max}ms`)
})
// Build configuration state over time
const configBuilder = Schedule.reduce(
Schedule.fixed("500 millis").pipe(Schedule.take(3)),
() => ({ retries: 1, timeout: 1000, backoff: 100 }),
(config, executionNumber) =>
Effect.succeed({
retries: config.retries + 1,
timeout: config.timeout * 1.5,
backoff: Math.min(config.backoff * 2, 5000)
})
)
const configProgram = Effect.gen(function* () {
const finalConfig = yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Updating configuration...")
return "updated"
}),
configBuilder.pipe(
Schedule.tapOutput((config) => Console.log(`Config: retries=${config.retries}, timeout=${config.timeout}ms`))
)
)
yield* Console.log(`Final config: ${JSON.stringify(finalConfig)}`)
})

Signature

declare const reduce: {
<State, Output, Error2 = never, Env2 = never>(
initial: LazyArg<State>,
combine: (state: State, output: Output) => State | Effect<State, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<State, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, State, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
initial: LazyArg<State>,
combine: (state: State, output: Output) => State | Effect<State, Error2, Env2>
): Schedule<State, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Type guard that checks if a value is a Schedule.

Example (Checking for schedules)

import { Schedule } from "effect"
const schedule = Schedule.exponential("100 millis")
const notSchedule = { foo: "bar" }
console.log(Schedule.isSchedule(schedule)) // true
console.log(Schedule.isSchedule(notSchedule)) // false
console.log(Schedule.isSchedule(null)) // false
console.log(Schedule.isSchedule(undefined)) // false

Signature

declare const isSchedule: (u: unknown) => u is Schedule<unknown, never, unknown, unknown>

Source

Since v2.0.0

Returns a new Schedule that maps the output of this schedule using the specified function.

Example (Mapping schedule outputs)

import { Console, Effect, Schedule } from "effect"
// Transform schedule output from number to string
const countSchedule = Schedule.recurs(5).pipe(Schedule.map((count) => Effect.succeed(`Execution #${count + 1}`)))
// Map schedule delays to human-readable format
const readableDelays = Schedule.exponential("100 millis").pipe(
Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`))
)
// Transform numeric output to structured data
const structuredSchedule = Schedule.spaced("1 second").pipe(
Schedule.map((recurrence) =>
Effect.succeed({
iteration: recurrence + 1,
phase: recurrence < 5 ? ("warmup" as const) : ("steady" as const)
})
)
)
const program = Effect.gen(function* () {
const results = yield* Effect.repeat(
Effect.succeed("task completed"),
structuredSchedule.pipe(
Schedule.take(8),
Schedule.tapOutput((info) => Console.log(`${info.phase} phase - iteration ${info.iteration}`))
)
)
yield* Console.log(`Completed iterations`)
})
// Map with effectful transformation
const effectfulMap = Schedule.fixed("2 seconds").pipe(
Schedule.map((count) =>
Effect.gen(function* () {
yield* Console.log(`Processing count: ${count}`)
return count * 10
})
)
)
// Combine mapping with other schedule operations
const complexSchedule = Schedule.fibonacci("100 millis").pipe(
Schedule.map((delay) => Effect.succeed(`Delay: ${delay}`))
)

Signature

declare const map: {
<Output, Output2, Error2 = never, Env2 = never>(
f: (output: Output) => Output2 | Effect<Output2, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output2, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Output2 | Effect<Output2, Error2, Env2>
): Schedule<Output2, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a new Schedule that outputs the inputs of the specified schedule.

Example (Passing inputs through as outputs)

import { Console, Effect, Schedule } from "effect"
// Create a schedule that outputs the inputs instead of original outputs
const inputSchedule = Schedule.passthrough(Schedule.exponential("100 millis").pipe(Schedule.take(3)))
const program = Effect.gen(function* () {
let counter = 0
yield* Effect.repeat(
Effect.gen(function* () {
counter++
yield* Console.log(`Task ${counter} executed`)
return `result-${counter}`
}),
inputSchedule
)
})

Signature

declare const passthrough: <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Input, Input, Error, Env>

Source

Since v2.0.0

Context reference containing metadata for the currently running schedule step.

Details

Repeat, retry, stream, and channel scheduling operations provide this service to effects run between schedule steps. The default value contains undefined input and output values, zero duration, and zeroed timing fields before any schedule step has produced metadata.

Signature

declare const CurrentMetadata: Context.Reference<Metadata<unknown, unknown>>

Source

Since v4.0.0

Metadata provided to schedule functions containing timing and input information.

Example (Reading schedule input metadata)

import { Console, Effect, Schedule } from "effect"
// Custom schedule that uses input metadata
const metadataAwareSchedule = Schedule.spaced("1 second").pipe(
Schedule.collectWhile((metadata) => Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 10000))
)
const program = Effect.gen(function* () {
yield* Effect.repeat(Console.log("Task execution"), metadataAwareSchedule)
})

Signature

export interface InputMetadata<Input> {
readonly input: Input
readonly attempt: number
readonly start: number
readonly now: number
readonly elapsed: number
readonly elapsedSincePrevious: number
}

Source

Since v4.0.0

Extended metadata that includes both input metadata and the output value from the schedule.

Example (Logging schedule output metadata)

import { Console, Duration, Effect, Schedule } from "effect"
// Custom schedule that logs metadata and output for each recurrence
const loggingSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe(
Schedule.addDelay(() => Effect.succeed(Duration.millis(100))),
Schedule.collectWhile((metadata) =>
Console.log(`Output: ${metadata.output}, attempt: ${metadata.attempt}, elapsed: ${metadata.elapsed}ms`).pipe(
Effect.as(metadata.attempt <= 3)
)
)
)
const program = Effect.gen(function* () {
yield* Effect.repeat(Effect.succeed("task completed"), loggingSchedule.pipe(Schedule.take(3)))
})
// Output logs will show:
// Output: 0, attempt: 1, elapsed: 0ms
// Output: 1, attempt: 2, elapsed: 100ms
// Output: 2, attempt: 3, elapsed: 200ms

Signature

export interface Metadata<Output = unknown, Input = unknown> extends InputMetadata<Input> {
readonly output: Output
readonly duration: Duration.Duration
}

Source

Since v4.0.0

A Schedule defines a strategy for repeating or retrying effects based on some policy.

Example (Defining retry and repeat schedules)

import { Console, Data, Effect, Schedule } from "effect"
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly attempt: number
}> {}
// Basic retry schedule - retry up to 3 times with exponential backoff
const retrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)))
// Basic repeat schedule - repeat every 30 seconds forever
const repeatSchedule: Schedule.Schedule<number, unknown, never> = Schedule.spaced("30 seconds")
const program = Effect.gen(function* () {
let attempts = 0
const result1 = yield* Effect.retry(
Effect.gen(function* () {
attempts++
if (attempts < 3) {
return yield* Effect.fail(new NetworkError({ attempt: attempts }))
}
return "Success"
}),
retrySchedule
)
console.log(result1) // "Success"
yield* Console.log("heartbeat").pipe(Effect.repeat(repeatSchedule.pipe(Schedule.take(5))))
})

Signature

export interface Schedule<out Output, in Input = unknown, out Error = never, out Env = never>
extends Schedule.Variance<Output, Input, Error, Env>, Pipeable {}

Source

Since v2.0.0

Returns a schedule that runs self to completion, then runs other, and merges their outputs.

Example (Sequencing quick and slow retries)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// First retry 3 times quickly, then switch to slower retries
const quickRetries = Schedule.exponential("100 millis").pipe(Schedule.take(3))
const slowRetries = Schedule.exponential("1 second").pipe(Schedule.take(2))
const combinedRetries = Schedule.andThen(quickRetries, slowRetries)
const program = Effect.gen(function* () {
let attempt = 0
yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Attempt ${attempt}`)
if (attempt < 6) {
return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` }))
}
return `Success on attempt ${attempt}`
}),
combinedRetries
)
})

Signature

declare const andThen: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output | Output2, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output | Output2, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a schedule that runs self to completion, then runs other, and preserves which schedule produced each output.

Details

The resulting schedule emits a Result to indicate which phase produced each output: outputs from self are emitted as Failure, and outputs from other are emitted as Success.

Example (Tracking sequential schedule phases)

import { Console, Effect, Result, Schedule } from "effect"
// Track which phase of the schedule we're in
const phaseTracker = Schedule.andThenResult(
Schedule.exponential("100 millis").pipe(Schedule.take(2)),
Schedule.spaced("500 millis").pipe(Schedule.take(2))
)
const program = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Task executed")
return "task-result"
}),
phaseTracker.pipe(
Schedule.tapOutput((result) =>
Result.match(result, {
onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
})
)
)
)
})

Signature

declare const andThenResult: {
<Output2, Input2, Error2, Env2>(
other: Schedule<Output2, Input2, Error2, Env2>
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Result.Result<Output2, Output>, Input & Input2, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Result.Result<Output2, Output>, Input & Input2, Error | Error2, Env | Env2>
}

Source

Since v4.0.0

Returns a new Schedule that allows execution of an effectful function for every decision of the schedule, but does not alter the inputs and outputs of the schedule.

Details

The callback receives the full schedule metadata, including the input, output, computed delay duration, current attempt, and elapsed timing information.

Example (Tapping schedule metadata)

import { Console, Effect, Schedule } from "effect"
const monitoredSchedule = Schedule.exponential("100 millis").pipe(
Schedule.take(5),
Schedule.tap((metadata) =>
Console.log(
`Attempt ${metadata.attempt} produced ${metadata.output} ` +
`after ${metadata.elapsed}ms; next delay is ${metadata.duration}`
)
)
)
const program = Effect.retry(Effect.fail("transient error"), monitoredSchedule)

Signature

declare const tap: {
<Output, Input, X, Error2, Env2>(
f: (metadata: Metadata<Output, Input>) => Effect<X, Error2, Env2>
): <Error, Env>(self: Schedule<Output, Input, Error, Env>) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (metadata: Metadata<Output, Input>) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
}

Source

Since v4.0.0

Returns a new Schedule that allows execution of an effectful function for every input to the schedule, but does not alter the inputs and outputs of the schedule.

Example (Tapping retry inputs)

import { Console, Data, Effect, Schedule } from "effect"
class RetryError extends Data.TaggedError("RetryError")<{ readonly message: string }> {}
// Log retry errors for debugging
const errorLoggingSchedule = Schedule.exponential("100 millis").pipe(
Schedule.take(3),
Schedule.tapInput((error: RetryError) => Console.log(`Retry triggered by error: ${String(error)}`))
)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
if (attempt < 4) {
return yield* Effect.fail(new RetryError({ message: `Network timeout on attempt ${attempt}` }))
}
return `Success on attempt ${attempt}`
}),
errorLoggingSchedule
)
yield* Console.log(`Final result: ${result}`)
})
// Monitor input frequency for metrics
const inputMonitoringSchedule = Schedule.spaced("1 second").pipe(
Schedule.take(5),
Schedule.tapInput((input: unknown) =>
Effect.gen(function* () {
yield* Console.log(`Input type: ${typeof input}`)
// In real applications, might send metrics to monitoring system
})
)
)
// Input validation with side effects
const validatingSchedule = Schedule.fixed("500 millis").pipe(
Schedule.take(4),
Schedule.tapInput((input: any) =>
Effect.gen(function* () {
if (typeof input === "object" && input !== null) {
yield* Console.log(`Valid object input: ${JSON.stringify(input)}`)
} else {
yield* Console.log(`Warning: Non-object input received: ${input}`)
}
})
)
)
const validationProgram = Effect.gen(function* () {
let count = 0
yield* Effect.repeat(
Effect.gen(function* () {
count++
yield* Console.log("Task with validation")
return { data: `sample-${count}` }
}),
validatingSchedule
)
})
// Conditional alerting based on input
const alertingSchedule = Schedule.exponential("200 millis").pipe(
Schedule.take(6),
Schedule.tapInput((error: RetryError) =>
Effect.gen(function* () {
if (String(error).includes("critical")) {
yield* Console.log(`Critical error: ${String(error)}`)
// In real applications, might trigger alerts or notifications
} else {
yield* Console.log(`Regular error: ${String(error)}`)
}
})
)
)
const alertProgram = Effect.gen(function* () {
let attempt = 0
yield* Effect.retry(
Effect.gen(function* () {
attempt++
const isCritical = attempt === 3
const errorType = isCritical ? "critical database failure" : "temporary network issue"
return yield* Effect.fail(new RetryError({ message: errorType }))
}),
alertingSchedule
).pipe(Effect.catch((error: unknown) => Console.log(`All retries exhausted: ${String(error)}`)))
})
// Chain multiple input taps for different purposes
const comprehensiveSchedule = Schedule.fibonacci("100 millis").pipe(
Schedule.take(5),
Schedule.tapInput((error: RetryError) => Console.log(`Error occurred: ${error._tag}`)),
Schedule.tapInput((error: RetryError) =>
String(error).length > 20 ? Console.log("Long error message detected") : Effect.void
)
)

Signature

declare const tapInput: {
<Input, X, Error2, Env2>(
f: (input: Input) => Effect<X, Error2, Env2>
): <Output, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (input: Input) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a new Schedule that allows execution of an effectful function for every output of the schedule, but does not alter the inputs and outputs of the schedule.

Example (Tapping schedule outputs)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Log schedule outputs for debugging/monitoring
const monitoredSchedule = Schedule.exponential("100 millis").pipe(
Schedule.take(5),
Schedule.tapOutput((delay) => Console.log(`Next delay will be: ${delay}`))
)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
if (attempt < 4) {
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
monitoredSchedule
)
yield* Console.log(`Final result: ${result}`)
})
// Tap output for metrics collection
const metricsSchedule = Schedule.spaced("1 second").pipe(
Schedule.take(10),
Schedule.tapOutput((executionCount) =>
Effect.gen(function* () {
// Simulate metrics collection
yield* Console.log(`Recording metric: execution_count=${executionCount}`)
// In real code, this might send to monitoring system
})
)
)
// Tap output with conditional side effects
const alertingSchedule = Schedule.fibonacci("200 millis").pipe(
Schedule.take(8),
Schedule.tapOutput((delay) =>
Effect.gen(function* () {
const delayMs = delay.toString()
if (delayMs.includes("1000")) {
// Alert on delays >= 1 second
yield* Console.log(`High delay detected: ${delay}`)
}
})
)
)
const healthCheckProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Performing health check...")
return "healthy"
}),
alertingSchedule
)
})
// Chain multiple taps for different purposes
const comprehensiveSchedule = Schedule.fixed("500 millis").pipe(
Schedule.take(6),
Schedule.tapOutput((count) => Console.log(`Execution ${count + 1}`)),
Schedule.tapOutput((count) => (count % 3 === 0 ? Console.log("Checkpoint reached") : Effect.void))
)

Signature

declare const tapOutput: {
<Output, X, Error2, Env2>(
f: (output: Output) => Effect<X, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
}

Source

Since v2.0.0

Returns a new Schedule that takes at most the specified number of outputs from the schedule. Once the specified number of outputs is reached, the schedule will stop.

When to use

Use to limit an existing schedule while preserving its output and delay behavior.

Gotchas

take(n) limits schedule outputs. When used with repeat or retry, the effect is evaluated once before the schedule is stepped, so the total number of evaluations can be one greater than the number of outputs taken.

Example (Taking a limited number of recurrences)

import { Console, Data, Effect, Schedule } from "effect"
class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
// Limit an infinite schedule to five recurrences
const limitedHeartbeat = Schedule.spaced("1 second").pipe(
Schedule.take(5) // Will stop after 5 schedule outputs
)
const heartbeatProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
yield* Console.log("Heartbeat")
return "pulse"
}),
limitedHeartbeat
)
yield* Console.log("Heartbeat sequence completed")
})
// Limit retry attempts to a specific number
const limitedRetry = Schedule.exponential("100 millis").pipe(
Schedule.take(3) // At most 3 retry attempts
)
const retryProgram = Effect.gen(function* () {
let attempt = 0
const result = yield* Effect.retry(
Effect.gen(function* () {
attempt++
yield* Console.log(`Attempt ${attempt}`)
if (attempt < 5) {
// Will fail more than 3 times
return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
}
return `Success on attempt ${attempt}`
}),
limitedRetry
)
yield* Console.log(`Result: ${result}`)
}).pipe(Effect.catch((error: unknown) => Console.log(`Failed after limited retries: ${String(error)}`)))
// Combine take with other schedule operations
const samplingSchedule = Schedule.fixed("500 millis").pipe(
Schedule.take(10), // Take at most 10 schedule outputs
Schedule.map((count) => Effect.succeed(`Sample #${count + 1}`))
)
const samplingProgram = Effect.gen(function* () {
yield* Effect.repeat(
Effect.gen(function* () {
const value = "sample"
yield* Console.log(`Sampled value: ${value}`)
return value
}),
samplingSchedule.pipe(Schedule.tapOutput((label) => Console.log(`Completed: ${label}`)))
)
})

See

  • recurs for creating a count-limited schedule

Signature

declare const take: {
(
n: number
): <Output, Input, Error, Env>(self: Schedule<Output, Input, Error, Env>) => Schedule<Output, Input, Error, Env>
<Output, Input, Error, Env>(self: Schedule<Output, Input, Error, Env>, n: number): Schedule<Output, Input, Error, Env>
}

Source

Since v4.0.0

Ensures that a schedule’s error type extends a given type T.

Details

This helper is checked at compile time and does not change the schedule’s runtime behavior.

Example (Constraining schedule error types)

import { Data, Schedule } from "effect"
// Create a custom error using Data.TaggedError
class CustomError extends Data.TaggedError("CustomError")<{
message: string
}> {}
declare const CustomErrorSchedule: Schedule.Schedule<number, unknown, CustomError>
declare const StringErrorSchedule: Schedule.Schedule<number, unknown, string>
const satisfiesCustomError = Schedule.satisfiesErrorType<CustomError>()
// This works because the schedule error type is CustomError.
const validSchedule = satisfiesCustomError(CustomErrorSchedule)
// This would cause a TypeScript compilation error:
// const invalidSchedule = satisfiesCustomError(StringErrorSchedule)

Signature

declare const satisfiesErrorType: <T>() => <Error extends T, Output = never, Input = unknown, Env = never>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>

Source

Since v4.0.0

Ensures that a schedule’s input type extends a given type T.

When to use

Use when you need a generic helper to prove that an existing schedule can consume a required input type without changing runtime behavior.

Example (Constraining schedule input types)

import { Schedule } from "effect"
declare const StringInputSchedule: Schedule.Schedule<number, string>
declare const NumberInputSchedule: Schedule.Schedule<number, number>
const satisfiesStringInput = Schedule.satisfiesInputType<string>()
// This works because the schedule input type is string.
const validSchedule = satisfiesStringInput(StringInputSchedule)
// This would cause a TypeScript compilation error:
// const invalidSchedule = satisfiesStringInput(NumberInputSchedule)

See

  • setInputType for adapting an input-agnostic schedule

Signature

declare const satisfiesInputType: <T>() => <Input extends T, Output = never, Error = never, Env = never>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>

Source

Since v4.0.0

Ensures that a schedule’s output type extends a given type T.

Details

This helper is checked at compile time and does not change the schedule’s runtime behavior.

Example (Constraining schedule output types)

import { Schedule } from "effect"
declare const StringOutputSchedule: Schedule.Schedule<string>
declare const NumberOutputSchedule: Schedule.Schedule<number>
const satisfiesStringOutput = Schedule.satisfiesOutputType<string>()
// This works because the schedule output type is string.
const validSchedule = satisfiesStringOutput(StringOutputSchedule)
// This would cause a TypeScript compilation error:
// const invalidSchedule = satisfiesStringOutput(NumberOutputSchedule)

Signature

declare const satisfiesOutputType: <T>() => <Output extends T, Error = never, Input = unknown, Env = never>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>

Source

Since v4.0.0

Ensures that a schedule’s context type extends a given type T.

Details

This helper is checked at compile time and does not change the schedule’s runtime behavior.

Example (Constraining schedule service types)

import { Schedule } from "effect"
interface Logger {
readonly log: (message: string) => void
}
declare const LoggerSchedule: Schedule.Schedule<number, unknown, never, Logger>
declare const NumberSchedule: Schedule.Schedule<number, unknown, never, number>
const satisfiesLogger = Schedule.satisfiesServicesType<Logger>()
// This works because the schedule context type is Logger.
const validSchedule = satisfiesLogger(LoggerSchedule)
// This would cause a TypeScript compilation error:
// const invalidSchedule = satisfiesLogger(NumberSchedule)

Signature

declare const satisfiesServicesType: <T>() => <Env extends T, Output = never, Input = unknown, Error = never>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>

Source

Since v4.0.0

Sets the input type of the provided schedule without altering its behavior.

When to use

Use to adapt a schedule that does not depend on its input values.

Details

This helper is checked at compile time and does not change the schedule’s runtime behavior.

Example (Setting a schedule input type)

import { Schedule } from "effect"
const schedule = Schedule.recurs(3).pipe(Schedule.setInputType<string>())

See

  • satisfiesInputType for checking an existing input type

Signature

declare const setInputType: <T>() => <Output, Error, Env>(
self: Schedule<Output, T, Error, Env>
) => Schedule<Output, T, Error, Env>

Source

Since v4.0.0

The Schedule namespace contains types and utilities for working with schedules.

Example (Creating custom schedules with the namespace)

import { Duration, Effect, Schedule } from "effect"
// Usage of the Schedule namespace for creating schedules
// Create custom schedule with metadata
const customSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe(
Schedule.addDelay((n) => Effect.succeed(Duration.millis(n * 100)))
)
const program = Effect.gen(function* () {
let attempt = 0
yield* Effect.retry(
Effect.gen(function* () {
attempt++
if (attempt < 3) {
return yield* Effect.fail(`Attempt ${attempt} failed`)
}
return `Success on attempt ${attempt}`
}),
customSchedule.pipe(Schedule.take(5))
)
})

Source

Since v2.0.0

Variance interface that defines the type parameter relationships for Schedule.

Example (Understanding schedule variance)

import { Effect, Schedule } from "effect"
// Understanding Schedule variance:
// - Output: covariant (can be a subtype)
// - Input: contravariant (can accept supertypes)
// - Error: covariant (can be a subtype)
// - Env: covariant (can be a subtype)
// Schedule that produces strings, accepts any input
const stringSchedule = Schedule.spaced("1 second").pipe(Schedule.map(() => Effect.succeed("tick")))
// Schedule that only accepts Error inputs
const errorSchedule = Schedule.exponential("100 millis").pipe(Schedule.take(5))
// Schedule requiring a service environment
const serviceSchedule = Schedule.spaced("5 seconds")

Signature

export interface Variance<out Output, in Input, out Error, out Env> {
readonly [TypeId]: VarianceStruct<Output, Input, Error, Env>
}

Source

Since v2.0.0

Type-level marker used by Schedule.Variance to record the variance of Schedule type parameters.

Details

This interface exists for TypeScript inference and assignability. Users normally do not construct or inspect it directly.

Signature

export interface VarianceStruct<out Output, in Input, out Error, out Env> {
readonly _Out: Covariant<Output>
readonly _In: Contravariant<Input>
readonly _Error: Covariant<Error>
readonly _Env: Covariant<Env>
}

Source

Since v4.0.0