Schedule.ts
Schedule.ts overview
Section titled “Schedule.ts overview”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
Exports Grouped by Category
Section titled “Exports Grouped by Category”- collecting
- combining
- constructors
- delays & timeouts
- destructors
- filtering
- folding
- guards
- mapping
- metadata
- models
- sequencing
- taking
- utility types
- utils
collecting
Section titled “collecting”collectInputs
Section titled “collectInputs”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 scheduleconst 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>Since v4.0.0
collectOutputs
Section titled “collectOutputs”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 scheduleconst 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>Since v4.0.0
collectWhile
Section titled “collectWhile”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 metconst 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 predicateconst 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 conditionconst 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>}Since v2.0.0
combining
Section titled “combining”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 continueconst timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 timesconst 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 delayconst 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 exhaustsconst 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
eitherfor 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>}Since v2.0.0
bothLeft
Section titled “bothLeft”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 outputconst 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>}Since v2.0.0
bothRight
Section titled “bothRight”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 outputconst 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>}Since v2.0.0
bothWith
Section titled “bothWith”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 combinationconst 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>}Since v2.0.0
either
Section titled “either”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 continueconst 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 strategiesconst 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 fallbackconst 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
bothfor 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>}Since v2.0.0
eitherLeft
Section titled “eitherLeft”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 outputconst 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>}Since v4.0.0
eitherRight
Section titled “eitherRight”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 outputconst 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>}Since v4.0.0
eitherWith
Section titled “eitherWith”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 combinationconst 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>}Since v2.0.0
constructors
Section titled “constructors”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 minuteconst 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 AMconst 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 timezoneconst 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 monthconst 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 handlingconst 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>}Since v2.0.0
delays
Section titled “delays”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 scheduleconst 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 scheduleconst 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 loggingconst 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 timingconst 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 logicconst 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>Since v2.0.0
duration
Section titled “duration”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
duringfor recurring until a duration has elapsed
Signature
declare const duration: (durationInput: Duration.Input) => Schedule<Duration.Duration>Since v2.0.0
during
Section titled “during”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 iterationsconst 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 executionconst 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 windowconst 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 secondsconst 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
durationfor one delayed recurrence
Signature
declare const during: (duration: Duration.Input) => Schedule<Duration.Duration>Since v4.0.0
elapsed
Section titled “elapsed”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>Since v2.0.0
exponential
Section titled “exponential”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 2const basicExponential = Schedule.exponential("100 millis")// Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ...
// Custom exponential backoff with factor 1.5const 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 successSignature
declare const exponential: (base: Duration.Input, factor?: number) => Schedule<Duration.Duration>Since v2.0.0
fibonacci
Section titled “fibonacci”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 100msconst fibSchedule = Schedule.fibonacci("100 millis")// Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, ...
// Retry with Fibonacci backoff for gradual increaseconst 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 comparisonconst 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>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 cadenceconst everySecond = Schedule.fixed("1 second")
// Health check that runs at fixed intervalsconst 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
spacedfor delaying after each action completes
Signature
declare const fixed: (interval: Duration.Input) => Schedule<number>Since v2.0.0
forever
Section titled “forever”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 delayconst 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>Since v2.0.0
fromStep
Section titled “fromStep”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>Since v4.0.0
fromStepWithMetadata
Section titled “fromStepWithMetadata”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>Since v4.0.0
identity
Section titled “identity”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
foreverfor an infinite schedule that returns incrementing step counts
Signature
declare const identity: <A>() => Schedule<A, A>Since v2.0.0
recurs
Section titled “recurs”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 timesconst maxThreeAttempts = Schedule.recurs(3)
// Retry a failing operation at most 5 timesconst 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 logicconst complexRetry = Schedule.exponential("100 millis").pipe( Schedule.both(Schedule.recurs(3)) // At most 3 retries)
// Allow ten recurrences after the initial runconst 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
takefor limiting an existing schedule
Signature
declare const recurs: (times: number) => Schedule<number>Since v2.0.0
spaced
Section titled “spaced”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 secondsconst everyTwoSeconds = Schedule.spaced("2 seconds")
// Heartbeat that runs indefinitely with fixed spacingconst heartbeat = Effect.gen(function* () { yield* Console.log("Heartbeat")}).pipe(Effect.repeat(everyTwoSeconds))
// Limited repeat - run only 5 times with 1-second spacingconst 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 repetitionsconst 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
fixedfor recurrence aligned to a regular cadence
Signature
declare const spaced: (duration: Duration.Input) => Schedule<number>Since v2.0.0
unfold
Section titled “unfold”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 timeconst 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 scheduleconst 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 stateconst 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 scheduleconst 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 scheduletype 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>Since v2.0.0
windowed
Section titled “windowed”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 boundariesconst 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>Since v2.0.0
delays & timeouts
Section titled “delays & timeouts”addDelay
Section titled “addDelay”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 outputconst 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 countconst 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 dataconst 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 backoffconst 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>}Since v2.0.0
jittered
Section titled “jittered”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
modifyDelayfor 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>Since v2.0.0
modifyDelay
Section titled “modifyDelay”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 countsconst 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>}Since v2.0.0
destructors
Section titled “destructors”toStep
Section titled “toStep”Extracts the step function from a Schedule.
Example (Extracting a schedule step function)
import { Effect, Schedule } from "effect"
// Extract step function from an existing scheduleconst 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>Since v4.0.0
toStepWithMetadata
Section titled “toStepWithMetadata”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
toStepfor manually supplying the timestamp and handling the returned delay yourselftoStepWithSleepfor 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>Since v4.0.0
toStepWithSleep
Section titled “toStepWithSleep”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 sleepingconst 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>Since v4.0.0
filtering
Section titled “filtering”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
collectWhilefor collecting outputs while using the same predicatetakefor 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>; }Since v4.0.0
folding
Section titled “folding”reduce
Section titled “reduce”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 scheduleconst 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 countsconst 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 combinationconst 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 timeconst 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>}Since v2.0.0
guards
Section titled “guards”isSchedule
Section titled “isSchedule”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)) // trueconsole.log(Schedule.isSchedule(notSchedule)) // falseconsole.log(Schedule.isSchedule(null)) // falseconsole.log(Schedule.isSchedule(undefined)) // falseSignature
declare const isSchedule: (u: unknown) => u is Schedule<unknown, never, unknown, unknown>Since v2.0.0
mapping
Section titled “mapping”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 stringconst countSchedule = Schedule.recurs(5).pipe(Schedule.map((count) => Effect.succeed(`Execution #${count + 1}`)))
// Map schedule delays to human-readable formatconst readableDelays = Schedule.exponential("100 millis").pipe( Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`)))
// Transform numeric output to structured dataconst 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 transformationconst 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 operationsconst 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>}Since v2.0.0
passthrough
Section titled “passthrough”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 outputsconst 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>Since v2.0.0
metadata
Section titled “metadata”CurrentMetadata
Section titled “CurrentMetadata”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>>Since v4.0.0
InputMetadata (interface)
Section titled “InputMetadata (interface)”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 metadataconst 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}Since v4.0.0
Metadata (interface)
Section titled “Metadata (interface)”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 recurrenceconst 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: 200msSignature
export interface Metadata<Output = unknown, Input = unknown> extends InputMetadata<Input> { readonly output: Output readonly duration: Duration.Duration}Since v4.0.0
models
Section titled “models”Schedule (interface)
Section titled “Schedule (interface)”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 backoffconst retrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)))
// Basic repeat schedule - repeat every 30 seconds foreverconst 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 {}Since v2.0.0
sequencing
Section titled “sequencing”andThen
Section titled “andThen”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 retriesconst 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>}Since v2.0.0
andThenResult
Section titled “andThenResult”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 inconst 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>}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>}Since v4.0.0
tapInput
Section titled “tapInput”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 debuggingconst 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 metricsconst 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 effectsconst 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 inputconst 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 purposesconst 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>}Since v2.0.0
tapOutput
Section titled “tapOutput”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/monitoringconst 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 collectionconst 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 effectsconst 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 purposesconst 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>}Since v2.0.0
taking
Section titled “taking”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 recurrencesconst 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 numberconst 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 operationsconst 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
recursfor 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>}Since v4.0.0
utility types
Section titled “utility types”satisfiesErrorType
Section titled “satisfiesErrorType”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.TaggedErrorclass 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>Since v4.0.0
satisfiesInputType
Section titled “satisfiesInputType”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
setInputTypefor 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>Since v4.0.0
satisfiesOutputType
Section titled “satisfiesOutputType”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>Since v4.0.0
satisfiesServicesType
Section titled “satisfiesServicesType”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>Since v4.0.0
setInputType
Section titled “setInputType”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
satisfiesInputTypefor checking an existing input type
Signature
declare const setInputType: <T>() => <Output, Error, Env>( self: Schedule<Output, T, Error, Env>) => Schedule<Output, T, Error, Env>Since v4.0.0
Schedule (namespace)
Section titled “Schedule (namespace)”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 metadataconst 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)) )})Since v2.0.0
Variance (interface)
Section titled “Variance (interface)”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 inputconst stringSchedule = Schedule.spaced("1 second").pipe(Schedule.map(() => Effect.succeed("tick")))
// Schedule that only accepts Error inputsconst errorSchedule = Schedule.exponential("100 millis").pipe(Schedule.take(5))
// Schedule requiring a service environmentconst serviceSchedule = Schedule.spaced("5 seconds")Signature
export interface Variance<out Output, in Input, out Error, out Env> { readonly [TypeId]: VarianceStruct<Output, Input, Error, Env>}Since v2.0.0
VarianceStruct (interface)
Section titled “VarianceStruct (interface)”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>}Since v4.0.0