/api/cron/explain
MCP tool 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.
Browser version (runs locally): Cron Explainer.
Try it
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
expression required | string (≤ 200 chars) | — | 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) | UTC | 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) | — | 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) | 5 | Number of upcoming run times to return (1-50). |
Also accepted on every endpoint: format (json | text).
Parameters may come from the query string, a JSON object body, an urlencoded or multipart form,
or (for input) a text/plain body.
Global parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | json | text | json | json returns the documented object. text returns only the primary value as text/plain (see "Text output"). |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
expression | string | The expression with whitespace collapsed. |
normalized | string | 5- or 6-field form actually evaluated (macros expanded). |
macro | string | null | The macro used, lower-cased (e.g. @daily), or null. |
dialect | "five-field" | "six-field-seconds" | five-field (standard crontab) or six-field-seconds. |
description | string | English description from cronstrue, 24-hour clock. For the day fields, day_rule is authoritative. |
day_rule | object | |
day_rule.rule | "every-day" | "day-of-month" | "day-of-week" | "either" | "both" | How days are selected: every-day, day-of-month only, day-of-week only, either (OR: both restricted), or both (AND: one starts with * but still narrows). |
day_rule.explanation | string | The rule in plain English for this expression. |
fields | array of object | One entry per field, in expression order (seconds first only for 6-field expressions). |
fields[].field | "second" | "minute" | "hour" | "day-of-month" | "month" | "day-of-week" | Field name. |
fields[].raw | string | The field as written. |
fields[].values | array of integer | Expanded matching values, ascending. Day-of-week uses 0-6 (Sunday = 0). L and # constraints are described in description, not listed. |
fields[].unrestricted | boolean | True for * (and, in the day fields, anything starting with *, or ?), per Vixie cron. |
fields[].description | string | What the field selects, in English. |
timezone | string | Zone the schedule was evaluated in. |
from | string | Start of the search (exclusive), ISO 8601 in timezone. |
next | array of object | Upcoming run times, ascending. Empty when the schedule never fires. |
next[].iso | string | Run time, ISO 8601 with the offset in timezone. |
next[].utc | string | Run time in UTC. |
next[].unix_seconds | integer | Run time as Unix seconds. |
next[].local | string | Wall-clock time in timezone, without offset. |
next[].weekday | string | English weekday in timezone. |
next[].dst_shifted | boolean | True when the scheduled wall-clock time did not exist (DST gap) and the run is shown at the shifted time. |
warnings | array of string | Portability and scheduling warnings: 6-field or Quartz-style syntax, DST shifts, schedules that never run, zone caveats. |
Text output (format=text, text/plain): The upcoming run times, one ISO 8601 time per line.
Errors
Errors are JSON: {"error": "<message>", "code": "<code>", "param": "<name>"?}.
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | zone is unknown or an abbreviation. |
422 | unprocessable_input | The expression has the wrong number of fields, a value out of range (the message names the field), unsupported syntax (H, W, @reboot, unknown macros), or day fields no date can satisfy (0 0 30 2 *); or from cannot be parsed. |
400 | missing_parameter | A required parameter is absent. |
400 | unknown_parameter | A parameter name is not in the table above (typos are rejected, not ignored). |
413 | payload_too_large | The request body exceeds 1 MiB. |
405 | method_not_allowed | A method other than GET, POST or OPTIONS. |
Behaviour
- Not idempotent: the result can differ between calls (time, randomness, or a live network check).
-
Requires a client ID and secret (authentication). Each call is
recorded as one metadata row against your client ID; inputs and outputs are never stored or
logged. Responses carry
cache-control: no-store. - CORS: any origin may call this endpoint from a browser, using your credentials.
Examples
Responses below were produced by running each request through this code at build time. Long strings are shortened.
Every 15 minutes in office hours, New York
curl 'https://util.caruana.me/api/cron/explain?expression=*%2F15%209-17%20*%20*%201-5&zone=America%2FNew_York&from=2026-10-13T16%3A50%3A00Z&count=3'
Response 200
{
"expression": "*/15 9-17 * * 1-5",
"normalized": "*/15 9-17 * * 1-5",
"macro": null,
"dialect": "five-field",
"description": "Every 15 minutes, between 09:00 and 17:59, Monday through Friday",
"day_rule": {
"rule": "day-of-week",
"explanation": "Only day-of-week (1-5) selects days; day-of-month is \"*\"."
},
"fields": [
{
"field": "minute",
"raw": "*/15",
"values": [
0,
15,
30,
45
],
"unrestricted": false,
"description": "every 15 minutes (0, 15, 30, 45)"
},
{
"field": "hour",
"raw": "9-17",
"values": [
9,
10,
11,
12,
13,
14,
15,
16,
17
],
"unrestricted": false,
"description": "hour 9 through 17"
},
{
"field": "day-of-month",
"raw": "*",
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
],
"unrestricted": true,
"description": "any day of the month"
},
{
"field": "month",
"raw": "*",
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12
],
"unrestricted": true,
"description": "every month"
},
{
"field": "day-of-week",
"raw": "1-5",
"values": [
1,
2,
3,
4,
5
],
"unrestricted": false,
"description": "Monday through Friday"
}
],
"timezone": "America/New_York",
"from": "2026-10-13T12:50:00-04:00",
"next": [
{
"iso": "2026-10-13T13:00:00-04:00",
"utc": "2026-10-13T17:00:00Z",
"unix_seconds": 1791910800,
"local": "2026-10-13T13:00:00",
"weekday": "Tuesday",
"dst_shifted": false
},
{
"iso": "2026-10-13T13:15:00-04:00",
"utc": "2026-10-13T17:15:00Z",
"unix_seconds": 1791911700,
"local": "2026-10-13T13:15:00",
"weekday": "Tuesday",
"dst_shifted": false
},
{
"iso": "2026-10-13T13:30:00-04:00",
"utc": "2026-10-13T17:30:00Z",
"unix_seconds": 1791912600,
"local": "2026-10-13T13:30:00",
"weekday": "Tuesday",
"dst_shifted": false
}
],
"warnings": []
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "cron_explain",
"arguments": {
"expression": "*/15 9-17 * * 1-5",
"zone": "America/New_York",
"from": "2026-10-13T16:50:00Z",
"count": 3
}
}
}Both day fields restricted: the 13th OR any Friday
curl 'https://util.caruana.me/api/cron/explain?expression=0%200%2013%20*%205&from=2026-11-01T00%3A00%3A00Z&count=4'
Response 200
{
"expression": "0 0 13 * 5",
"normalized": "0 0 13 * 5",
"macro": null,
"dialect": "five-field",
"description": "At 00:00, on day 13 of the month, and on Friday",
"day_rule": {
"rule": "either",
"explanation": "Both day-of-month (13) and day-of-week (5) are restricted, so a day matches if EITHER field matches (the Vixie cron OR rule), not only days that satisfy both."
},
"fields": [
{
"field": "minute",
"raw": "0",
"values": [
0
],
"unrestricted": false,
"description": "minute 0"
},
{
"field": "hour",
"raw": "0",
"values": [
0
],
"unrestricted": false,
"description": "hour 0"
},
{
"field": "day-of-month",
"raw": "13",
"values": [
13
],
"unrestricted": false,
"description": "day 13"
},
{
"field": "month",
"raw": "*",
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12
],
"unrestricted": true,
"description": "every month"
},
{
"field": "day-of-week",
"raw": "5",
"values": [
5
],
"unrestricted": false,
"description": "Friday"
}
],
"timezone": "UTC",
"from": "2026-11-01T00:00:00Z",
"next": [
{
"iso": "2026-11-06T00:00:00Z",
"utc": "2026-11-06T00:00:00Z",
"unix_seconds": 1793923200,
"local": "2026-11-06T00:00:00",
"weekday": "Friday",
"dst_shifted": false
},
{
"iso": "2026-11-13T00:00:00Z",
"utc": "2026-11-13T00:00:00Z",
"unix_seconds": 1794528000,
"local": "2026-11-13T00:00:00",
"weekday": "Friday",
"dst_shifted": false
},
{
"iso": "2026-11-20T00:00:00Z",
"utc": "2026-11-20T00:00:00Z",
"unix_seconds": 1795132800,
"local": "2026-11-20T00:00:00",
"weekday": "Friday",
"dst_shifted": false
},
{
"iso": "2026-11-27T00:00:00Z",
"utc": "2026-11-27T00:00:00Z",
"unix_seconds": 1795737600,
"local": "2026-11-27T00:00:00",
"weekday": "Friday",
"dst_shifted": false
}
],
"warnings": []
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "cron_explain",
"arguments": {
"expression": "0 0 13 * 5",
"from": "2026-11-01T00:00:00Z",
"count": 4
}
}
}Daily 02:30 across the spring DST gap (New York)
curl 'https://util.caruana.me/api/cron/explain?expression=30%202%20*%20*%20*&zone=America%2FNew_York&from=2026-03-07T00%3A00%3A00-05%3A00&count=3'
Response 200
{
"expression": "30 2 * * *",
"normalized": "30 2 * * *",
"macro": null,
"dialect": "five-field",
"description": "At 02:30",
"day_rule": {
"rule": "every-day",
"explanation": "Both day fields are \"*\" or \"?\": every day matches."
},
"fields": [
{
"field": "minute",
"raw": "30",
"values": [
30
],
"unrestricted": false,
"description": "minute 30"
},
{
"field": "hour",
"raw": "2",
"values": [
2
],
"unrestricted": false,
"description": "hour 2"
},
{
"field": "day-of-month",
"raw": "*",
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
],
"unrestricted": true,
"description": "any day of the month"
},
{
"field": "month",
"raw": "*",
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12
],
"unrestricted": true,
"description": "every month"
},
{
"field": "day-of-week",
"raw": "*",
"values": [
0,
1,
2,
3,
4,
5,
6
],
"unrestricted": true,
"description": "any day of the week"
}
],
"timezone": "America/New_York",
"from": "2026-03-07T00:00:00-05:00",
"next": [
{
"iso": "2026-03-07T02:30:00-05:00",
"utc": "2026-03-07T07:30:00Z",
"unix_seconds": 1772868600,
"local": "2026-03-07T02:30:00",
"weekday": "Saturday",
"dst_shifted": false
},
{
"iso": "2026-03-08T03:30:00-04:00",
"utc": "2026-03-08T07:30:00Z",
"unix_seconds": 1772955000,
"local": "2026-03-08T03:30:00",
"weekday": "Sunday",
"dst_shifted": true
},
{
"iso": "2026-03-09T02:30:00-04:00",
"utc": "2026-03-09T06:30:00Z",
"unix_seconds": 1773037800,
"local": "2026-03-09T02:30:00",
"weekday": "Monday",
"dst_shifted": false
}
],
"warnings": [
"Some run times fall in a DST gap in America/New_York; they are shown at the shifted wall-clock time (dst_shifted: true)."
]
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "cron_explain",
"arguments": {
"expression": "30 2 * * *",
"zone": "America/New_York",
"from": "2026-03-07T00:00:00-05:00",
"count": 3
}
}
}