The 3 a.m. page that shouldn't have happened

The pager went off at 4:30 on a Friday morning in October. The on-call engineer stared at a cleanup job mid-execution and at its crontab line — `30 4 1,15 * 5` — with the specific dread of a person discovering their own signature on the problem. The job was supposed to run on the 1st and 15th of the month. It was October 30th. A Friday. What the expression actually meant, in the dialect of standard cron, was 'the 1st, the 15th, and every single Friday' — and that distinction had been quietly running a production job three extra days a month since summer.

That bug is not rare. It is the most common cron mistake in the world, and it survives because cron's day-of-week semantics are genuinely counterintuitive: restrict both day fields and the daemon treats them as alternatives, not conditions. Five fields, ten seconds to write, months of subtle damage. The only real defenses are knowing the semantics — or never writing the five fields by hand at all.

The Cron Expression Builder above is the second defense. It assembles expressions from visual fields and presets, translates whatever you build into plain English, and — the part that catches every bug in this story — computes the next run times from your device clock, so a schedule that fires on unexpected Fridays announces its intentions before it pages anyone. Build the expression here, verify the next runs, then paste it into crontab.

Building a schedule without memorizing syntax

  1. Start from a preset

    'Every 5 minutes,' 'weekdays at 9 a.m.,' 'first of the month at midnight,' 'Sunday 2:30 a.m.' — presets cover the majority of real schedules and give you a correct base to edit.

  2. Dial in each field

    Five rows — minute, hour, day of month, month, day of week — each with values, ranges (1-5), steps (*/10), and lists (1,15). The builder writes the expression as you click; you never type a character unless you want to.

  3. Read the human translation

    Every change updates a plain-English sentence: 'At 09:00 on every day-of-week from Monday through Friday.' If that sentence is not the schedule in your head, the expression is wrong — and now you know before production does.

  4. Check the next run times

    The tool lists the next several execution times computed from your current clock. This is the Friday-bug detector: an accidental day-of-week match shows up in the next-7-days list in plain view.

  5. Test the edge you're worried about

    Month-end? Leap years? The run-list spans far enough to show whether '0 0 L * *' style expressions and month boundaries behave the way you assume.

  6. Copy into crontab

    Paste the finished expression into `crontab -e` on your machine, or into your scheduler's config — Kubernetes CronJob, GitHub Actions schedule, Cloud Scheduler all speak this dialect, with dialect caveats covered below.

Five fields, five ranges, one notorious day

A standard cron expression is five fields, separated by spaces, each constraining one axis of time:

FieldAllowed valuesNotes
Minute0–590 is the top of the hour
Hour0–2324-hour clock, no AM/PM
Day of month1–31Months without day 31 simply never match it
Month1–12, or JAN–DECNames take the first three letters
Day of week0–6, or SUN–SATSunday is 0 or 7 — both valid
The five fields, left to right, in every standard cron expression.

Within a field, four syntaxes cover everything: a single value (9), a range (1-5), a list (1,15), and a step (*/10, or 10-50/5). Combinations are legal — `0 9 1-7 * 1` is 'nine o'clock on days 1–7 of the month that are Mondays,' which people use to approximate 'first Monday,' and which works only because of the semantics we are about to address. A field containing just * matches every value and, critically, marks that field as unrestricted.

Now the notorious part. Standard cron — the Vixie family that runs on Linux, macOS, and the BSDs — resolves the two day fields with this rule: if both day-of-month and day-of-week are restricted, the job fires when either matches. If either field is *, the restricted one governs alone. This is an OR where every human instinct expects an AND. The rationale is decades old and mildly regretted: the two day fields serve different scheduling styles, and the daemon refuses to guess which one you meant when you fill both. The consequence is the Friday-bug from the opening story, the 3 a.m. page, and a small permanent industry of cron-explainer pages — this builder's next-run list exists precisely because a plain-English translation alone doesn't always make the OR visible.

There are shorthand strings, too, worth having in your pocket: `@yearly`/`@annually` (0 0 1 1 *), `@monthly` (0 0 1 * *), `@weekly` (0 0 * * 0), `@daily`/`@midnight` (0 0 * * *), `@hourly` (0 * * * *), and `@reboot`, which runs once when the cron daemon starts rather than on any schedule. They expand to exactly the expressions shown, so you can use them interchangeably where crontab syntax is expected.

