# Schedules

> Run a job on a cron expression in a time zone, pause and resume schedules, and understand what happens when a schedule overlaps or is missed.

Source: https://galley.themes.dev/docs/guides/schedules/

A schedule runs a job on a timetable: every night at two, every fifteen minutes, the first Monday of the month. Runnel keeps the timetable, so you no longer need a cron server, and every scheduled run is an ordinary run with retries, logs and a status.

## Defining a schedule

Schedules are declared in code next to the job they run. `runnel deploy` registers them, and they start firing in that environment straight away.

```ts title="src/schedules.ts"
import { runnel } from '@runnel/sdk';
import { usageReport } from './jobs/usage-report';
import { syncSupplier } from './jobs/sync-supplier';

runnel.schedule({
  name: 'nightly-usage',
  cron: '0 2 * * *',
  timezone: 'Europe/London',
  job: usageReport,
  payload: { period: 'day' },
});

runnel.schedule({
  name: 'supplier-sync',
  cron: '*/15 * * * *',
  job: syncSupplier,
  payload: { supplierId: 'sup_atlas' },
});
```

Import `schedules.ts` from your worker file so the definitions are picked up. One job can have several schedules with different payloads.

## Cron expressions

Runnel uses the standard five-field syntax: minute, hour, day of month, month, day of week. A few that come up often:

| Expression | Runs |
|---|---|
| `*/15 * * * *` | every 15 minutes |
| `0 2 * * *` | daily at 02:00 |
| `30 8 * * 1-5` | weekdays at 08:30 |
| `0 9 1 * *` | the first of every month at 09:00 |
| `0 0 * * 0` | Sundays at midnight |

The shorthands `@hourly`, `@daily` and `@weekly` work too. Seconds are not supported; the shortest interval is one minute.

## Time zones

Without `timezone`, a schedule runs in UTC. With it, the expression is read in that zone, using IANA names such as `Europe/London` or `America/Chicago`. Daylight saving is handled the way people expect:

- When clocks go forward and a time does not exist (02:30 on the last Sunday of March in London), the run fires at the next valid minute.
- When clocks go back and a time happens twice, the run fires once, on the first occurrence.

> **Tip** Reports for people should use their time zone. Housekeeping jobs (cleaning up expired sessions, rotating files) are simpler in UTC, where every day has 24 hours.

## Overlaps and missed runs

If a run is still going when the next one is due, Runnel starts the next one anyway. For jobs that must not overlap, set `overlap: 'skip'` and the new run is skipped and logged instead:

```ts
runnel.schedule({ name: 'supplier-sync', cron: '*/15 * * * *', job: syncSupplier, overlap: 'skip', payload: { supplierId: 'sup_atlas' } });
```

Schedules fire from Runnel's side, so a worker that is down does not lose them: the runs wait in the queue until a worker picks them up. If you would rather drop runs that waited too long, add `expireAfter: '10m'`; queued scheduled runs older than that are cancelled.

## Pausing and resuming

You can pause a schedule from the dashboard or the CLI without a deploy. A paused schedule fires nothing, and on resume it does not catch up on the runs it skipped.

```bash
runnel schedules list --env production
runnel schedules pause nightly-usage --env production
runnel schedules resume nightly-usage --env production
```

A paused schedule stays paused across deploys until you resume it. Removing a schedule from your code and deploying deletes it.

## Python and Go

```python
runnel.schedule(name="nightly-usage", cron="0 2 * * *", timezone="Europe/London",
                job=usage_report, payload={"period": "day"})
```

```go
runnel.NewSchedule("nightly-usage", "0 2 * * *", UsageReport,
	runnel.InTimezone("Europe/London"), runnel.WithPayload(Period{Kind: "day"}))
```

To see when a schedule will next fire, run `runnel schedules list`; it prints the next three times in the schedule's own zone. The [Jobs API](/docs/reference/jobs-api/) returns the same under each job's `schedules`.
