Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Diagnose and validate cron expressions before they ship. Catches the five silent death-traps: impossible dates that never fire, OR-semantics that fire too often, midnight spikes, uneven step drift, and leap-year February 29.
.claude/skills/sickn33-cron-doctor/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 173% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 47% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 119% | 0% |
Cron is deceptively error-prone. The failure mode is silent — a syntactically valid expression that simply never fires, or fires far more often than intended. 0 0 30 2 * parses cleanly and then sits dead forever (February has no 30th). 0 0 1,15 * 1 looks like "1st and 15th if Monday" but actually means "1st, 15th, OR every Monday" — ~6 fires/month instead of ~2.
This skill teaches an agent to catch those before they reach production. It comes with a zero-dependency validation engine (scripts/cron-engine.js, no install needed) that parses, describes, deep-validates, and computes next fire times.
crontab, a Kubernetes CronJob, a GitHub Actions schedule, an Airflow DAG, a Celery beat schedule, a systemd timer, or any scheduled task.
run next?" or "how often does this run per year?"
schedule field.
Split on whitespace into 5 fields: minute, hour, day-of-month, month, day-of-week. Confirm valid ranges:
| Field | Position | Range | Notes | |-------|----------|-------|-------| | minute | 1 | 0–59 | | | hour | 2 | 0–23 | | | day-of-month | 3 | 1–31 | | | month | 4 | 1–12 | names (JAN–DEC) accepted | | day-of-week | 5 | 0–7 | 0 and 7 both = Sunday; names (SUN–SAT) accepted |
State what the user thinks it does vs. what it actually does. Be explicit about OR-vs-AND semantics for day-of-month + day-of-week (see death-trap #2).
Check the five death-traps below and flag any that apply.
Compute the next 5 fire times as concrete dates so the user can verify the schedule behaves as expected. Estimate annual fire count — a schedule that fires 365×/year vs. 12×/year is a ~30× cost and load difference.
These are the bugs that pass crontab -l validation but break in production.
0 0 30 2 *Valid syntax. Never fires. February has no 30th. This schedule is a dead job that silently sits forever. The same applies to day 31 in any 30-day month: 0 0 31 4 *, 0 0 31 6 *, 0 0 31 9 *, 0 0 31 11 *.
Fix: use 0 0 28-31 * * and check for end-of-month in the script, or use L (last day) syntax if your scheduler supports it.
0 0 1,15 * 1Does NOT mean "midnight on the 1st and 15th if it's Monday." Does mean "midnight on the 1st, the 15th, OR every Monday." That's ~6 fires/month instead of ~2.
This is the single most misunderstood cron rule. When both day-of-month AND day-of-week are restricted (neither is *), cron uses OR logic, not AND.
Fix: if you need "1st and 15th only if Monday," run daily and check in the script:
bash0 0 * * 1 [ "$(date +%d)" = "01" -o "$(date +%d)" = "15" ] && your-command
0 0 * * *Every job scheduled at 0 0 competes for resources at exactly 00:00. Database backups, log rotations, cert renewals, report generation — all fire simultaneously. This causes load spikes, connection-pool exhaustion, and cascading timeouts.
Fix: stagger jobs across the hour. Use 17 2 * * * or 43 3 * * * instead of 0 0. Jitter is your friend.
*/7 * * * *Does NOT mean "every 7 minutes evenly." It means "every 7 minutes starting at 0, then resets at 60." So: 0, 7, 14, 21, 28, 35, 42, 49, 56 — then 0 again (a 4-minute gap). The intervals drift: 7,7,7,7,7,7,7,7,4.
Fix: 60 is not divisible by 7. Use step values that divide 60 evenly: */5, */10, */15, */20, */30. If you truly need every-7-minutes, use a loop with sleep 420.
0 0 29 2 *Fires only on leap years — February 29, 2024 / 2028 / 2032… If someone writes this expecting "end of February," they'll be confused for 3 out of every 4 years.
Fix: use 0 0 28 2 * and handle the 29th case in the script if needed.
This skill ships a zero-dependency engine at scripts/cron-engine.js (Node.js, no npm install needed). You can use it programmatically or from the CLI:
javascript// Programmatic — Node.js, zero dependencies const { describe, validate, nextRuns, formatNextRuns } = require('./scripts/cron-engine.js'); // Parse + describe -> returns { text, error, parsed } const d = describe('0 0 30 2 *'); console.log(d.text); // "At 00:00, on day-of-month 30 in in FEB" // Deep validation -> catches the traps const result = validate('0 0 30 2 *'); console.log(result.valid); // true (syntax is valid) console.log(result.observations); // includes the "never fires" insight console.log(result.suggestions); // e.g. "Midnight is a common spike..." // Next 5 fire times -> returns Date[] const runs = nextRuns('0 9 * * 1-5', new Date(), 5); console.log(formatNextRuns(runs, new Date())); // [{ date, relative, formatted }, ...]
bash# CLI (via the bundled wrapper) node scripts/cli.js describe "*/5 * * * *" node scripts/cli.js validate "0 0 30 2 *" node scripts/cli.js next "0 9 * * 1-5" 5
| Expression | Description | Use case | |-----------|-------------|----------| | */5 * * * * | Every 5 minutes | Health checks, polling | | 0 * * * * | Every hour | Hourly aggregation | | 0 */2 * * * | Every 2 hours | Semi-frequent sync | | 0 9 * * 1-5 | 9am Mon–Fri | Business-hours task | | 0 2 * * * | 2am daily | Off-peak batch (avoid midnight) | | 0 0 * * 0 | Midnight Sunday | Weekly maintenance | | 0 0 1 * * | Midnight 1st of month | Monthly report | | 0 0 1 1 * | Midnight Jan 1st | Annual task |
*/5, */15, */30).CRON_TZ) on schedulers that support it.crontab -l validation — it only checks syntax, not semantics.0 0.Solution: Check for an impossible date (trap #1) and confirm the daemon is running (service cron status / systemctl status crond). Verify the file ends with a newline and has correct ownership.
Solution: You hit OR-semantics (trap #2). If both day-of-month and day-of-week are set, cron ORs them. Move one to * or guard in-script.
Solution: Step value doesn't divide 60 evenly (trap #4). Use a divisor of 60.
Solution: Timezone mismatch. Kubernetes CronJob and GitHub Actions default to UTC. Confirm timeZone / TZ is set as intended.
timers, Kubernetes CronJob, GitHub Actions schedule, and most libraries. It does not validate Quartz 6/7-field expressions with seconds/years, nor non-standard @reboot / L / # extensions without a note.
schedules (trap #5) are flagged explicitly.
review. Stop and ask for clarification if required inputs, permissions, or safety boundaries are missing.
docker-expert — when the cron job runs inside a container and the issue is thecontainer/entrypoint rather than the schedule.
kubernetes-deployment — when validating a CronJob manifest's spec.schedulefield alongside the broader resource config.
This skill is read-only and risk: safe. The validation script performs no file writes, network calls, or mutations — it only parses and computes. It is safe to run against any cron expression without preconditions.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 9,127 | 11,505 | +26% | 1 | 1 | 0% | 1,866 | 5,098 | +173% | 0 | 0 | — |
case-02 | pass→pass | 15,582 | 8,625 | -45% | 1 | 1 | 0% | 2,963 | 4,364 | +47% | 0 | 0 | — |
case-03 | pass→pass | 8,686 | 5,855 | -33% | 1 | 1 | 0% | 1,831 | 4,001 | +119% | 0 | 0 | — |
case-04 | pass→pass | 7,172 | 4,951 | -31% | 1 | 1 | 0% | 1,466 | 3,726 | +154% | 0 | 0 | — |
case-05 | pass→pass | 5,468 | 5,808 | +6% | 1 | 1 | 0% | 1,120 | 3,915 | +250% | 0 | 0 | — |
case-06 | pass→pass | 7,125 | 5,495 | -23% | 1 | 1 | 0% | 1,321 | 3,809 | +188% | 0 | 0 | — |
case-07 | fail→fail | 5,705 | 6,082 | +7% | 1 | 1 | 0% | 1,199 | 3,923 | +227% | 0 | 0 | — |
case-08 | fail→fail | 7,855 | 8,116 | +3% | 1 | 1 | 0% | 1,560 | 4,463 | +186% | 0 | 0 | — |
case-09 | fail→pass | 7,792 | 6,101 | -22% | 1 | 1 | 0% | 1,515 | 3,812 | +152% | 0 | 0 | — |
case-10 | pass→pass | 6,107 | 4,515 | -26% | 1 | 1 | 0% | 1,169 | 3,552 | +204% | 0 | 0 | — |
case-11 | pass→pass | 7,842 | 5,532 | -29% | 1 | 1 | 0% | 1,624 | 3,862 | +138% | 0 | 0 | — |
case-12 | pass→pass | 12,878 | 6,043 | -53% | 1 | 1 | 0% | 2,481 | 3,953 | +59% | 0 | 0 | — |
case-13 | pass→pass | 5,444 | 5,391 | -1% | 1 | 1 | 0% | 1,110 | 3,771 | +240% | 0 | 0 | — |
case-14 | pass→pass | 4,472 | 3,894 | -13% | 1 | 1 | 0% | 971 | 3,554 | +266% | 0 | 0 | — |
case-15 | pass→pass | 4,165 | 3,990 | -4% | 1 | 1 | 0% | 720 | 3,495 | +385% | 0 | 0 | — |
case-16 | pass→pass | 2,675 | 3,042 | +14% | 1 | 1 | 0% | 611 | 3,347 | +448% | 0 | 0 | — |
case-17 | pass→pass | 4,913 | 3,883 | -21% | 1 | 1 | 0% | 1,097 | 3,502 | +219% | 0 | 0 | — |
case-18 | pass→pass | 9,973 | 6,396 | -36% | 1 | 1 | 0% | 1,914 | 3,905 | +104% | 0 | 0 | — |
case-19 | pass→pass | 4,289 | 5,038 | +17% | 1 | 1 | 0% | 894 | 3,638 | +307% | 0 | 0 | — |
case-20 | pass→pass | 6,883 | 5,191 | -25% | 1 | 1 | 0% | 1,463 | 3,799 | +160% | 0 | 0 | — |
case-21 | fail→pass | 9,238 | 7,870 | -15% | 1 | 1 | 0% | 1,827 | 4,109 | +125% | 0 | 0 | — |
case-22 | fail→fail | 10,943 | 7,780 | -29% | 1 | 1 | 0% | 2,039 | 4,235 | +108% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.