# Natural language schedules

cronpad understands schedules written in plain English, like `every 15 minutes`, `every weekday at 6:30pm` or `second tuesday of every month at 9am`. The parser is deterministic (no AI involved): the same text always means the same thing. It is converted to an [RRULE](rrule-and-one-off.md), which is stored as the job's canonical schedule and shown back to you in plain English.

## Examples

### Intervals

| You write | Fires |
|---|---|
| `every 30 seconds` | Every 30 s (subject to the [minimum interval](../platform/limits.md)) |
| `every 15 minutes` | Every 15 minutes |
| `every hour`, `hourly`, `every 1 hour` | Every hour |
| `every 6 hours` | Every 6 hours |
| `every 10 minutes for 2 hours` | Every 10 minutes, stopping 2 hours after the start |

### Days and times

| You write | Fires |
|---|---|
| `every day at 9am`, `daily at 09:00` | 09:00 every day |
| `every day at 9am and 6pm` | 09:00 and 18:00 every day |
| `every weekday at 6:30pm` | 18:30 Monday–Friday |
| `every weekend at 10am` | 10:00 Saturday and Sunday |
| `every monday and thursday at 10:00` | 10:00 on Mondays and Thursdays |
| `every 2 weeks on friday at noon` | 12:00 every other Friday |
| `every hour between 9am and 5pm on weekdays` | 09:00, 10:00 … 17:00 Monday–Friday |

### Days of the month

| You write | Fires |
|---|---|
| `second tuesday of every month at 9am` | 09:00 on the 2nd Tuesday |
| `last friday of the month at 5pm` | 17:00 on the last Friday |
| `last day of every month at 23:59` | 23:59 on the 28th–31st, whichever is last |
| `first weekday of the month` | 00:00 on the first Monday–Friday |
| `on the 1st and 15th of every month at 8am` | 08:00 on the 1st and 15th |
| `every 3 months on the 1st`, `quarterly` | The 1st of every third month |
| `every year on march 31 at 10am` | 10:00 every 31 March |

### Limits

| You write | Effect |
|---|---|
| `every day at 9am 10 times` | Fires 10 times, then the job is `completed` |
| `every monday at 8am until 2027-01-01` | Stops after 1 January 2027 |

### One-off

| You write | Fires once |
|---|---|
| `in 20 minutes` | 20 minutes after you save |
| `tomorrow at 8am` | 08:00 tomorrow in the job's timezone |
| `at 2026-12-25 07:00` | That moment in the job's timezone |

## Grammar

Words are case-insensitive. Clauses after the frequency may appear in any order. Filler words (`the`, `of`, `on`, `at`, `each`, `and`, commas) are accepted wherever they read naturally.

```
schedule   := oneshot | recurring
oneshot    := "in" duration | "at"? datetime | relday "at" time
recurring  := frequency dayclause? timeclause? window? bound?
frequency  := "every" [N] unit | hourly | daily | weekly | monthly | quarterly | yearly
unit       := second(s) | minute(s) | hour(s) | day(s) | week(s) | month(s) | year(s)
dayclause  := weekday-list | ordinal weekday ["of" "every"? "month"] | ordinal "day" | monthday-list
              | "weekday" | "weekend"
timeclause := "at" time ("and" time)*
window     := "between" time "and" time | "from" time "to" time
bound      := N "times" | "until" date | "for" duration
ordinal    := first | 1st | second | 2nd | third | 3rd | fourth | 4th | fifth | 5th | last | second to last
time       := 9am | 9:30am | 09:30 | 21:00 | noon | midnight
relday     := today | tomorrow | weekday-name
```

## Defaults

- No time given for a day-based schedule → `00:00` in the job's timezone.
- `every N days/weeks/months` counts from the job's start time (`starts_at`, or when you created it).
- A month that doesn't have the requested day (the 31st in April, the 5th Friday) is skipped, as in RRULE. Use `last day of every month` for month-end.

## When parsing fails

The error points at the word it didn't understand and, when it can, suggests a correction (`tuesdy` → `tuesday`). If a schedule is too unusual for the grammar, write it as [cron, Quartz](cron-and-quartz.md) or [RRULE](rrule-and-one-off.md).

## Parsing limits and canonical form

Expressions are limited to 4096 UTF-8 bytes, recurrence intervals to 1,000,000
units, and occurrence counts to 100,000. These parser bounds keep invalid input
and finite-count checks bounded. Relative durations accept positive decimal
numbers with seconds, minutes, hours, days, or weeks; a day is 24 elapsed hours.
Time windows include their endpoints and must not end before their start on
the same day. Multiple times with different minute/second values, and partial-hour
windows, compile to a union of RRULE lines so they do not introduce unintended
cross-products of hours and minutes.