The most expensive cron expression I ever wrote was 0 0 * * 0. It ran a weekly digest email for a Laravel SaaS I was building, and I was absolutely certain it meant "midnight on the last day of the week." It means midnight Sunday. My mental model said the week ended Saturday. For five weeks, customers got their "week in review" email a day late, and nobody on the team caught it because nobody on the team could read cron either β we all just squinted at the five fields and nodded.
That's the dirty secret of cron syntax: almost everyone who writes it is pattern-matching from a previous expression they half-remember. The format is forty-plus years old, dense enough that a single character changes the schedule entirely, and it fails silently. There's no compiler error for "runs on the wrong day." The job just runs on the wrong day, forever, until someone notices.
A cron expression parser closes that gap. You paste the expression, and it tells you in plain English what will actually happen β 0 0 * * 0 comes back as "At 00:00, on Sunday" β plus when the next runs will fire. That readback step is the difference between shipping a schedule and shipping a guess. I built the one on toolz.dev because I got tired of context-switching to a terminal, and because the wp-cron mess I dealt with for years in WP Adminify taught me that scheduling bugs are the most patient bugs in software.
This guide covers how to use the parser, how the five fields actually work (including the two quirks that cause most production incidents), and where cron syntax differs across crontab, GitHub Actions, Quartz, and Laravel.
TL;DR: Paste any crontab expression into the toolz.dev Cron Parser and get a plain-English translation plus upcoming run times β instantly, client-side, no signup. Before you deploy a schedule, always verify two things: the day-of-week numbering (0 and 7 are both Sunday) and the timezone the scheduler runs in (GitHub Actions is always UTC). Pair it with the Timestamp Converter when you need to translate those next-run times across zones, and the Date Difference Calculator to sanity-check intervals.
Key Features
Plain-English Translation
The core job of the parser is turning */15 9-17 * * 1-5 into "Every 15th minute past hour 9, 10, 11, 12, 13, 14, 15, 16, and 17, on Monday, Tuesday, Wednesday, Thursday, and Friday." It's verbose β it enumerates rather than collapsing "9 through 17" back into a range β and that verbosity is the point. Enumeration is unambiguous; a summarized range is another chance to misread. The sentence is something you can paste into a pull request description, read aloud in a standup, or show a non-technical stakeholder. The raw expression is not. In my experience the translation matters most during code review: a reviewer who would rubber-stamp five cryptic fields will immediately catch "wait, why does hour 17 appear when the job is supposed to stop at 5 PM?" when every hour is spelled out. The translation makes the schedule falsifiable, which is exactly what raw cron syntax fails at.
Next Run Preview
Knowing what an expression means is half the problem; knowing when it fires next is the other half. The parser computes the next five execution times so you can eyeball them against your intent. This is where subtle mistakes surface β an expression that reads fine in English but produces a next-run of "in 27 days" because you confused day-of-month with month, or one that fires at 03:00 tonight when you meant 03:00 after the weekend. I check the next-run list every single time now, even for expressions I'm confident about. Especially for expressions I'm confident about β see the opening anecdote.
Two details about how those times are computed. First, they're evaluated in your browser's local timezone, not UTC and not your server's zone β each run is shown twice, once as a local timestamp and once as the equivalent UTC instant, so you can read whichever matches your deployment target. Second, when both day-of-month and day-of-week are restricted, the parser applies real cron's OR semantics rather than AND: 0 0 1 * 1 fires on the 1st of the month and on every Monday, not only on Mondays that fall on the 1st. That rule trips up experienced people, and seeing five concrete dates makes it obvious in a way the sentence never does.
One rough edge: an impossible expression like 0 0 30 2 * (February 30th) parses as valid, describes itself happily as "At 00:00, on day-of-month 30, in February," and then shows an empty next-runs list. Empty means never. It's correct, but it's quiet β a "this schedule will never fire" warning is the obvious improvement and I haven't built it yet.
Field-by-Field Breakdown
The parser splits the expression into its five components β minute, hour, day of month, month, day of week β and shows what each one contributes. This matters because cron errors are almost always a single-field problem: the right value in the wrong column. 0 12 * * * (noon daily) and 12 0 * * * (12:12 AM daily... no, 00:12 daily) are one swap apart. Seeing "hour: 0" labeled explicitly is how you catch that swap in two seconds instead of two weeks.
Support for Ranges, Steps, and Lists
Real-world expressions lean hard on the operator syntax: 1-5 ranges, */10 steps, 1,15 lists, and combinations like 0 8-18/2 * * 1,3,5. The parser handles all of it, including the combined forms that trip up human readers. Step values over ranges β 8-18/2 meaning "every 2 hours from 8 to 18" β are legal, useful, and nearly unreadable without tooling. If you've ever inherited a crontab full of these from a departed sysadmin, you know why this feature exists.
Strict Five-Field Validation (Including What It Won't Accept)
The parser takes standard five-field expressions and nothing else. Paste @daily and you get an error β "Expected 5 fields (minute hour day-of-month month day-of-week), got 1" β not a translation. Same for @hourly, @weekly, and @reboot. That's a genuine gap rather than a design principle, and I'd rather say so than let you find out mid-debug; the shorthand macros are common enough in real crontabs that they belong in the tool. Until they're in, translate manually: @hourly is 0 * * * *, @daily is 0 0 * * *, @weekly is 0 0 * * 0, @monthly is 0 0 1 * *, @yearly is 0 0 1 1 *. @reboot has no five-field equivalent at all β it isn't a schedule, it runs once at daemon startup, a fact that has surprised plenty of people running migrations from crontab.
The strictness pays off elsewhere. A six-field Quartz expression is rejected with a field count instead of being silently misread. Out-of-range values name the field and the legal range (Value 25 out of range for hour (allowed 0-23)). Reversed ranges like 5-1 are caught. And it accepts the things real crontabs contain: month and day names (JAN, SUN), 7 as a second spelling of Sunday, and the Vixie 5/15 form meaning "every 15 starting at 5". Worth knowing while you're translating those macros by hand: every @daily job on a server fires at the same instant β midnight β so forty of them is a nightly load spike. I scatter mine across odd minutes (17 3 * * *, 43 4 * * *) for exactly that reason.
Client-Side Processing
The parser runs entirely in your browser. Nothing you paste is uploaded, logged, or stored. That sounds like boilerplate privacy language until you remember what crontabs actually contain: your backup schedule, your billing-run timing, the exact minute your security scans fire. Infrastructure scheduling is reconnaissance data. Keeping it off other people's servers isn't paranoia; it's just not creating a problem where none needs to exist.
How to Use the Cron Parser
Step 1: Paste or Type Your Expression
Open the Cron Parser and drop in the expression β from a crontab file, a schedule: block in a GitHub Actions workflow, a Kubernetes CronJob manifest, or a Laravel ->cron() call. Standard five-field syntax works as-is; @daily and its siblings don't, so expand those to five fields first. Then hit Parse. If you're starting from scratch rather than decoding, the preset buttons (Every minute, Hourly, Daily at midnight, Weekdays 9am, 1st of month) load a working expression you can modify field by field, re-parsing as you go.
Step 2: Read the Translation Back
This is the step people skip and shouldn't. Read the plain-English output and compare it against the sentence in your head. If you wrote the expression intending "every Monday at 9 AM" and the readback says "At 09:00 on day-of-month 1" β congratulations, you just caught the classic column swap before production did. The readback is your unit test.
Step 3: Verify the Next Run Times
Check the five upcoming executions. Do the dates land where you expect? Is the first run tonight, tomorrow, or next month? Pay attention to the gap between runs β a misplaced */ step turns "every 6 hours" into "every minute of every 6th hour" (* */6 * * * vs 0 */6 * * *), and five runs one minute apart instead of six hours apart is hard to miss. An empty list means the schedule can never fire.
Step 4: Account for Timezone Before Deploying
The parser tells you when relative to a clock; your scheduler decides whose clock. Before you deploy, confirm what timezone the executing system uses. GitHub Actions: always UTC, no exceptions. Servers: whatever the OS is set to, frequently UTC on cloud boxes. Laravel: your app timezone, unless you chain ->timezone(). Translate a next-run time through the Timestamp Converter if you need to see it in your local zone or a customer's.
Technical Deep Dive: How Cron Expressions Actually Work
The five-field format comes from Unix cron, standardized in practice by Paul Vixie's cron implementation in the late 1980s β the one documented in crontab(5) and still shipping, in descendant form, on most Linux systems today. The fields, left to right:
| Field | Allowed values | Notes |
|---|---|---|
| Minute | 0β59 | |
| Hour | 0β23 | 24-hour clock, 0 is midnight |
| Day of month | 1β31 | Beware months without a 31st |
| Month | 1β12 or JANβDEC | Names allowed in Vixie cron |
| Day of week | 0β7 or SUNβSAT | 0 and 7 are both Sunday |
Each field accepts * (any value), lists (1,15), ranges (1-5), and steps (*/10 or 20-59/5). That's the whole grammar. The complexity isn't in the syntax β it's in three behavioral quirks.
Quirk one: the day-of-week zero. Per crontab(5), both 0 and 7 mean Sunday. Some older or stricter implementations only accept 0. Quartz β the Java scheduler used by Jenkins and half of enterprise software β numbers days 1β7 starting at Sunday, so Quartz's 2 is Monday while crontab's 2 is Tuesday. If you ever migrate schedules between systems, this off-by-one is lying in wait. Always parse, never transcribe.
Quirk two: day-of-month OR day-of-week. Here's the one almost nobody knows until it bites them. When both the day-of-month and day-of-week fields are restricted (neither is *), Vixie cron runs the job when either matches β an OR, not an AND. So 0 0 13 * 5 doesn't mean "Friday the 13th." It means "every 13th of the month AND every Friday." This is documented behavior in crontab(5) and it is deeply counterintuitive. If you genuinely need "Friday the 13th," you need a script-side date check or a scheduler with richer syntax.
Quirk three: timezones and DST. Cron has no timezone field. The expression is interpreted in the scheduler's local time, whatever that is. Two concrete consequences:
- GitHub Actions runs
schedule:triggers in UTC, full stop. A workflow scheduled at0 9 * * *fires at 9 AM UTC β 4 or 5 AM in New York depending on the season, because UTC doesn't observe DST but your intended audience's clock does. Your "9 AM daily report" drifts by an hour twice a year unless you adjust the workflow or handle it in code. - On servers set to a DST-observing zone, one night a year the 02:00β03:00 hour doesn't exist, and one night it happens twice. A job scheduled at 02:30 either skips or double-fires depending on the implementation. The boring, correct fix: schedule critical jobs outside 01:00β03:00 local, or run servers on UTC. I do both.
Extended formats. Quartz uses six or seven fields (a leading seconds field and an optional trailing year), plus extra operators like L (last), W (nearest weekday), and # (nth weekday of month). Some crons support a leading seconds field too. If your expression has six fields and you're not sure which dialect it is, paste it into the parser β a six-field expression interpreted as five-field will produce visibly wrong output, which is itself diagnostic. And @reboot, the odd duck of the special strings, isn't a schedule at all: it runs once at daemon startup, which on modern systems means "whenever the box restarts," a fact that has surprised many people running database migrations from crontab.
For a broader tour of the developer utilities that pair with scheduling work, the coding tools guide covers the whole toolbox.
Common Use Cases
Debugging a Laravel Scheduler Entry
Laravel's scheduler wraps cron in fluent methods β ->dailyAt('03:00'), ->weeklyOn(1, '8:00') β but the escape hatch, ->cron('*/5 * * * 1-5'), is raw crontab syntax, and complicated schedules end up there. The system crontab runs schedule:run every minute, and Laravel decides internally what's due. When a scheduled command isn't firing, my first move is pasting the ->cron() string into the parser to confirm it means what the comment above it claims. About half the time, it doesn't. The other half, the bug is timezone: the app is set to UTC while the developer assumed local time. The parser resolves the first case in seconds and points a finger at the second.
Untangling wp-cron on WordPress Sites
WordPress ships with wp-cron, which isn't cron at all β it's a pseudo-scheduler that piggybacks on page visits, so a low-traffic site's "hourly" job might run every four hours, and a high-traffic site pays a small tax on every request. During my WP Adminify years this generated a steady stream of "scheduled posts aren't publishing" reports. The standard fix is disabling wp-cron (DISABLE_WP_CRON) and triggering wp-cron.php from a real server crontab β at which point you're writing actual cron expressions, usually */5 * * * *, and the parser earns its keep verifying them. If you run WordPress at any scale, this migration is worth doing this week, not someday.
Verifying GitHub Actions Schedules
CI schedules fail quietly: a nightly build that stops running doesn't page anyone. When writing a schedule: trigger, I parse the expression, look at the next-run times, then mentally add the UTC offset. Two extra details specific to Actions: schedules only run on the default branch, and runs can be delayed or dropped during high-load periods β GitHub's own docs say so. If exact timing matters, cron-triggered Actions are the wrong tool; if approximate timing is fine, at least make the approximate time the right approximate time.
Auditing an Inherited Crontab
Every long-lived server accumulates a crontab written by people who no longer work there. Running crontab -l and pasting each line through the parser is the fastest audit I know: within ten minutes you have a plain-English schedule inventory, and you will almost always find at least one job doing something nobody remembered β a backup running twice, a cleanup script that never matched the day it was supposed to, a * * * * * that should have been 0 * * * * hammering an API sixty times an hour. When comparing an old crontab against a new one during a migration, the Text Diff tool alongside the parser makes the review mechanical.
Scheduling Kubernetes CronJobs
K8s CronJobs use standard five-field syntax and, since 1.27, support an explicit timeZone field β a genuine improvement over classic cron. The parser workflow is the same: verify the expression, check next runs, then confirm startingDeadlineSeconds and concurrencyPolicy cover the failure modes cron itself doesn't. An expression can be perfect and the job still pile up if a slow run overlaps the next trigger; the parser gets the schedule right so you can spend your attention on those operational settings instead.
Cron Dialects Compared
| Vixie cron / crontab | GitHub Actions | Quartz | Laravel Scheduler | systemd timers | |
|---|---|---|---|---|---|
| Fields | 5 | 5 | 6β7 (seconds, year) | 5 (via ->cron()) |
OnCalendar syntax, not cron |
| Day-of-week numbering | 0β7 (0 and 7 = Sun) | 0β6 (0 = Sun) | 1β7 (1 = Sun) | Follows crontab | Names (Mon, Tue) |
| Timezone | System local | Always UTC | Configurable | App timezone or ->timezone() |
System local or Timezone= |
| Special strings | @daily, @reboot, etc. |
Not supported | Not supported | Fluent methods instead | OnBootSec=, calendar shorthands |
| Seconds precision | No | No (min ~5 min practical) | Yes | No (per-minute tick) | Yes |
| Best for | Server jobs | CI/CD schedules | JVM ecosystems | Laravel apps | Modern Linux services |
When to use which: crontab for plain server jobs, systemd timers when you want logging and dependency handling for free on modern Linux, the framework scheduler when the job lives inside your app anyway, and Actions schedules only for CI tasks that tolerate fuzzy timing. Whichever you pick, the five-field expression is the lingua franca β which is why a parser that speaks it fluently belongs in your bookmarks next to the rest of your web developer toolkit.
FAQ
What is a cron expression parser?
A cron expression parser is a tool that reads crontab syntax β like */15 9-17 * * 1-5 β and translates it into a plain-English schedule description, typically alongside a preview of the next execution times. It lets you verify what a schedule actually does before deploying it, instead of discovering a misread field when a job fires at the wrong time in production.
What do the five fields in a cron expression mean?
Left to right: minute (0β59), hour (0β23), day of month (1β31), month (1β12), and day of week (0β7, where both 0 and 7 mean Sunday). Each field accepts * for any value, comma lists, hyphen ranges, and / step values. So 30 2 1 * * means 02:30 on the first day of every month.
Why does my cron job run at the wrong time?
The two most common causes are timezone and field confusion. Cron runs in the scheduler's local time β GitHub Actions always uses UTC, and many cloud servers do too β so a job scheduled for "9 AM" may fire hours off from your wall clock. The other classic is swapping fields, like putting the hour value in the minute column. Parsing the expression and checking the next-run times catches both.
Are 0 and 7 both Sunday in cron?
In Vixie cron and most Linux implementations, yes β the crontab(5) man page allows both 0 and 7 for Sunday. But this is not universal: Quartz numbers days 1β7 starting at Sunday, and some strict parsers reject 7. When moving expressions between systems, re-verify the day-of-week field rather than assuming the numbering carries over.
What does 0 0 13 * 5 actually do?
Not "Friday the 13th." When both day-of-month and day-of-week are restricted, standard cron treats them as an OR: the job runs on every 13th of the month and on every Friday. This is documented Vixie cron behavior and one of the format's most misunderstood rules. If you need a true AND, add a date check inside the script itself.
How does daylight saving time affect cron jobs?
On servers in DST-observing timezones, jobs scheduled between 01:00 and 03:00 local time can skip a run (when clocks jump forward) or run twice (when they fall back), depending on the implementation. The safest patterns are running servers on UTC or scheduling critical jobs outside that window. Note that UTC-based schedulers like GitHub Actions don't skip runs, but the local time they correspond to shifts by an hour twice a year.
Is @daily the same as 0 0 * * *?
Yes β @daily (and its synonym @midnight) expands to exactly 0 0 * * * in Vixie cron. Two caveats. First, the toolz.dev parser only accepts five-field expressions, so paste 0 0 * * * rather than @daily for now. Second, every @daily job fires at the same instant, so a server with many of them gets a midnight load spike. Spreading daily jobs across staggered minutes and hours avoids that self-inflicted thundering herd.
Does the toolz.dev cron parser upload my expressions?
No. The Cron Parser runs entirely in your browser β expressions are parsed client-side and never sent to a server, logged, or stored. Since crontabs reveal operational details like backup timing and billing runs, keeping them off third-party servers is a sensible default, and you can confirm the behavior yourself in your browser's Network tab.
Read It Back Before You Ship It
Five weeks of digest emails arriving a day late is not a dramatic outage. Nobody paged me. That's exactly what makes scheduling bugs expensive: they don't announce themselves, they just quietly do the wrong thing until a customer mentions it in passing. The habit that fixed it for me isn't discipline or a better memory for field order β it's a thirty-second readback. Paste the expression, read the English sentence, look at five concrete dates, then deploy.
Keep the Cron Parser next to the tools you'll reach for in the same debugging session: the Timestamp Converter when a next-run time needs to move between zones (I've written up the Unix timestamp traps that bite hardest), the Date Difference Calculator for sanity-checking intervals, and the Text Diff tool when you're comparing an old crontab against a new one during a migration. The coding tools guide walks through how the set fits together, and everything runs client-side β which, for a file that documents exactly when your backups and billing runs fire, is the only sensible default.