Three schedules, decoded with real next-run times

The builder's run-list is its most valuable feature, so here is what it looks like in practice. These examples are computed from a real reference point — Sunday, October 25, 2026, mid-afternoon — and every date below is correct for that anchor.

Every weekday at 9 a.m.

Input

Expression: 0 9 * * 1-5

Output

Next runs: Mon 26 Oct 2026 09:00 · Tue 27 Oct 09:00 · Wed 28 Oct 09:00 · Thu 29 Oct 09:00 · Fri 30 Oct 09:00

Note the day-of-week range 1-5, Monday through Friday. The 0 minute keeps it off the :00 collision — see the staggering note below.
First of the month, at midnight

Input

Expression: 0 0 1 * *

Output

Next run: Sun 1 Nov 2026 00:00 · then 1 Dec 2026 00:00 · 1 Jan 2027 00:00

Day-of-week is *, so the single restricted day field governs alone — no OR surprises possible.
The Friday bug, demonstrated

Input

Expression: 30 4 1,15 * 5

Output

Next runs: Fri 30 Oct 2026 04:30 · Sun 1 Nov 2026 04:30 · Sun 15 Nov 2026 04:30

That first run is the trap: a Friday that is neither the 1st nor the 15th. Both day fields restricted → OR semantics → the job fires. The run list makes the bug visible a week before it pages you.

One production habit worth stealing from sysadmins with scar tissue: avoid scheduling everything at minute 0. Every cluster on earth has a dozen jobs at `0 * * * *`, and they thunder into contention on the hour, every hour. Spreading jobs across odd minutes — `7 */2 * * *`, `23 3 * * *` — costs nothing and quietly removes a whole class of load-spike mysteries. The builder's per-field dropdowns make that staggering a two-second decision instead of a mental arithmetic chore.

The sharp edges nobody warns you about

Beyond the OR rule, cron has a small set of documented-but-undiscovered behaviors. Each of these has cost somebody a night's sleep:

  • The percent sign is a newline. In a crontab command, the first unescaped % starts the command's standard input and becomes a newline; every later % becomes another newline. `date +%F` truncates at the %; `date +\%F` works. Escape every % in cron commands and you will never meet this one.
  • The environment is a desert. Cron jobs run with a minimal environment — a short PATH, no shell profile, often no locale you expect. Scripts that work perfectly in your interactive shell fail in cron because /usr/local/bin isn't on PATH. Absolute paths everywhere, or set PATH explicitly at the top of the crontab.
  • Missed runs stay missed. Cron matches the current minute; it does not replay history. A machine asleep or powered off at the scheduled minute simply skips the job. anacron exists for this; systemd timers have Persistent= for it.
  • Time zones bite twice. The daemon's TZ decides when '9 a.m.' happens — usually the server's local zone, configurable per-line in modern Vixie cron with CRON_TZ, and settable to UTC in containers where 'local' is a fiction. And DST transitions can double-fire or skip an hour for jobs landing in the 01:00–03:00 window.
  • Sunday is 0 and 7, but Quartz disagrees. Port an expression to a Java system and 7 stops being Sunday — Quartz's day-of-week runs 1–7 with Sunday as 1. Any port between dialects deserves a fresh read-through of the next section.
  • Names don't step. You can write JAN or MON in the month and weekday fields, but combine names with ranges or steps in most implementations and the parser will decline, sometimes unkindly.

A worked crontab: six lines from a production box

Syntax tables teach fields; a real crontab teaches judgment. Below is the scheduled-jobs file from a small production server, cleaned of anything incriminating, with every line written the way it is for a reason. Read it as six small decisions rather than six strings.

