---
title: Cron
description: Distributed cron scheduling with leader election and per-tenant support.
---

The `cron` package provides distributed cron scheduling. Only the cluster leader fires cron entries, guaranteeing at-most-once job enqueue even when multiple Dispatch instances are running.

## Registering a cron

Use `engine.RegisterCron` at startup:

```go
type ReportInput struct {
    Format string `json:"format"`
}

var GenerateReport = job.NewDefinition("generate_report",
    func(ctx context.Context, input ReportInput) error {
        return generateDailyReport(input.Format)
    },
)

// Run every day at 9 AM
engine.RegisterCron(ctx, eng, "daily-report", "0 9 * * *",
    GenerateReport, ReportInput{Format: "pdf"})
```

The cron entry is persisted in the store. If an entry with the same name already exists, `ErrDuplicateCron` is returned (unless `UpsertOnConflict` is used).

## Schedule format

Dispatch uses standard 5-field cron expressions (via `robfig/cron/v3`):

```
┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12)
│ │ │ │ ┌───── day of week (0–6, Sunday=0)
│ │ │ │ │
* * * * *
```

Common expressions:

| Expression | Meaning |
|-----------|---------|
| `* * * * *` | Every minute |
| `0 * * * *` | Every hour |
| `0 9 * * 1-5` | Weekdays at 9 AM |
| `0 0 1 * *` | First of each month at midnight |
| `@hourly` | Every hour (alias) |
| `@daily` | Every day at midnight (alias) |

## Enable / Disable

Cron entries can be toggled at runtime:

```bash
POST /v1/crons/:cronId/enable
POST /v1/crons/:cronId/disable
```

Disabled entries are skipped by the scheduler without being deleted.

## Admin API

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/crons` | List all cron entries |
| `GET` | `/v1/crons/:cronId` | Get cron entry |
| `POST` | `/v1/crons/:cronId/enable` | Enable |
| `POST` | `/v1/crons/:cronId/disable` | Disable |
| `DELETE` | `/v1/crons/:cronId` | Delete |

## Extension hook

When a cron fires and a job is enqueued, the `ext.CronFired` hook is called:

```go
type CronFired interface {
    OnCronFired(ctx context.Context, entryName string, jobID id.JobID) error
}
```
