Cron schedules are a good AI use case for one reason: they are cheaply verifiable. Unlike a shell command that might destroy data, a wrong cron expression can be checked against a calculator or systemd-analyze calendar in seconds, before it ever runs.
There is one specific rule models get wrong repeatedly, and it produces jobs that run far more often than intended. That rule is worth knowing yourself.
The Trap: Day-of-Month and Day-of-Week Are ORed
When both the day-of-month and day-of-week fields are restricted, cron runs the job if either matches — not both. This is unintuitive and is the most common cron misunderstanding, in humans and models alike:
# INTENT: "the 1st of the month, but only if it is a Monday"
0 3 1 * 1
# ACTUAL: every 1st of the month, AND every Monday
# That is roughly 5x more runs than intended.
The correct approach is to leave one field as * and put the extra condition in the command:
# Run on the 1st only if it is a Monday — test inside the job
0 3 1 * * [ "$(date +\%u)" = "1" ] && /opt/scripts/monthly.sh
# Note: % must be escaped as \% inside a crontab, or cron truncates the line
Ask the model about this explicitly — "does this expression AND or OR the day fields" is a good sanity check — and a model that answers wrongly tells you not to trust its other cron claims.
The percent escape is its own trap — an unescaped % in a crontab command is treated as a newline. Generated crontab lines containing date +%u or date +%Y are frequently broken this way.
Ask With a Verification Request Built In
$ ollama run llama3.2 "Produce a cron expression.
REQUIREMENT: every weekday at 02:30, plus the last day of each month at 04:00.
CRON FLAVOUR: Vixie cron on Debian (standard 5-field crontab).
Reply as:
1. EXPRESSION(S) - the crontab line(s)
2. FIRING TIMES - the next 6 times each would run
3. DAY FIELD LOGIC - state whether the day-of-month and day-of-week fields
are ANDed or ORed in this expression, and whether that matters here
4. CAVEATS - anything about DST, timezones, or escaping
5. UNCERTAIN - anything you are not sure about"
Asking for the next six firing times is the highest-value part. It converts an abstract expression into something you can check against a calendar immediately, and a model that produces an expression whose stated firing times are wrong has told you the answer is wrong.
Verify Before You Install
Three independent checks, in increasing order of authority:
# 1. systemd's calendar parser — authoritative for OnCalendar, useful as a cross-check
$ systemd-analyze calendar 'Mon..Fri 02:30'
$ systemd-analyze calendar --iterations=6 '*-*-01 04:00'
# 2. Install into a test crontab and read it back
$ crontab -l > /tmp/cron.bak # always back up first
$ echo '30 2 * * 1-5 /opt/scripts/job.sh' | crontab -
$ crontab -l
# 3. Check what cron actually recorded when it ran
$ journalctl -u cron --since today | tail
$ grep CRON /var/log/syslog | tail
Back up before every crontab change — crontab -r and a piped crontab - both replace the whole file without confirmation. crontab -l > backup costs nothing.
systemd-analyze calendar is free verification — even on a cron-based job, expressing the intent as an OnCalendar string and checking its iterations validates your understanding of the schedule.
Use our generator as a second opinion — the cron expression generator shows the schedule in plain language, which is exactly the cross-check an AI answer needs.
Explaining an Inherited Crontab
This is where AI earns its place — decoding a crontab nobody has understood for years:
$ ollama run llama3.2 "Explain each line of this crontab in plain English,
including how often it runs and any problems you see.
$(crontab -l)
For each line report: SCHEDULE (plain English), COMMAND (what it does),
ISSUES (missing redirect, relative path, unescaped %, overlap risk, day-field OR)."
| Issue to look for | Why it matters |
|---|---|
| No output redirect | Output goes to local mail, which is usually unconfigured — failures vanish |
| Relative paths | cron runs with a minimal environment and a different working directory |
Unescaped % | cron treats it as a newline, truncating the command |
| No locking | A job slower than its interval runs concurrently with itself |
| Both day fields set | Fires on the OR of the two, not the AND |
| Everything at midnight | Simultaneous jobs create a resource spike |
Our cron jobs guide covers the fixes for each of these — the environment problem and flock in particular.
Converting cron to systemd Timers
Migration is another strong use case, because the result is instantly verifiable:
$ ollama run llama3.2 "Convert this crontab line to a systemd service + timer.
Line: 30 2 * * 1-5 /opt/scripts/report.sh >> /var/log/report.log 2>&1
Requirements: run missed jobs after downtime, prevent overlapping runs,
log to journald instead of a file. Give both unit files."
# Then verify the calendar expression it produced:
$ systemd-analyze calendar --iterations=6 'Mon..Fri 02:30'
# The shape a correct answer takes
# /etc/systemd/system/report.service
[Unit]
Description=Nightly report
[Service]
Type=oneshot
ExecStart=/opt/scripts/report.sh
# /etc/systemd/system/report.timer
[Timer]
OnCalendar=Mon..Fri 02:30
Persistent=true
RandomizedDelaySec=120
[Install]
WantedBy=timers.target
Persistent=true — runs a missed job after the machine was off — cron has no equivalent.
RandomizedDelaySec — spreads load when many machines share a schedule. Models rarely suggest it unprompted.
Overlap prevention is free — systemd will not start a oneshot service that is already running, so the flock wrapper cron needs becomes unnecessary.
Verify the OnCalendar string — systemd-analyze calendar is definitive here — an invalid expression fails loudly rather than silently never firing.
Timezones and DST
cron uses the system timezone — check with timedatectl. A server on UTC and an operator on local time is a recurring source of confusion.
DST creates gaps and repeats — a job scheduled in the hour that DST skips may not run that day; in the repeated hour it may run twice. Models almost never mention this.
Schedule outside the transition window — avoiding 01:00–03:00 local time removes the entire problem for daily jobs.
Prefer UTC on servers — it eliminates DST ambiguity for infrastructure schedules entirely.
systemd timers can pin a timezone — an OnCalendar value can include a timezone, which cron cannot express.
Frequently Asked Questions
Are cron day-of-month and day-of-week fields ANDed or ORed?
ORed, when both are restricted. An expression like 0 3 1 * 1 runs on the 1st of every month AND every Monday, not only on Mondays that fall on the 1st. To require both, leave one field as * and test the other condition inside the command.
Can I trust an AI-generated cron expression?
Verify it, but the verification is cheap. Ask for the next six firing times alongside the expression, cross-check with systemd-analyze calendar or a cron calculator, and confirm the day-field logic. Unlike most generated commands, a bad schedule is caught before it does anything.
Why did my cron job stop at a percent sign?
In a crontab, an unescaped % is treated as a newline, so the command is truncated at that point. Escape it as \% — this bites any generated line containing date +%Y or similar format strings.
How do I verify a systemd OnCalendar expression?
Run systemd-analyze calendar 'your expression', optionally with --iterations=6 to list upcoming firing times. It reports invalid syntax immediately, which is safer than a cron expression that parses but never matches.
Should I use cron or systemd timers?
Timers when you want missed jobs to run after downtime (Persistent=true), built-in overlap prevention, journald logging, and randomized delays. Cron remains fine for simple, portable schedules — see our cron jobs guide for the full comparison.