Scheduling work with cron
Use cron when work must run at a calendar time, such as 06:30 every weekday in
New York. Use Schedule.spaced or
Schedule.fixed
when only the time between runs matters.
Define the calendar rule
A cron expression has five fields: minutes, hours, days of the month, months, and weekdays, in that order.
# ┌──────── minute (0-59)# │ ┌────── hour (0-23)# │ │ ┌──── day of month (1-31)# │ │ │ ┌── month (1-12)# │ │ │ │ ┌ weekday (0-7, Sunday is 0 or 7)# │ │ │ │ │ 30 6 * * 1-5This expression means 06:30, Monday through Friday. * allows every value in
a field, and 1-5 covers Monday through Friday.
Effect also accepts a sixth field for seconds at the beginning. With five
fields, seconds default to 0, so these expressions mean the same thing:
30 6 * * 1-50 30 6 * * 1-5Fields also accept comma-separated values. In the minute field, */15 means
every 15 minutes.
Choose the time zone
Use a named time zone such as America/New_York when the job should follow a
local clock. Use UTC when its time must remain fixed throughout the year. Do
not rely on the server’s local time zone for a business rule.
The UTC offset of a named local time zone changes when daylight saving time starts or ends. Some local times then disappear, while others occur twice. Test dates around both changes before choosing a local time for work that must run exactly once per business day.
Validate, preview, and run the job
Parse values from configuration before starting the job. Cron.parse returns a
Result; Effect.fromResult keeps a parse failure in the Effect error channel.
An invalid expression or time zone therefore stops the program before the
scheduled action can run.
Cron.sequence previews the same parsed rule that the job will use. After the
preview looks correct, pass that Cron to Schedule.cron and start the Effect
in a fiber. Interrupt the fiber during application shutdown.
Example (Validating and running a scheduled report)
The application validates a report schedule from configuration, previews its next three runs, and starts the job in a fiber.
import { Cron, Effect, Fiber, Schedule } from "effect"
const config = { expression: "30 6 * * 1-5", timeZone: "America/New_York",}
let runs = 0const generateReport = Effect.sync(() => { runs++})
const reportJob = Effect.gen(function* () { const configuredCron = yield* Effect.fromResult( Cron.parse(config.expression, config.timeZone), )
const dates = Cron.sequence(configuredCron, "2026-01-01T12:00:00Z") const nextThree = Array.from({ length: 3 }, () => dates.next().value?.toISOString(), )
nextThree // => ["2026-01-02T11:30:00.000Z", "2026-01-05T11:30:00.000Z", "2026-01-06T11:30:00.000Z"]
yield* Effect.schedule(generateReport, Schedule.cron(configuredCron))})
// During application startupconst reportFiber = Effect.runFork(reportJob)
// During application shutdownawait Effect.runPromise(Fiber.interrupt(reportFiber))
runs // => 0Effect.schedule waits for the first matching time, so the report does not run
at startup. The test interrupts it before the first scheduled run. In the
application, keep reportFiber for as long as the job should remain active.
This schedule exists only in the running process. It does not recover a missed report after a restart. Work that must run despite downtime needs an external scheduler or a durable record of pending runs.
See the Cron API reference for programmatic
construction and date matching.