# daily backup at 02:17 — off the hour, off the :00
17 2 * * * /usr/local/bin/backup-db.sh >> /var/log/backup.log 2>&1
# temp cleanup on the 1st at 04:33
33 4 1 * * /usr/local/bin/cleanup-temp.sh
# cache warm on weekday mornings at 09:07
7 9 * * 1-5 /usr/local/bin/warm-cache.sh
# invoice run on the 1st at midnight
0 0 1 * * /usr/local/bin/invoice-run.sh
# health probe every five minutes
*/5 * * * * /usr/local/bin/health-probe.sh
# month-start and mid-month report — the Friday bug, repaired
30 4 1,15 * * /usr/local/bin/report.sh
LineWhat it doesWhy it is written that way
17 2 * * *Database backup daily at 02:17Odd minute avoids the 02:00 contention; output redirected to a log so cron never mails a line of it
33 4 1 * *Temp cleanup on the 1st at 04:33Day-of-week left as *, so the one restricted day field governs alone — no OR semantics possible
7 9 * * 1-5Cache warm weekdays at 09:07Range 1-5 in day-of-week; minute 7 dodges the pile-up every cluster has at 09:00
0 0 1 * *Invoice run on the 1st at midnightThe single job permitted at :00, because nothing else in this file shares its minute
*/5 * * * *Health probe every 5 minutesStep value in the minute field; the overlap guard lives inside the script, not the schedule
30 4 1,15 * *Report on the 1st and 15th at 04:30The opening story's job, repaired: day-of-week is *, so Fridays can never ambush it
Six lines, six deliberate choices. The repaired report line is the one that paged an engineer in October.

The probe line hides the one failure mode this file still has to defend against by hand: overlapping runs. A job scheduled every five minutes that takes seven minutes will find its second copy starting while the first is still running, and the two will race each other for the same resources every single invocation after that. Cron has no built-in guard, so the script takes one — the flock pattern wraps the work in a lock that fails fast when the previous run still holds it: `flock -n /tmp/probe.lock /usr/local/bin/health-probe.sh`. The -n flag means non-blocking: if the lock is taken, this run exits quietly instead of queueing a zombie. Database migrations and file syncs want the same treatment, whatever the scheduler.

