# Cron Explainer

> Cron expressions in plain English, field by field, with the next run times in your time zone.

Category: Time & Scheduling · Page: https://util.caruana.me/tools/cron/

## API, MCP and A2A

### Explain a cron expression (`cron_explain`)

`GET|POST https://util.caruana.me/api/cron/explain` · MCP tool `cron_explain` · A2A skill `cron_explain` · [reference](https://util.caruana.me/reference/cron_explain/)

Accepts the 5-field crontab form (minute hour day-of-month month day-of-week), a 6-field form whose first field is seconds, and the macros @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly. Returns an English description, a field-by-field breakdown with the expanded values, the day-matching rule in effect, and the next run times after `from` in `zone`. Day-of-month and day-of-week follow Vixie cron: when both are restricted a day matches if EITHER matches (`0 0 13 * 5` runs on every 13th AND every Friday); a field starting with `*` (such as `*/2`) counts as unrestricted, and then a day must match both. Errors name the offending field.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `expression` | string (≤ 200 chars) | yes | Cron expression, e.g. `*/15 9-17 * * 1-5`, `0 0 1 * *`, `30 0 9 * * MON` (6 fields: seconds first), or `@daily`. Month and weekday names (`JAN`, `MON`) are case-insensitive; 0 and 7 are both Sunday. |
| `zone` | string (≤ 64 chars) | no | Zone in which the schedule is evaluated (the cron daemon's zone, or `CRON_TZ` / Kubernetes `timeZone`): IANA name, `UTC`, or fixed offset. |
| `from` | string (≤ 100 chars) | no | List runs strictly after this instant (default: now). ISO 8601, RFC 2822, HTTP-date, Unix timestamp, or `now`; a time without offset is read in `zone`. |
| `count` | integer (1–50) | no | Number of upcoming run times to return (1-50). |

Privacy: The server receives the cron expression, zone and start time, evaluates them in memory, and returns the result. Your input is not stored or logged; only the per-call metadata row (operation, time, status, sizes) is recorded.


## User guide

The tool sits in a window that works like a desktop app: menus on the top row and the **full-screen** icon at its right (**Esc** leaves full screen). The set-up options described below are in **Settings → Options…**, an overlay, and the **Settings** menu also lists the common choices directly. To keep the options in view instead, choose **View → Inline config**; the page remembers that choice. **Help → Keyboard shortcuts…** lists the keys.

1. **Type a cron expression**, for example `*/15 9-17 * * 1-5`.
2. **Choose the time zone** your cron daemon or scheduler runs in. It defaults to your browser's zone. Servers often run in UTC. Kubernetes CronJobs use the controller's zone unless `spec.timeZone` is set.
3. Read:
   - the **description** in plain English;
   - the **day rule**: which of the two day fields decides, and whether they combine with OR or AND;
   - the **fields** table, with every value each field expands to;
   - the **next runs**, with the UTC equivalent of each. Runs moved by a DST change are marked.

**The rule that surprises everyone.** When _both_ day-of-month and day-of-week are restricted, cron runs on days matching **either** of them. `0 0 13 * 5` runs at midnight on every 13th _and_ on every Friday, not only on Friday the 13th. To run on a weekday within a range of days, schedule on the weekday and check the date in the job, e.g. `[ "$(date +\%d)" -le 07 ]` for "first Monday".

**Syntax at a glance.**

| Syntax         | Meaning                            | Example                                  |
| -------------- | ---------------------------------- | ---------------------------------------- |
| `*`            | every value                        | `* * * * *` every minute                 |
| `a,b`          | list                               | `0 9,17 * * *` at 09:00 and 17:00        |
| `a-b`          | range (inclusive)                  | `0 9-17 * * *` every hour 09:00 to 17:00 |
| `*/n`, `a-b/n` | step                               | `*/10 * * * *` every 10 minutes          |
| names          | `JAN`–`DEC`, `SUN`–`SAT`, any case | `0 0 * * MON-FRI`                        |

## Specification

**Accepted forms.**

| Form                                                | Fields                                                                                                                                                  |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Standard (Vixie cron, cronie, Kubernetes)           | `minute hour day-of-month month day-of-week`                                                                                                            |
| With seconds (cron-parser, node-cron, Spring style) | `second minute hour day-of-month month day-of-week`; a warning notes that standard crontab does not accept it                                           |
| Macros (case-insensitive)                           | `@yearly` = `@annually` = `0 0 1 1 *`; `@monthly` = `0 0 1 * *`; `@weekly` = `0 0 * * 0`; `@daily` = `@midnight` = `0 0 * * *`; `@hourly` = `0 * * * *` |

Fields are separated by any whitespace. Any other number of fields is rejected; a 7th (Quartz "year") field is named in the error.

**Field values.**

- **Ranges:** second and minute 0–59, hour 0–23, day-of-month 1–31, month 1–12, day-of-week 0–7.
- **Sunday** is both 0 and 7; the `values` list always reports Sunday as 0.
- **Steps:** a step of 0, or a range written backwards (`5-1`), is an error.

**Extensions.** These are accepted, and each use produces a portability warning, because Vixie cron and cronie reject them:

| Syntax | Field        | Meaning                                                                                          |
| ------ | ------------ | ------------------------------------------------------------------------------------------------ |
| `L`    | day-of-month | last day of the month (`L`, or in a list: `1,L`)                                                 |
| `nL`   | day-of-week  | last weekday _n_ of the month (`5L` = last Friday)                                               |
| `n#k`  | day-of-week  | _k_-th weekday _n_ of the month, k = 1–5 (`1#2` = second Monday); cannot be combined with a list |
| `?`    | day fields   | same as `*`                                                                                      |

**Rejected, with an explanation.**

- `H` (Jenkins hash), `W` and `LW` (nearest weekday);
- `@reboot` (it has no schedule), and the non-standard macros `@weekdays`, `@weekends`, `@minutely` and `@secondly`;
- any value outside the field's range.

Every syntax and range error starts with the name of the offending field, e.g. `minute field "61": …`.

**Day-of-month and day-of-week (Vixie cron rule).**

- **Unrestricted field:** a day field is _unrestricted_ when it begins with `*` (this includes `*/2`) or is `?`.
- **Both restricted** (`0 0 13 * 5`): a day matches when **either** field matches. `day_rule.rule` is `either`.
- **One field `*` or `?`:** only the other field selects days (`day-of-month` or `day-of-week`). If both are, every day matches (`every-day`).
- **A `*`-prefixed field that still narrows** (`0 0 */2 * 1`): Vixie cron counts it as unrestricted, so a day must match **both** fields (an odd day that is also a Monday). `day_rule.rule` is `both`. This matches Vixie cron's `DOM_STAR`/`DOW_STAR` flags and robfig/cron (Kubernetes). cron-parser alone would OR these fields; this tool filters its results to apply the Vixie rule.

**Run times.**

- **Search:** runs are listed strictly after `from` (default: now), in ascending order, at whole-second resolution.
- **`count`:** 1–50 (default 5 on the API, 10 on the page).
- **Zone:** run times are computed in `zone` with cron-parser, then shown as ISO 8601 with offset, UTC, Unix seconds, wall-clock time and weekday.
- **Never-matching days:** a schedule whose day fields can never match (`0 0 30 2 *`) is rejected by cron-parser with a 422 error.
- **Rare schedules:** if the search ends before `count` runs are found, the list is shorter and `warnings` says why. cron-parser stops at its internal loop limit, and the AND filter examines at most 5,000 candidates.

**Daylight saving time.** Behaviour in a zone with DST, verified for `America/New_York`:

- **Spring forward (gap).** A run scheduled at a wall-clock time that does not exist is moved forward by the gap. `30 2 * * *` runs at 03:30 EDT on the changeover day and is flagged `dst_shifted: true`. Runs every 20 minutes during 02:00–02:59 run at 03:00, 03:20 and 03:40.
- **Fall back (overlap).** A job at a fixed time in the repeated hour runs once, at the first occurrence: `30 1 * * *` runs at 01:30 EDT, not again at 01:30 EST. A job whose hour field is `*` runs in both copies of the hour (`30 * * * *` runs at 01:30 EDT and 01:30 EST).

Real daemons differ in this. cronie runs skipped jobs immediately after the change; some systems skip them. Check your scheduler if it matters, or run the job in UTC.

**Description.** The English text comes from cronstrue (24-hour clock). For expressions where both day fields are set, its wording ("on day 13 of the month, and on Friday") is less precise than `day_rule.explanation`, which is authoritative.

**Limits.** Expression up to 200 characters.
