{"slug":"cron-doctor","title":"cron-doctor","summary":"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.","platform":"ChatGPT","tags":["debugging","kubernetes","devops"],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-16T13:38:23.442351Z","repo":{"url":"https://github.com/sickn33/agentic-awesome-skills","stars":46883,"forks":6831,"license":"MIT","updatedAt":"2026-09-25T05:43:16Z"},"bodyHtml":"<hr>\n<h2>name: cron-doctor\ndescription: \"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.\"\ncategory: devops\nrisk: safe\nsource: community\nsource_repo: takeaseatventure/devops-skills\nsource_type: community\ndate_added: \"2026-06-26\"\nauthor: takeaseat\ntags: [cron, crontab, scheduling, devops, debugging, kubernetes, validation]\ntools: [claude, cursor, codex, gemini, opencode]\nlicense: \"MIT\"\nlicense_source: \"https://github.com/takeaseatventure/devops-skills/blob/main/LICENSE\"</h2>\n<h1>cron-doctor</h1>\n<h2>Overview</h2>\n<p>Cron is deceptively error-prone. The failure mode is <strong>silent</strong> — a syntactically\nvalid expression that simply never fires, or fires far more often than intended.\n<code>0 0 30 2 *</code> parses cleanly and then sits dead forever (February has no 30th).\n<code>0 0 1,15 * 1</code> looks like \"1st and 15th if Monday\" but actually means \"1st, 15th,\n<strong>OR</strong> every Monday\" — ~6 fires/month instead of ~2.</p>\n<p>This skill teaches an agent to catch those before they reach production. It comes\nwith a zero-dependency validation engine (<code>scripts/cron-engine.js</code>, no install\nneeded) that parses, describes, deep-validates, and computes next fire times.</p>\n<h2>When to Use This Skill</h2>\n<ul>\n<li>Use when a user writes, edits, reviews, or deploys a cron expression — in a\ncrontab, a Kubernetes <code>CronJob</code>, a GitHub Actions <code>schedule</code>, an Airflow DAG,\na Celery beat schedule, a systemd timer, or any scheduled task.</li>\n<li>Use when debugging a job that \"didn't fire\" or \"fired at the wrong time.\"</li>\n<li>Use when a user asks \"what does this cron expression mean?\" or \"when will this\nrun next?\" or \"how often does this run per year?\"</li>\n<li>Use when reviewing a CI/CD pipeline or infrastructure config that contains a\n<code>schedule</code> field.</li>\n<li>Use when a user pastes a 5-field cron expression and asks for a sanity check.</li>\n</ul>\n<h2>How It Works</h2>\n<h3>Step 1: Parse the expression</h3>\n<p>Split on whitespace into 5 fields: minute, hour, day-of-month, month, day-of-week.\nConfirm valid ranges:</p>\n<table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Position</th>\n<th>Range</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>minute</td>\n<td>1</td>\n<td>0–59</td>\n<td></td>\n</tr>\n<tr>\n<td>hour</td>\n<td>2</td>\n<td>0–23</td>\n<td></td>\n</tr>\n<tr>\n<td>day-of-month</td>\n<td>3</td>\n<td>1–31</td>\n<td></td>\n</tr>\n<tr>\n<td>month</td>\n<td>4</td>\n<td>1–12</td>\n<td>names (JAN–DEC) accepted</td>\n</tr>\n<tr>\n<td>day-of-week</td>\n<td>5</td>\n<td>0–7</td>\n<td>0 and 7 both = Sunday; names (SUN–SAT) accepted</td>\n</tr>\n</tbody>\n</table>\n<h3>Step 2: Describe it in plain English</h3>\n<p>State what the user <em>thinks</em> it does vs. what it <em>actually</em> does. Be explicit\nabout OR-vs-AND semantics for day-of-month + day-of-week (see death-trap #2).</p>\n<h3>Step 3: Run the trap checklist</h3>\n<p>Check the five death-traps below and flag any that apply.</p>\n<h3>Step 4: Calculate next runs and annual fire count</h3>\n<p>Compute the next 5 fire times as concrete dates so the user can verify the\nschedule behaves as expected. Estimate annual fire count — a schedule that fires\n365×/year vs. 12×/year is a ~30× cost and load difference.</p>\n<h2>The Five Cron Death-Traps</h2>\n<p>These are the bugs that pass <code>crontab -l</code> validation but break in production.</p>\n<h3>1. Impossible dates — the \"never fires\" bug</h3>\n<pre><code>0 0 30 2 *\n</code></pre>\n<p><strong>Valid syntax. Never fires.</strong> February has no 30th. This schedule is a dead job\nthat silently sits forever. The same applies to day 31 in any 30-day month:\n<code>0 0 31 4 *</code>, <code>0 0 31 6 *</code>, <code>0 0 31 9 *</code>, <code>0 0 31 11 *</code>.</p>\n<p><strong>Fix:</strong> use <code>0 0 28-31 * *</code> and check for end-of-month in the script, or use <code>L</code>\n(last day) syntax if your scheduler supports it.</p>\n<h3>2. OR-semantics — the \"fires too often\" bug</h3>\n<pre><code>0 0 1,15 * 1\n</code></pre>\n<p><strong>Does NOT mean</strong> \"midnight on the 1st and 15th if it's Monday.\"\n<strong>Does mean</strong> \"midnight on the 1st, the 15th, <strong>OR</strong> every Monday.\" That's ~6\nfires/month instead of ~2.</p>\n<p>This is the single most misunderstood cron rule. When <strong>both</strong> day-of-month AND\nday-of-week are restricted (neither is <code>*</code>), cron uses OR logic, not AND.</p>\n<p><strong>Fix:</strong> if you need \"1st and 15th only if Monday,\" run daily and check in the\nscript:</p>\n<pre><code>0 0 * * 1 [ \"$(date +%d)\" = \"01\" -o \"$(date +%d)\" = \"15\" ] &amp;&amp; your-command\n</code></pre>\n<h3>3. Midnight spike — the \"everything at once\" bug</h3>\n<pre><code>0 0 * * *\n</code></pre>\n<p>Every job scheduled at <code>0 0</code> competes for resources at exactly 00:00. Database\nbackups, log rotations, cert renewals, report generation — all fire simultaneously.\nThis causes load spikes, connection-pool exhaustion, and cascading timeouts.</p>\n<p><strong>Fix:</strong> stagger jobs across the hour. Use <code>17 2 * * *</code> or <code>43 3 * * *</code> instead of\n<code>0 0</code>. Jitter is your friend.</p>\n<h3>4. Uneven steps — the \"drift\" bug</h3>\n<pre><code>*/7 * * * *\n</code></pre>\n<p><strong>Does NOT mean</strong> \"every 7 minutes evenly.\" It means \"every 7 minutes starting at\n0, then resets at 60.\" So: 0, 7, 14, 21, 28, 35, 42, 49, 56 — then 0 again\n(a 4-minute gap). The intervals drift: 7,7,7,7,7,7,7,7,<strong>4</strong>.</p>\n<p><strong>Fix:</strong> 60 is not divisible by 7. Use step values that divide 60 evenly: <code>*/5</code>,\n<code>*/10</code>, <code>*/15</code>, <code>*/20</code>, <code>*/30</code>. If you truly need every-7-minutes, use a loop with\n<code>sleep 420</code>.</p>\n<h3>5. Leap-year February 29 — the \"annual surprise\"</h3>\n<pre><code>0 0 29 2 *\n</code></pre>\n<p>Fires only on leap years — February 29, 2024 / 2028 / 2032… If someone writes this\nexpecting \"end of February,\" they'll be confused for 3 out of every 4 years.</p>\n<p><strong>Fix:</strong> use <code>0 0 28 2 *</code> and handle the 29th case in the script if needed.</p>\n<h2>Using the validation script</h2>\n<p>This skill ships a zero-dependency engine at <code>scripts/cron-engine.js</code> (Node.js, no\n<code>npm install</code> needed). You can use it programmatically or from the CLI:</p>\n<pre><code>// Programmatic — Node.js, zero dependencies\nconst { describe, validate, nextRuns, formatNextRuns } = require('./scripts/cron-engine.js');\n\n// Parse + describe -&gt; returns { text, error, parsed }\nconst d = describe('0 0 30 2 *');\nconsole.log(d.text);   // \"At 00:00, on day-of-month 30 in in FEB\"\n\n// Deep validation -&gt; catches the traps\nconst result = validate('0 0 30 2 *');\nconsole.log(result.valid);              // true (syntax is valid)\nconsole.log(result.observations);       // includes the \"never fires\" insight\nconsole.log(result.suggestions);        // e.g. \"Midnight is a common spike...\"\n\n// Next 5 fire times -&gt; returns Date[]\nconst runs = nextRuns('0 9 * * 1-5', new Date(), 5);\nconsole.log(formatNextRuns(runs, new Date())); // [{ date, relative, formatted }, ...]\n</code></pre>\n<pre><code># CLI (via the bundled wrapper)\nnode scripts/cli.js describe \"*/5 * * * *\"\nnode scripts/cli.js validate \"0 0 30 2 *\"\nnode scripts/cli.js next \"0 9 * * 1-5\" 5\n</code></pre>\n<h2>Common cron presets</h2>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Description</th>\n<th>Use case</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>*/5 * * * *</code></td>\n<td>Every 5 minutes</td>\n<td>Health checks, polling</td>\n</tr>\n<tr>\n<td><code>0 * * * *</code></td>\n<td>Every hour</td>\n<td>Hourly aggregation</td>\n</tr>\n<tr>\n<td><code>0 */2 * * *</code></td>\n<td>Every 2 hours</td>\n<td>Semi-frequent sync</td>\n</tr>\n<tr>\n<td><code>0 9 * * 1-5</code></td>\n<td>9am Mon–Fri</td>\n<td>Business-hours task</td>\n</tr>\n<tr>\n<td><code>0 2 * * *</code></td>\n<td>2am daily</td>\n<td>Off-peak batch (avoid midnight)</td>\n</tr>\n<tr>\n<td><code>0 0 * * 0</code></td>\n<td>Midnight Sunday</td>\n<td>Weekly maintenance</td>\n</tr>\n<tr>\n<td><code>0 0 1 * *</code></td>\n<td>Midnight 1st of month</td>\n<td>Monthly report</td>\n</tr>\n<tr>\n<td><code>0 0 1 1 *</code></td>\n<td>Midnight Jan 1st</td>\n<td>Annual task</td>\n</tr>\n</tbody>\n</table>\n<h2>Best Practices</h2>\n<ul>\n<li>✅ Always provide the plain-English description AND run the trap checklist.</li>\n<li>✅ Stagger midnight jobs to avoid the spike.</li>\n<li>✅ Prefer step values that divide 60 evenly (<code>*/5</code>, <code>*/15</code>, <code>*/30</code>).</li>\n<li>✅ Add a comment above every crontab line explaining intent.</li>\n<li>✅ Set an explicit timezone (<code>CRON_TZ</code>) on schedulers that support it.</li>\n<li>❌ Don't trust <code>crontab -l</code> validation — it only checks syntax, not semantics.</li>\n<li>❌ Don't restrict both day-of-month and day-of-week without confirming OR-logic.</li>\n<li>❌ Don't schedule everything at <code>0 0</code>.</li>\n</ul>\n<h2>Common Pitfalls</h2>\n<ul>\n<li><p><strong>Problem:</strong> \"My cron job isn't running.\"\n<strong>Solution:</strong> Check for an impossible date (trap #1) and confirm the daemon is\nrunning (<code>service cron status</code> / <code>systemctl status crond</code>). Verify the file\nends with a newline and has correct ownership.</p>\n</li>\n<li><p><strong>Problem:</strong> \"My job runs far more often than expected.\"\n<strong>Solution:</strong> You hit OR-semantics (trap #2). If both day-of-month and\nday-of-week are set, cron ORs them. Move one to <code>*</code> or guard in-script.</p>\n</li>\n<li><p><strong>Problem:</strong> \"Intervals are uneven — sometimes 7 min, sometimes 4.\"\n<strong>Solution:</strong> Step value doesn't divide 60 evenly (trap #4). Use a divisor of 60.</p>\n</li>\n<li><p><strong>Problem:</strong> \"My job works locally but not in the cluster.\"\n<strong>Solution:</strong> Timezone mismatch. Kubernetes <code>CronJob</code> and GitHub Actions default\nto UTC. Confirm <code>timeZone</code> / <code>TZ</code> is set as intended.</p>\n</li>\n</ul>\n<h2>Limitations</h2>\n<ul>\n<li>This skill targets standard 5-field cron as implemented by Vixie cron, systemd\ntimers, Kubernetes <code>CronJob</code>, GitHub Actions <code>schedule</code>, and most libraries. It\ndoes <strong>not</strong> validate Quartz 6/7-field expressions with seconds/years, nor\nnon-standard <code>@reboot</code> / <code>L</code> / <code>#</code> extensions without a note.</li>\n<li>Estimated annual fire counts assume a non-leap reference year; February 29\nschedules (trap #5) are flagged explicitly.</li>\n<li>This skill does not replace environment-specific validation, testing, or expert\nreview. Stop and ask for clarification if required inputs, permissions, or\nsafety boundaries are missing.</li>\n</ul>\n<h2>Related Skills</h2>\n<ul>\n<li><code>docker-expert</code> — when the cron job runs inside a container and the issue is the\ncontainer/entrypoint rather than the schedule.</li>\n<li><code>kubernetes-deployment</code> — when validating a <code>CronJob</code> manifest's <code>spec.schedule</code>\nfield alongside the broader resource config.</li>\n</ul>\n<h2>Security &amp; Safety Notes</h2>\n<p>This skill is read-only and <code>risk: safe</code>. The validation script performs no file\nwrites, network calls, or mutations — it only parses and computes. It is safe to\nrun against any cron expression without preconditions.</p>\n","files":[{"path":"scripts/cli.js","sizeBytes":2184,"isText":true},{"path":"scripts/cron-engine.js","sizeBytes":21935,"isText":true},{"path":"SKILL.md","sizeBytes":9556,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-16T13:40:44.944773Z","sha256":"39205A306645E4E96EE1C31B6D48D8E9C8D7B48167E074D366A314F10D4B46E3","sizeBytes":11535},"review":null,"source":{"repositoryUrl":"https://github.com/sickn33/agentic-awesome-skills","path":"skills/cron-doctor","license":"MIT","commit":"f2bba339de74414b0771234cbe4f6a15258e32a3","subtreeSha":"F12BA29513E3E9105D6C5CDA088536C79E11B08F3A6D32AB726E067484DD838B","lastSyncedAt":"2026-09-25T06:48:39.853703Z"},"reviewedAt":"2026-08-16T13:43:28.504575Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/cron-doctor"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart"},{"target":"git","command":"git clone https://github.com/sickn33/agentic-awesome-skills.git"}]}