Quartz expressions look like cron expressions and are not cron expressions. They have an extra field at the front, an optional one at the back, four characters Unix cron has never heard of, and a day-of-week numbering that is off by one. Copying a working Unix schedule into a Spring @Scheduled annotation is therefore a coin flip: sometimes it throws, and sometimes — worse — it is accepted and means something else.
Field layout: 6 or 7 instead of 5
Standard Unix cron has five fields, starting at the minute. Quartz has six required fields, starting at seconds, plus an optional seventh for the year.
| Position | Quartz field | Values | Unix cron equivalent |
|---|---|---|---|
| 1 | Seconds | 0-59 | — no equivalent |
| 2 | Minutes | 0-59 | Field 1 |
| 3 | Hours | 0-23 | Field 2 |
| 4 | Day of month | 1-31, ? L W | Field 3 |
| 5 | Month | 1-12 or JAN-DEC | Field 4 |
| 6 | Day of week | 1-7 or SUN-SAT, ? L # | Field 5 (renumbered) |
| 7 | Year (optional) | 1970-2099 | — no equivalent |
The seconds field is the headline difference and the main reason people reach for Quartz: 0/30 * * * * ? fires every thirty seconds, something standard cron cannot express at all. Our builder has a + Seconds toggle for composing six-field expressions, and the explainer detects a leading seconds field automatically.
Because Quartz prepends seconds, every field in a pasted Unix expression moves one place left. 0 8 * * 1-5 — weekdays at 08:00 in Unix cron — becomes, if Quartz accepts it at all, a schedule whose minute value is 8 and whose hour is a wildcard. This is the failure mode to watch for: not an exception, but a job running 60 times a day at eight minutes past the hour.
Day-of-week numbering is shifted
Unix cron numbers days of the week 0–6 with Sunday as 0. Quartz numbers them 1–7 with Sunday as 1. Every numeric day-of-week value therefore differs by one.
| Day | Unix cron | Quartz | Name (both) |
|---|---|---|---|
| Sunday | 0 (or 7) | 1 | SUN |
| Monday | 1 | 2 | MON |
| Tuesday | 2 | 3 | TUE |
| Wednesday | 3 | 4 | WED |
| Thursday | 4 | 5 | THU |
| Friday | 5 | 6 | FRI |
| Saturday | 6 | 7 | SAT |
"Weekdays" is 1-5 in Unix cron and 2-6 in Quartz. Both dialects accept three-letter names, and MON-FRI means the same thing in both — which is a strong argument for always using names in the day-of-week field when you work across schedulers.
The ? character, and why it is mandatory
Quartz refuses to let you restrict day-of-month and day-of-week at the same time. Where Unix cron resolves the ambiguity with OR logic — running when either matches — Quartz simply rejects the expression, and requires you to write ? ("no specific value") in whichever of the two fields you are not using.
0 0 9 * * ? // 09:00 every day — day-of-week is unspecified
0 0 9 ? * MON-FRI // 09:00 on weekdays — day-of-month is unspecified
0 0 9 * * MON // rejected: both fields restricted
This is arguably the better design: the Unix OR rule is the most common source of accidental over-scheduling, and Quartz makes it impossible to write by accident. It is also why a Unix expression with a restricted day-of-week rarely survives a copy-paste — * in the day-of-month field is exactly what Quartz will not accept alongside it.
Note that ? is invalid in standard Unix cron. Going the other direction, replace it with *.
L, W and # — the calendar operators
These three characters express calendar rules that Unix cron can only approximate with shell guards. They are the strongest reason to use Quartz when you have the choice.
| Character | Field | Meaning | Example |
|---|---|---|---|
L | Day of month | Last day of the month | 0 0 23 L * ? — 23:00 on the last day |
L-n | Day of month | n days before the last day | 0 0 12 L-3 * ? — three days before month end |
L | Day of week | Last given weekday of the month | 0 0 17 ? * 6L — last Friday |
W | Day of month | Nearest weekday to the given day | 0 0 9 15W * ? — the weekday closest to the 15th |
LW | Day of month | Last weekday of the month | 0 0 18 LW * ? — month-end business close |
# | Day of week | The nth given weekday of the month | 0 0 9 ? * 2#1 — first Monday |
W does not cross a month boundary: if the 1st falls on a Saturday, 1W fires on Monday the 3rd rather than the previous Friday. And # does not roll over either — 6#5 ("fifth Friday") simply does not fire in months that have only four.
The same schedules in standard cron
Unix cron has none of these, so the portable pattern is to narrow the schedule to the candidate days and let the command decide:
# Last day of the month, 23:00
0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/close.sh
# First Monday, 09:00
0 9 1-7 * * [ "$(date +\%u)" = "1" ] && /usr/local/bin/first-monday.sh
The escaped \% is a crontab requirement, not a shell one — see the crontab guide.
Translating between the dialects
Going Unix → Quartz:
- Prepend
0as the seconds field. - Add one to every numeric day-of-week value, or convert them to names.
- If day-of-week is restricted, change day-of-month to
?. Otherwise change day-of-week to?. - Check for
7as Sunday — in Quartz that is Saturday.
Going Quartz → Unix:
- Drop the seconds field — and confirm it was
0. If it was0/30, the schedule cannot be expressed in cron at all. - Drop the year field if present.
- Subtract one from numeric day-of-week values.
- Replace
?with*. - Rewrite any
L,Wor#as a shell guard. - Re-check the day-of-month / day-of-week combination: what Quartz forbade, Unix will accept and interpret as OR.
| Intent | Unix cron | Quartz |
|---|---|---|
| Every day at 09:00 | 0 9 * * * | 0 0 9 * * ? |
| Weekdays at 08:00 | 0 8 * * 1-5 | 0 0 8 ? * MON-FRI |
| Every 15 minutes | */15 * * * * | 0 0/15 * * * ? |
| Sundays at midnight | 0 0 * * 0 | 0 0 0 ? * SUN |
| 1st of the month, 03:00 | 0 3 1 * * | 0 0 3 1 * ? |
| Every 30 seconds | Not expressible | 0/30 * * * * ? |
| Last Friday, 17:00 | Shell guard required | 0 0 17 ? * 6L |
Spring's @Scheduled, and its sixth field
Spring's own cron parser is a third dialect, and the difference from Quartz catches people who assume Spring is Quartz. Spring uses six fields — seconds through day-of-week, no year — and follows Unix day-of-week numbering, where 0 is Sunday, while also accepting 7 for Sunday and three-letter names.
@Scheduled(cron = "0 0 8 * * MON-FRI", zone = "Europe/Amsterdam")
public void morningReport() { ... }
Spring accepts ? for compatibility but treats it as equivalent to *, so it does not enforce the Quartz rule against restricting both day fields. It does support L and #, and it adds macros of its own — @daily, @hourly, @weekly, @monthly, @yearly — plus the zone attribute shown above, which is the cleanest way to pin a schedule to a time zone in a JVM app. Without it, the JVM default zone applies, which in a container is usually UTC.
MON-FRI means Monday to Friday in Unix cron, in Quartz and in Spring. 1-5 means Monday to Friday in two of the three. If an expression will be read or copied by someone working in another scheduler, names remove an entire class of bug at no cost.
Misfires: what happens to a missed run
One more difference is behavioural rather than syntactic. Unix cron has no concept of a missed run: if the machine was down at 03:00, the 03:00 job simply did not happen, and nothing catches up.
Quartz tracks triggers in a job store and applies a misfire instruction when a trigger's fire time passes without execution — because the scheduler was down, the thread pool was saturated, or the job took longer than its own interval. The defaults differ by trigger type; for cron triggers the usual behaviour is to fire once as soon as possible and then resume the normal schedule. Other instructions let you skip the missed fire entirely.
This matters when designing the job itself. A Quartz job can be invoked at an unexpected moment after an outage, possibly several at once, so idempotency is not optional. The same discipline pays off under plain cron too — see the best practices guide — but Quartz makes it a routine occurrence rather than an edge case.