Cron
Backoff and spacing schedules recur relative to when the last run finished.
Some jobs instead need to fire at specific points on the calendar — “every
weekday at 9:00”, “at 4 AM on the 8th through 14th of each month”. The Cron
module describes those points using UNIX cron expressions,
and Schedule.cron turns one into a Schedule you can
drive with Effect.repeat.
import { Cron, Effect, Schedule } from "effect"
// "second minute hour day-of-month month day-of-week"// → at 09:00:00, Monday through Friday.const businessDaysAt9 = Schedule.cron("0 0 9 * * 1-5", "Europe/Rome")
const sendDigest = Effect.gen(function* () { yield* Effect.log("sending the daily digest")}).pipe(Effect.repeat(businessDaysAt9))Schedule.cron accepts the expression as a string (with an optional time zone)
or a pre-built Cron value. The resulting schedule’s Output is a
Duration — the gap it slept until the next matching instant — and its Error
channel is Cron.CronParseError, since an invalid expression surfaces as a
typed failure rather than a thrown exception.
Building a Cron without parsing
Section titled “Building a Cron without parsing”When you’d rather not hand-write an expression, Cron.make builds a Cron from
explicit numeric constraints. An empty (or omitted) field means “no constraint”.
import { Cron, DateTime } from "effect"
// 04:00 on the 8th–14th of every month, in the Europe/Rome time zone.const cron = Cron.make({ seconds: [0], minutes: [0], hours: [4], days: [8, 9, 10, 11, 12, 13, 14], months: [], // any month weekdays: [], // any weekday tz: DateTime.zoneMakeNamedUnsafe("Europe/Rome")})Parsing expressions safely
Section titled “Parsing expressions safely”Cron.parse validates an expression and returns a
Result — Result.Success with the Cron, or
Result.Failure with a CronParseError. Use it when the expression comes from
configuration or user input and might be malformed.
import { Cron, Result } from "effect"
const parsed = Cron.parse("0 0 4 8-14 * *", "UTC")
if (Result.isSuccess(parsed)) { // parsed.success is a Cron} else { // parsed.failure is a CronParseError with a .message}If you control the expression and want to fail fast on a typo, Cron.parseUnsafe
returns the Cron directly and throws on invalid input — convenient for
module-level constants.
Querying a Cron directly
Section titled “Querying a Cron directly”A Cron is useful even outside a Schedule. These helpers take a DateTime.Input
(a Date, epoch millis, or DateTime):
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")
// Does this instant satisfy the schedule?Cron.match(cron, new Date("2025-01-08T04:00:00Z")) // true
// When does it next fire on or after a given instant? (returns a Date)Cron.next(cron, new Date("2025-01-01T00:00:00Z")) // 2025-01-08T04:00:00.000Z
// Iterate matching instants lazily.const it = Cron.sequence(cron, new Date("2025-01-01T00:00:00Z"))it.next().value // 2025-01-08T04:00:00.000Zit.next().value // 2025-01-09T04:00:00.000ZCombining cron with other schedules
Section titled “Combining cron with other schedules”Because Schedule.cron returns an ordinary Schedule, it composes like any
other. Cap a cron job to a fixed number of runs, or tap its output for logging:
import { Duration, Effect, Schedule } from "effect"
const limited = Schedule.cron("0 0 9 * * 1-5", "Europe/Rome").pipe( Schedule.both(Schedule.recurs(10)), // run at most 10 times Schedule.tapOutput((slept: Duration.Duration) => Effect.logDebug(`woke after ${Duration.toMillis(slept)}ms`) ))To verify cron-driven code without waiting for the calendar, advance simulated
time with TestClock — see Clock.