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, for seconds/tz, omitted) field
means “no constraint” — i.e. “match any value”, exactly like *.
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.
Cron field syntax
Section titled “Cron field syntax”Each of the six fields accepts the same grammar:
| Form | Example | Meaning |
|---|---|---|
| Any | * | every value in the field’s range |
| Single | 9 | exactly 9 |
| Range | 8-14 | 8 through 14 inclusive |
| List | 1,15 | 1 and 15 |
| Step | */15 | every 15th value (0, 15, 30, …) |
| Range+step | 0-30/10 | every 10th value in 0–30 (0, 10, 20, 30) |
The field ranges are: seconds 0-59, minutes 0-59, hours 0-23, day-of-month
1-31, month 1-12, weekday 0-6 (0 is Sunday). The month and weekday fields
also accept three-letter aliases — JAN–DEC and SUN–SAT (case-insensitive).
import { Cron } from "effect"
// Every 15 minutes, business hours, weekdays — using aliases.Cron.parseUnsafe("0 */15 9-17 * * MON-FRI")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.
Reference
Section titled “Reference”Everything the Cron module exports. A Cron value is immutable, hashable, and
implements Equal, so two crons with the same field
restrictions compare equal by value.
Builds a Cron from explicit field constraints. minutes, hours, days,
months, and weekdays are required iterables of numbers; seconds (defaults
to [0]) and tz (a DateTime.TimeZone) are optional. An empty iterable for a
field means “match any value” — the same as *. Values may be passed in any
order; they are sorted and de-duplicated internally.
import { Cron, DateTime } from "effect"
// 09:30 every Monday, in New York local time.const cron = Cron.make({ seconds: [0], minutes: [30], hours: [9], days: [], // any day-of-month months: [], // any month weekdays: [1], // Monday tz: DateTime.zoneMakeNamedUnsafe("America/New_York")})
Cron.match(cron, new Date("2025-06-02T13:30:00Z")) // => true (Mon 09:30 EDT)Parses a five- or six-field cron expression into a
Result<Cron, CronParseError>. The optional second argument is a time zone,
either a DateTime.TimeZone or an IANA string like "Europe/Rome". A five-field
expression has seconds prepended as 0 automatically.
import { Cron, Result } from "effect"
Cron.parse("0 0 4 8-14 * *", "UTC")// => Result.Success(Cron { hours: {4}, days: {8..14}, tz: Some(UTC) })
Cron.parse("nope")// => Result.Failure(CronParseError: "Invalid number of segments in cron expression")parseUnsafe
Section titled “parseUnsafe”Like parse, but returns the Cron directly and throws on invalid input. Use it
for trusted, literal expressions (e.g. module-level constants).
import { Cron } from "effect"
Cron.parseUnsafe("0 0 9 * * *", "America/New_York")// => Cron { hours: {9}, tz: Some(America/New_York) }
// Cron.parseUnsafe("invalid") // throws CronParseErrorisCron
Section titled “isCron”Type guard that narrows an unknown value to Cron.
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 9 * * *")
Cron.isCron(cron) // => trueCron.isCron({}) // => falseCron.isCron("0 0 9 * * *") // => falseisCronParseError
Section titled “isCronParseError”Type guard that narrows an unknown value to CronParseError — handy when
inspecting a caught defect or a generic failure channel.
import { Cron, Result } from "effect"
const result = Cron.parse("totally invalid")
if (Result.isFailure(result)) { Cron.isCronParseError(result.failure) // => true}
Cron.isCronParseError(new Error("nope")) // => falseReturns true when a given instant satisfies the schedule. Seconds, minutes,
hours, and months are matched directly; the day-of-month/weekday pair follows
cron’s inclusive rule (either may match when both are constrained). The instant
is interpreted in the cron’s time zone, if any.
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")
Cron.match(cron, new Date("2025-01-08T04:00:00Z")) // => trueCron.match(cron, new Date("2025-01-08T05:00:00Z")) // => false (wrong hour)Cron.match(cron, new Date("2025-01-07T04:00:00Z")) // => false (wrong day)Returns the next matching Date strictly after the supplied instant (or after
“now” when omitted). Because the search is strict, passing a matching instant
yields the following occurrence.
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")
Cron.next(cron, new Date("2025-01-01T00:00:00Z"))// => 2025-01-08T04:00:00.000Z
Cron.next(cron, new Date("2025-01-08T04:00:00Z")) // already a match// => 2025-01-09T04:00:00.000ZReturns the previous matching Date strictly before the supplied instant (or
before “now” when omitted). The search is strict, mirroring next.
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")
Cron.prev(cron, new Date("2025-01-20T00:00:00Z"))// => 2025-01-14T04:00:00.000Zsequence
Section titled “sequence”Returns an infinite IterableIterator<Date> of matching instants, each strictly
after the previous. Combine with Array.from({ length }) or a for…of with a
break to take a finite slice.
import { Cron } from "effect"
const cron = Cron.parseUnsafe("0 0 9 * * 1-5") // 09:00, weekdays
const iterator = Cron.sequence(cron, new Date("2025-01-01T00:00:00Z"))const next3 = Array.from({ length: 3 }, () => iterator.next().value)// => [// 2025-01-01T09:00:00.000Z, // Wed// 2025-01-02T09:00:00.000Z, // Thu// 2025-01-03T09:00:00.000Z // Fri// ]Equivalence
Section titled “Equivalence”An Equivalence over Cron that compares the six
field restrictions (seconds, minutes, hours, days, months, weekdays). It does
not compare the optional time zone, and ordering of the input values is
irrelevant.
import { Cron } from "effect"
const a = Cron.parseUnsafe("0 0,30 9 1,15 * 1-5")const b = Cron.parseUnsafe("0 30,0 9 15,1 * 1-5") // values reordered
Cron.Equivalence(a, b) // => trueequals
Section titled “equals”Checks whether two Cron values have the same field restrictions (ignoring time
zone). Available in both data-last (curried) and data-first forms.
import { Cron } from "effect"
const a = Cron.parseUnsafe("0 0 9 1,15 * 1-5")const b = Cron.parseUnsafe("0 0 9 1,15 * 1-5")
Cron.equals(a, b) // => trueCron.equals(a)(b) // => true (curried)CronParseError
Section titled “CronParseError”The tagged error (_tag: "CronParseError") produced when parsing fails. It
carries a human-readable message and the offending input string. Cron.parse
returns it in the failure channel of its Result, and Schedule.cron surfaces
the very same type in its Error channel — so a malformed expression there is a
typed failure you can Effect.catch,
never a thrown exception.
import { Cron, Result } from "effect"
const result = Cron.parse("0 99 9 * * *") // minute out of range
if (Result.isFailure(result)) { const error = result.failure error._tag // => "CronParseError" error.message // => "Expected a value between 0 and 59" error.input // => "99"}import { Cron, Effect, Schedule } from "effect"
// The CronParseError reaches the effect's error channel, not a throw.const program = Effect.void.pipe( Effect.repeat(Schedule.cron("not a cron")), Effect.catchTag("CronParseError", (e: Cron.CronParseError) => Effect.logError(`bad cron: ${e.message}`) ))Cron (type)
Section titled “Cron (type)”The model interface. A Cron exposes six ReadonlySet<number> fields plus an
optional time zone. Empty sets denote unconstrained fields (the * case), so
inspect schedules through the public helpers rather than assuming every allowed
value is stored.
import { Cron } from "effect"
declare const cron: Cron.Croncron.seconds // ReadonlySet<number>cron.minutes // ReadonlySet<number>cron.hours // ReadonlySet<number>cron.days // ReadonlySet<number> (day-of-month, 1–31)cron.months // ReadonlySet<number> (1–12)cron.weekdays // ReadonlySet<number> (0–6, Sunday = 0)cron.tz // Option.Option<DateTime.TimeZone>