Skip to main content

Cron Expression Generator

Describe a schedule in plain language and get the matching cron expression.

“At every minute.”

next at 2026-10-02 03:04:00

(second)minutehourdaymonthweekday

Examples

Cron format: 5-field is minute hour day-of-month month day-of-week. Edit the cron box directly or describe a schedule below to update it.

Describe a schedule in plain English and get the cron expression, either in the standard five-field form or in six fields with a leading seconds column. Cron syntax is small enough to read at a glance and still manages to encode three behaviours that surprise people: the two day fields are combined with OR, step values reset at period boundaries, and nothing in an expression stops a slow job from overlapping itself.

Five fields, and the ranges that are not consistent

The fields run minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-7). Three are zero-based and two are one-based, which accounts for a good share of hand-written mistakes. There is no year field and no seconds field in standard cron, so one minute is the finest resolution available.

Day of week accepts both 0 and 7 for Sunday. POSIX specifies 0 through 6 with Sunday as 0; Vixie cron added 7 so that people arriving from systems that number Sunday last would not be caught out. Both values work in the same field, which means the range 0-7 covers seven distinct days, not eight.

Each field takes a single value, a list (1,15), a range (1-5), a step (*/15), or a step across a range (0-30/5). Month and day of week also accept three-letter names such as JAN and MON, but classic Vixie cron will not accept names inside a range or a list — MON-FRI is rejected where 1-5 is fine, and implementations differ on this.

Because there is no seconds field, this generator switches to a six-field expression with seconds in front when you ask for something like every 10 seconds, producing */10 * * * * *. That form is what Quartz and several application-level schedulers expect, and it will be rejected outright by a five-field parser such as the one in a Unix crontab or a Kubernetes CronJob. The shorthand macros — @hourly, @daily, @weekly, @monthly, @yearly, and the Vixie-only @reboot, which has no time semantics at all — are also not universally supported.

Day-of-month and day-of-week are combined with OR

When both the day-of-month and day-of-week fields are restricted, meaning neither is *, cron runs the job whenever either one matches. This is specified behaviour, stated in both POSIX and the crontab manual page, and it is almost never what the author intended.

0 0 13 * 5 does not mean midnight on Friday the 13th. It means midnight on the 13th of every month plus midnight every Friday: twelve dates plus fifty-two weekdays, so more than sixty runs a year rather than the one or two the author had in mind. The same trap catches "the 1st, but only if it is a weekday" and every variation of it.

The rule only applies when both fields are restricted, so 0 9 * * 1-5 (weekdays at nine) and 0 9 1 * * (the 1st at nine) are unambiguous — one field is a wildcard in each. There is no portable way to express AND in five-field cron. The standard workaround is to schedule the superset and test inside the job, running 0 0 13 * * with a guard such as [ "$(date +%u)" = 5 ] || exit 0 as the first line.

Quartz removes the ambiguity by requiring exactly one of the two fields to be ?, meaning "no specific value". Supplying * for both is an error there rather than a silent OR, which is one of the few places where the more complicated dialect is the safer one.

Step values restart at the boundary, they do not accumulate

*/n means "every value in this field divisible by n", evaluated inside that field's own range. It does not mean "n units after the previous run", and the difference shows up at every period boundary.

0 0 */7 * * fires on the 1st, 8th, 15th, 22nd and 29th, then starts over. In January the last run is the 29th and the next is 1 February — a three-day gap, not seven — and after twelve months the schedule bears no relation to any weekly rhythm. The 0 0 */3 * * that this tool emits for "every 3 days" behaves the same way: eleven runs in a 31-day month, and a gap of one to three days whenever the month rolls over.

The minute field has the identical problem. */25 * * * * runs at :00, :25 and :50, then jumps back to :00 — a ten-minute gap once every hour. A step only behaves as a true interval when it divides its field evenly, which in the minute field means 5, 10, 15, 20 or 30. If you need a genuine fixed interval, cron is the wrong tool: use a systemd timer with OnUnitActiveSec=, or have the job enqueue its own next run.

A related edge on the calendar side: 0 0 31 * * simply does not run in months that have no 31st, so it fires seven times a year rather than twelve. Quartz has an L token for the last day of the month; in plain cron the usual fix is to schedule on the 1st and operate on the previous month.

The same five fields mean different things in different schedulers

Quartz uses six required fields with seconds first and an optional seventh for the year, and its day-of-week is numbered 1 to 7 with Sunday as 1. A Unix 5 meaning Friday is a Quartz 6, so an expression copied between the two runs a day early and nothing complains. Quartz also adds L for last, W for the nearest weekday, and # for the nth weekday of a month, so 0 0 12 ? * 6#3 is noon on the third Friday.

Kubernetes CronJob takes five fields, and its most consequential behaviour lives outside the expression. concurrencyPolicy defaults to Allow, so a job that runs long will overlap itself unless you set Forbid or Replace. If the controller finds more than 100 missed schedules — after a control-plane outage, typically — it stops scheduling entirely and logs an error rather than catching up. And without an explicit .spec.timeZone, the schedule is interpreted in the kube-controller-manager's zone, which is usually UTC and rarely the one written on the ticket.

GitHub Actions accepts POSIX five-field syntax, always evaluates it in UTC, refuses intervals shorter than five minutes, and documents that scheduled runs can be delayed during periods of high load. 0 * * * * there means "somewhere in that hour", not "on the hour". Scheduled workflows are also disabled automatically after 60 days without repository activity.

One portability trap has nothing to do with the fields at all. In a crontab file, an unescaped % in the command is converted to a newline, and everything after the first one is handed to the job as standard input. date +%Y-%m-%d in a crontab therefore executes date + and quietly produces nothing useful. Escape every percent sign as \%, or move the command into a script.

Daylight saving, and the fact that cron never waits

Any schedule between roughly 01:00 and 03:00 local time misbehaves twice a year. In a spring-forward transition that local hour does not exist; in an autumn transition it happens twice. What your scheduler does about that is implementation-defined, and the implementations genuinely disagree.

Vixie-derived cron on Debian and Red Hat special-cases fixed-time jobs: a job that would have fallen inside a skipped interval runs once immediately after the jump, and a job inside a repeated interval runs only once. Jobs with an interval shorter than an hour, such as */10 * * * *, are deliberately not special-cased and simply follow the clock, so they lose six runs in spring and gain six in autumn. Schedulers that merely compare a wall clock every minute do neither: the 02:30 job is skipped in March and executes twice in November.

Run the scheduler in UTC and convert inside the job if a report needs local dates. If a job genuinely must fire at a fixed local time, keep it out of the midnight-to-04:00 window, where every one of these behaviours lives.

Independently of any of that, cron has no concept of a previous run still executing. If */5 * * * * takes seven minutes, you get two copies, then three, then a machine under load making each run slower still. No expression prevents this. Wrap the command in a lock — flock -n /var/lock/job.lock command is one line and solves it — or set concurrencyPolicy: Forbid on Kubernetes.

Finally, check the environment before you blame the schedule. Vixie cron runs jobs with a minimal PATH (commonly just /usr/bin:/bin), no shell profile, and SHELL=/bin/sh. A script that works in your interactive shell and does nothing under cron has usually failed on a missing binary rather than a mis-typed field.

ThenCatch is a free developer toolkit. Learn more about us.