Skip to content
Effect Days 2026 Get your ticket
Docs menu / Scheduling work with cron

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-5

This 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-5
0 30 6 * * 1-5

Fields 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 = 0
const 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 startup
const reportFiber = Effect.runFork(reportJob)
// During application shutdown
await Effect.runPromise(Fiber.interrupt(reportFiber))
runs // => 0

Effect.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.