Skip to content

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.

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")
})

Cron.parse validates an expression and returns a ResultResult.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.

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.000Z
it.next().value // 2025-01-09T04:00:00.000Z

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.