Quartz expressions look like cron expressions, but they follow different rules. They have an extra field at the front, an optional one at the back, four characters that Unix cron does not recognize, and a day-of-week numbering that is off by one. That makes copying a working Unix schedule into a Spring @Scheduled annotation a coin flip. Sometimes it throws an exception, and sometimes it is accepted and quietly means something else, which is the worse outcome.
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 most visible difference, and it is the main reason people choose Quartz: 0/30 * * * * ? fires every thirty seconds, which standard cron has no way to express. Our builder has a + Seconds toggle for composing six-field expressions, and the explainer reads full Quartz expressions, including ?, L, day names and the year field.
Because Quartz prepends seconds, every field in a pasted Unix expression moves one place left. In Unix cron, 0 8 * * 1-5 means weekdays at 08:00. If Quartz accepts it at all, it becomes a schedule whose minute value is 8 and whose hour is a wildcard. This is the failure mode to watch for, because nothing raises an exception. You get a job running 24 times a day at eight minutes past every hour, and only from January to May, because 1-5 has moved into the month field.
Day-of-week numbering is shifted
Unix cron numbers days of the week 0 to 6, with Sunday as 0. Quartz numbers them 1 to 7, with Sunday as 1. As a result, every numeric day-of-week value differs by one between the two.
| 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. If you work across schedulers, that is a good reason to always use names in the day-of-week field.
The ? character, and why it is mandatory
Quartz does not let you restrict day-of-month and day-of-week at the same time. Unix cron resolves that ambiguity with OR logic and runs the job when either field matches. Quartz rejects the expression instead, 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. The same rule explains why a Unix expression with a restricted day-of-week rarely survives a copy-paste: Quartz will not accept * in the day-of-month field alongside it.
In standard Unix cron, ? is invalid. When you convert in 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. When you get to pick the scheduler, they are the best argument for choosing Quartz.
| 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. # does not roll over into the next month either, so 6#5 ("fifth Friday") does not fire at all in months that have only four Fridays.
The same schedules in standard cron
Unix cron has none of these. The portable approach is to narrow the schedule to the candidate days and let the command decide whether to run:
# 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 required by crontab rather than by the shell. The crontab guide explains why.
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
7used as Sunday. In Quartz, 7 is Saturday.
Going Quartz → Unix:
- Drop the seconds field, after confirming it was
0. If it was0/30, there is no way to express the schedule in cron. - 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. Unix accepts combinations that Quartz forbids, and it interprets them 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 its differences from Quartz catch people who assume Spring is Quartz. Spring uses six fields (seconds through day-of-week, with no year field) and follows Unix day-of-week numbering, where 0 is Sunday. It also accepts 7 for Sunday, as well as 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 #. It also adds macros of its own (@daily, @hourly, @weekly, @monthly, @yearly) and the zone attribute shown above, which is the simplest way to pin a schedule to a time zone in a JVM app. Without it, the JVM default zone applies, and in a container that is usually UTC.
MON-FRI means Monday to Friday in Unix cron, in Quartz and in Spring. 1-5 means Monday to Friday in only two of the three. If someone who works in another scheduler might read or copy the expression, using names rules out this whole category of bug, and it costs nothing.
Misfires: what happens to a missed run
The last difference is about behaviour rather than syntax. Unix cron has no concept of a missed run: if the machine was down at 03:00, the 03:00 job did not happen, and nothing runs it later.
Quartz tracks triggers in a job store and applies a misfire instruction when a trigger's fire time passes without execution. That happens when the scheduler was down, when the thread pool was saturated, or when 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 affects how you design the job itself. After an outage, a Quartz job can be invoked at an unexpected moment, possibly several at once, so the job has to be idempotent. The same discipline helps under plain cron too (see the best practices guide), but under Quartz a late run is a routine event, where under cron it is an edge case.