One more boundary case this file sidesteps by not existing: day-of-month 31. A job written `0 0 31 * *` fires in the seven 31-day months and never in February, April, June, September, or November — a schedule that quietly loses five months a year. Standard cron has no last-day-of-month token (Quartz's L exists precisely for this), so the standard workaround runs on days 28 through 31 and lets the script itself check whether tomorrow is the 1st before doing anything. The builder's next-run list surfaces the gap immediately: ask it for a month of runs on the 31st pattern and watch February return nothing.

Sysadmins, schedulers and the Quartz crowd

Who builds cron expressions? The honest answer is 'almost everyone who operates software,' sliced into a few distinct habits.

Sysadmins are the natives — they speak five-field cron fluently, and what they want from a builder is speed and the next-run list as a verification step, not education. Backend and platform developers meet cron through Kubernetes CronJobs, GitHub Actions schedules, and cloud schedulers, all of which accept standard five-field expressions — and all of which fail differently from a Linux crontab when day semantics or time zones are involved, which sends them searching. Data engineers schedule pipelines and dbt jobs and live in mortal fear of the double-run at midnight UTC boundaries. Java teams live on Quartz and must consciously translate between dialects — the comparison below is for them. And students and junior engineers encounter cron in the same place everyone always has: the week something must 'run automatically every night,' which is how a generation has met the OR rule since 1975. Demand for builders and explainers of cron has been continuous for as long as search engines have measured it; the tool category is about as evergreen as software gets.

Cron vs. Quartz: five fields against seven

PropertyStandard (Vixie) cronQuartz
Field count56–7 (seconds first, optional year)
Day-of-week numbering0–6, Sunday = 0 or 71–7, Sunday = 1
Both day fields restrictedOR — fires when either matchesAND-style with ? disambiguation
The ? characterNot usedMarks 'no specific value' in one day field
Names with steps/rangesGenerally not allowedAllowed (e.g., MON-FRI)
Natural habitatLinux, macOS, BSD crontabs; most cloud schedulersJava applications, Spring scheduling
Same ancestor, divergent dialects — porting expressions between them without checking this table is how bugs are born.

The deepest difference is the one the opening story was about. Quartz resolves the two day fields with AND semantics — you can set both and it means both — and provides the `?` character to explicitly mark a field as unset. Standard cron's OR is the historical accident Quartz was designed to fix. So the same string means different things in the two dialects whenever both day fields are restricted, and nothing else about the expression warns you. This builder produces standard five-field cron; if your destination is Quartz, translate deliberately, and if your team runs both dialects in the same stack, write the dialect on the wall next to the schedule.

The builder runs in your tab

Expression assembly, the plain-English translation, and the next-run computation all happen in your browser. There is no server round-trip for any of it, which is why the translation updates as you click rather than after a spinner.

Next-run computation is worth a sentence on method, since its correctness is the whole point: the tool evaluates your expression against the minute grid the same way a daemon would, starting from your device clock, and lists the matches it finds. That makes it an honest simulation of standard Vixie semantics — including the OR rule — rather than a dictionary of common patterns. The privacy consequence is the same as every tool in this cluster: schedules are internal infrastructure information, and a builder that ships them to a server for evaluation is creating a record of your systems' timing for no architectural reason. This one keeps your schedules exactly where they started.

Cron questions that come up weekly

What does */5 mean in a cron expression?

It means 'every 5 units within the field's range.' In the minute field, */5 fires at 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, and 55 past the hour. The syntax is step values: range/step. Writing 10-50/5 would fire every 5 minutes but only between 10 and 50. A plain * means every value in the field.

Is Sunday 0 or 7 in the day-of-week field?

Both. Standard (Vixie) cron accepts 0 and 7 as Sunday — a deliberate compatibility kindness inherited from historical implementations. Monday through Saturday are 1 through 6. Day names work too: SUN, MON, TUE, and so on, case-insensitive. Quartz, by contrast, uses 1 for Sunday, which is the source of endless porting bugs.

Why did my cron job run on a day I didn't ask for?

Almost always the day-of-month/day-of-week rule: when BOTH fields are restricted (neither is *), standard cron fires the job when either one matches — an OR, not an AND. The expression '30 4 1,15 * 5' does not mean 'the 1st and 15th when they're Fridays.' It means 'the 1st, the 15th, and every Friday.' To restrict to one field, leave the other as *.

What does @reboot do in crontab?

It runs the command once, at cron's startup, instead of on a schedule — after a reboot or after the cron daemon restarts. It is one of the shorthand strings alongside @yearly (0 0 1 1 *), @monthly (0 0 1 * *), @weekly (0 0 * * 0), @daily or @midnight (0 0 * * *), and @hourly (0 * * * *). Note that @reboot timing is daemon-dependent — jobs may run before the network is fully up.

Can I use day and month names like MON and JAN?

Yes — the first three letters, case-insensitive: MON, TUE, JAN, FEB. Names work in the month and day-of-week fields anywhere a number works in standard cron. What you cannot do is combine names with step values or ranges in most implementations — 1-5 style ranges and */2 steps want numbers.

Why did my cron command break the moment I added a % sign?

Because in crontab files, an unescaped % is special: the first % starts the command's standard input and becomes a newline, and every subsequent % becomes another newline. A command like date +%Y-%m-%d silently truncates at the % . Escape it with a backslash — date +\%Y-\%m-\%d — and it works. This is the most-Googled cron surprise after the Sunday question.

Does cron catch up on jobs missed while the machine was off?

No. Standard cron evaluates the schedule at each minute boundary and runs what matches; missed opportunities are simply gone. A laptop asleep at 03:00 misses its 03:00 job until the next day. If you need catch-up semantics, look at anacron (designed for machines that are not always on) or a scheduler with missed-run policies.

Is a cron expression the same in Quartz?

No, and porting them blindly is a classic bug factory. Quartz uses six or seven fields — seconds first, optional year last; day-of-week runs 1–7 with Sunday as 1; both day fields can be set and combine with AND-like semantics; the ? character marks 'no specific value'; and names can take steps. The five-field expressions this builder produces are for standard Unix cron, crontab.guru-style tooling, and Vixie-compatible daemons.

Tools that live next door

The schedule-adjacent toolkit on this site is deliberately deep. When you already have an expression and need it decoded rather than built, the Cron Expression Explainer is the reverse operation — paste the five fields, get the semantics and next runs. For the other repo-infrastructure files that pair with automation, the .gitignore Generator and its neighbor in your first commit are covered elsewhere on this page's cluster. And when the scheduled job's failure mode is an HTTP problem — a webhook, a health check, a 429 that started everything — the HTTP Status Code Lookup decodes what came back.

Build here, explain there, and debug the response codes when the run happens. That trio covers most of the scheduled-job lifecycle.

Canonical references

Cron's semantics are documented at the source. These four references define everything this builder implements:

Sources and further reading

We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.