Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if step_match:\n start = step_match.group(1)\n step = int(step_match.group(2))\n if field == 'minute':\n return f'every {step} minute{\"s\" if step > 1 else \"\"}'\n if field == 'hour':\n return f'every {step} hour{\"s\" if step > 1 else \"\"}'\n if field == 'day_of_month':\n return f'every {step} day{\"s\" if step > 1 else \"\"}'\n if field == 'month':\n return f'every {step} month{\"s\" if step > 1 else \"\"}'\n if field == 'second':\n return f'every {step} second{\"s\" if step > 1 else \"\"}'\n return f'every {step}'\n\n # Range: N-M\n range_match = re.match(r'^(\\d+)-(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if range_match:\n lo, hi = int(range_match.group(1)), int(range_match.group(2))\n if names:\n lo_name = names[lo] if lo \u003c len(names) else str(lo)\n hi_name = names[hi] if hi \u003c len(names) else str(hi)\n return f'{lo_name} through {hi_name}'\n if field == 'hour':\n lo_fmt = f'{lo}:00 {\"AM\" if lo \u003c 12 else \"PM\"}'\n hi_fmt = f'{hi % 12 or 12}:00 {\"AM\" if hi \u003c 12 else \"PM\"}'\n return f'between {lo_fmt} and {hi_fmt}'\n return f'{lo} to {hi}'\n\n # Range with step: N-M/S\n range_step_match = re.match(r'^(\\d+)-(\\d+)/(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if range_step_match:\n lo = int(range_step_match.group(1))\n hi = int(range_step_match.group(2))\n step = int(range_step_match.group(3))\n range_desc = explain_field(f'{lo}-{hi}', field, names)\n return f'every {step} {field.replace(\"_\", \" \")}{\"s\" if step > 1 else \"\"} {range_desc}'\n\n # List: N,M,P\n if ',' in value:\n parts = [p.strip() for p in value.split(',')]\n if names:\n named = [names[int(p)] if p.isdigit() and int(p) \u003c len(names) else p for p in parts]\n else:\n named = parts\n if len(named) == 2:\n return f'{named[0]} and {named[1]}'\n return ', '.join(named[:-1]) + f', and {named[-1]}'\n\n # Single value\n if value.isdigit():\n n = int(value)\n if field == 'hour':\n if n == 0: return 'midnight'\n if n == 12: return 'noon'\n if n \u003c 12: return f'{n}:00 AM'\n return f'{n - 12}:00 PM'\n if field == 'minute':\n return f'minute {n}' if n != 0 else 'on the hour'\n if field == 'second':\n return f'second {n}'\n if field == 'day_of_month':\n suffix = 'st' if n % 10 == 1 and n != 11 else \\\n 'nd' if n % 10 == 2 and n != 12 else \\\n 'rd' if n % 10 == 3 and n != 13 else 'th'\n return f'the {n}{suffix}'\n if field == 'month' and names:\n return names[n - 1] if 1 \u003c= n \u003c= 12 else str(n)\n if field == 'day_of_week' and names:\n return names[n % 7]\n return str(n)\n\n # Special Quartz: L (last), W (nearest weekday), # (Nth weekday)\n if value.upper() == 'L' and field == 'day_of_month':\n return 'the last day of the month'\n if value.upper() == 'L' and field == 'day_of_week':\n return 'the last weekday of the month'\n lw_match = re.match(r'^(\\d+)W

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value, re.I)\n if lw_match:\n return f'the weekday nearest the {lw_match.group(1)}th'\n hash_match = re.match(r'^(\\d+)#(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if hash_match:\n day = int(hash_match.group(1))\n nth = int(hash_match.group(2))\n ordinals = ['', '1st', '2nd', '3rd', '4th', '5th']\n day_name = WEEKDAY_NAMES[day % 7]\n return f'the {ordinals[nth]} {day_name} of the month'\n\n return value # fallback — return as-is\n\n\ndef explain_cron(expression: str) -> dict:\n \"\"\"\n Parse a cron expression and return a human-readable explanation.\n Returns dict with: explanation, flavor, issues, fields\n \"\"\"\n expr = expression.strip()\n\n # Handle special strings\n expr_lower = expr.lower()\n if expr_lower in SPECIAL_STRINGS:\n cron_equiv, desc = SPECIAL_STRINGS[expr_lower]\n return {\n 'expression': expr,\n 'flavor': 'special',\n 'explanation': desc,\n 'next_runs': [] if expr_lower == '@reboot' else None,\n 'issues': [],\n 'fields': {},\n }\n\n # Normalize: replace month/weekday names with numbers\n expr = expr.upper()\n month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,\n 'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}\n dow_map = {'SUN':0,'MON':1,'TUE':2,'WED':3,'THU':4,'FRI':5,'SAT':6}\n for name, num in {**month_map, **dow_map}.items():\n expr = expr.replace(name, str(num))\n\n parts = expr.split()\n flavor = detect_flavor(expression)\n issues = []\n\n # Map parts to field names by flavor\n if flavor == '6field-seconds':\n if len(parts) != 6:\n issues.append({'severity': 'ERROR', 'message': f'Expected 6 fields, got {len(parts)}'})\n return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}\n field_names = ['second', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week']\n field_values = dict(zip(field_names, parts))\n elif len(parts) == 5:\n field_names = ['minute', 'hour', 'day_of_month', 'month', 'day_of_week']\n field_values = dict(zip(field_names, parts))\n else:\n issues.append({'severity': 'ERROR', 'message': f'Unrecognized format: {len(parts)} fields'})\n return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}\n\n # Explain each field\n minute_desc = explain_field(field_values.get('minute', '*'), 'minute')\n hour_desc = explain_field(field_values.get('hour', '*'), 'hour')\n dom_desc = explain_field(field_values.get('day_of_month', '*'), 'day_of_month')\n month_desc = explain_field(field_values.get('month', '*'), 'month', MONTH_NAMES)\n dow_desc = explain_field(field_values.get('day_of_week', '*'), 'day_of_week', WEEKDAY_NAMES)\n sec_desc = explain_field(field_values.get('second', None) or '0', 'second') if flavor == '6field-seconds' else None\n\n # Compose natural language description\n parts_en = []\n\n # Frequency\n if minute_desc and minute_desc.startswith('every'):\n parts_en.append(minute_desc.capitalize())\n elif minute_desc and minute_desc.startswith('minute'):\n if hour_desc:\n parts_en.append(f'At {hour_desc}, {minute_desc}')\n else:\n parts_en.append(f'Every hour, {minute_desc}')\n elif not minute_desc and not hour_desc:\n parts_en.append('Every minute')\n elif not minute_desc and hour_desc:\n parts_en.append(f'Every hour at {hour_desc}')\n elif minute_desc == 'on the hour' and hour_desc:\n parts_en.append(f'At {hour_desc}')\n else:\n if hour_desc and minute_desc:\n # Format as clock time if both are single values\n parts_en.append(f'At {hour_desc}')\n if minute_desc and not minute_desc.startswith('on the hour'):\n parts_en[-1] += f', {minute_desc}'\n elif hour_desc:\n parts_en.append(f'At {hour_desc}')\n elif minute_desc:\n parts_en.append(f'Every hour, at {minute_desc}')\n\n if sec_desc and sec_desc != 'second 0':\n parts_en.append(f'at {sec_desc}')\n\n # Day constraints\n if dom_desc and dow_desc:\n parts_en.append(f'on {dom_desc} or {dow_desc}')\n elif dom_desc:\n parts_en.append(f'on {dom_desc}')\n elif dow_desc:\n parts_en.append(f'on {dow_desc}{\"s\" if not dow_desc.endswith(\"day\") else \"\"}')\n\n # Month constraint\n if month_desc:\n parts_en.append(f'in {month_desc}')\n\n explanation = ', '.join(parts_en) if parts_en else 'Every minute'\n\n # ── Validation ────────────────────────────────────────────────────────────\n\n # Every minute in production\n if field_values.get('minute', '') == '*' and field_values.get('hour', '') == '*':\n issues.append({\n 'severity': 'WARNING',\n 'message': 'Runs every minute — confirm this is intentional for a production schedule',\n })\n\n # Validate numeric bounds\n BOUNDS = {\n 'minute': (0, 59), 'hour': (0, 23),\n 'day_of_month': (1, 31), 'month': (1, 12), 'day_of_week': (0, 7),\n 'second': (0, 59),\n }\n for fname, fval in field_values.items():\n if fname not in BOUNDS or fval in ('*', '?'):\n continue\n lo, hi = BOUNDS[fname]\n # Extract all numeric values\n nums = re.findall(r'\\d+', fval)\n for n_str in nums:\n n = int(n_str)\n if n \u003c lo or n > hi:\n issues.append({\n 'severity': 'ERROR',\n 'message': f'Invalid {fname} value: {n} (valid range: {lo}-{hi})',\n })\n\n # February 30 / 31 impossibility\n month_val = field_values.get('month', '*')\n dom_val = field_values.get('day_of_month', '*')\n if month_val == '2' and dom_val not in ('*', '?'):\n days = re.findall(r'\\d+', dom_val)\n for d in days:\n if int(d) > 29:\n issues.append({\n 'severity': 'ERROR',\n 'message': f'day-of-month={d} in month=2 (February) — this date never occurs',\n })\n elif int(d) == 29:\n issues.append({\n 'severity': 'WARNING',\n 'message': 'day-of-month=29 in February — only runs on leap years',\n })\n\n # Detect conflicting DOW + DOM (both specified — runs on OR condition, often surprising)\n if dom_val not in ('*', '?') and field_values.get('day_of_week', '*') not in ('*', '?'):\n issues.append({\n 'severity': 'INFO',\n 'message': (\n 'Both day-of-month and day-of-week are specified — '\n 'standard cron uses OR (runs if EITHER matches). '\n 'Use ? for one field if you want AND behavior (Quartz only).'\n ),\n })\n\n return {\n 'expression': expression,\n 'flavor': flavor,\n 'explanation': explanation,\n 'fields': field_values,\n 'issues': issues,\n }\n```\n\n---\n\n## Step 2: Calculate Next Run Times\n\n```python\nfrom datetime import datetime, timezone\n\ndef next_runs(expression: str, count: int = 5, after: Optional[datetime] = None) -> list[datetime]:\n \"\"\"\n Calculate the next N run times for a cron expression.\n Uses croniter if available, otherwise falls back to manual calculation.\n \"\"\"\n after = after or datetime.now(tz=timezone.utc)\n\n try:\n from croniter import croniter\n cron = croniter(expression, after)\n return [cron.get_next(datetime) for _ in range(count)]\n except ImportError:\n pass\n\n # Fallback: install croniter\n import subprocess\n subprocess.run(['pip', 'install', 'croniter', '-q'], check=True)\n from croniter import croniter\n cron = croniter(expression, after)\n return [cron.get_next(datetime) for _ in range(count)]\n\n\ndef format_run_times(runs: list[datetime], timezone_name: str = 'UTC') -> list[str]:\n \"\"\"Format run times for display.\"\"\"\n now = datetime.now(tz=timezone.utc)\n result = []\n for dt in runs:\n diff = dt - now\n if diff.total_seconds() \u003c 60:\n rel = 'in less than a minute'\n elif diff.total_seconds() \u003c 3600:\n mins = int(diff.total_seconds() / 60)\n rel = f'in {mins} minute{\"s\" if mins > 1 else \"\"}'\n elif diff.total_seconds() \u003c 86400:\n hours = int(diff.total_seconds() / 3600)\n rel = f'in {hours} hour{\"s\" if hours > 1 else \"\"}'\n else:\n days = int(diff.total_seconds() / 86400)\n rel = f'in {days} day{\"s\" if days > 1 else \"\"}'\n\n result.append(f'{dt.strftime(\"%Y-%m-%d %H:%M:%S\")} UTC ({rel})')\n return result\n```\n\n---\n\n## Step 3: English → Cron Converter\n\n```python\nimport re\n\ndef english_to_cron(description: str) -> dict:\n \"\"\"\n Convert a plain-English schedule description to cron expression.\n Returns {'cron': str, 'explanation': str, 'confidence': str}\n \"\"\"\n desc = description.lower().strip()\n\n # Special patterns\n if re.search(r'every\\s+minute', desc):\n return {'cron': '* * * * *', 'explanation': 'Every minute', 'confidence': 'HIGH'}\n\n if re.search(r'every\\s+hour', desc):\n return {'cron': '0 * * * *', 'explanation': 'Every hour', 'confidence': 'HIGH'}\n\n if re.search(r'midnight|every\\s+(?:day\\s+at\\s+)?12\\s*am', desc):\n return {'cron': '0 0 * * *', 'explanation': 'Every day at midnight', 'confidence': 'HIGH'}\n\n if re.search(r'noon|every\\s+(?:day\\s+at\\s+)?12\\s*pm', desc):\n return {'cron': '0 12 * * *', 'explanation': 'Every day at noon', 'confidence': 'HIGH'}\n\n # Extract time: \"at 9am\", \"at 3:30pm\", \"at 14:00\"\n time_match = re.search(\n r'at\\s+(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?',\n desc, re.I\n )\n hour = minute = None\n if time_match:\n hour = int(time_match.group(1))\n minute = int(time_match.group(2)) if time_match.group(2) else 0\n ampm = (time_match.group(3) or '').lower()\n if ampm == 'pm' and hour != 12:\n hour += 12\n elif ampm == 'am' and hour == 12:\n hour = 0\n\n # Day of week\n dow_map = {\n 'sunday': 0, 'monday': 1, 'tuesday': 2, 'wednesday': 3,\n 'thursday': 4, 'friday': 5, 'saturday': 6,\n 'weekday': '1-5', 'weekdays': '1-5',\n 'weekend': '0,6', 'weekends': '0,6',\n 'every day': '*', 'daily': '*',\n }\n\n dow = '*'\n for key, val in dow_map.items():\n if key in desc:\n dow = str(val)\n break\n\n # Interval: \"every N minutes/hours\"\n interval_match = re.search(r'every\\s+(\\d+)\\s+(minute|hour|min|hr)s?', desc, re.I)\n if interval_match:\n n = int(interval_match.group(1))\n unit = interval_match.group(2).lower()\n\n # Business hours modifier\n if re.search(r'business\\s+hours?|work\\s+hours?|9\\s*(?:am|to)\\s*5', desc):\n if unit in ('minute', 'min'):\n cron = f'*/{n} 9-17 * * 1-5'\n return {'cron': cron,\n 'explanation': f'Every {n} minutes, between 9am and 5pm, Monday–Friday',\n 'confidence': 'HIGH'}\n if unit in ('hour', 'hr'):\n cron = f'0 */{ n} * * 1-5' if n > 1 else f'0 9-17 * * 1-5'\n return {'cron': cron,\n 'explanation': f'Every {n} hour{\"s\" if n>1 else \"\"}, weekdays only',\n 'confidence': 'MEDIUM'}\n\n if unit in ('minute', 'min'):\n cron_min = f'*/{n}' if n > 1 else '*'\n cron = f'{cron_min} * * * *'\n return {'cron': cron,\n 'explanation': f'Every {n} minute{\"s\" if n>1 else \"\"}',\n 'confidence': 'HIGH'}\n\n if unit in ('hour', 'hr'):\n cron = f'0 */{n} * * *'\n return {'cron': cron,\n 'explanation': f'Every {n} hour{\"s\" if n>1 else \"\"}',\n 'confidence': 'HIGH'}\n\n # \"first day of every month\"\n if re.search(r'first\\s+day\\s+of\\s+(?:every|each)\\s+month', desc):\n h = hour if hour is not None else 0\n m = minute if minute is not None else 0\n cron = f'{m} {h} 1 * *'\n time_str = f'at {h:02d}:{m:02d}' if hour is not None else 'at midnight'\n return {'cron': cron,\n 'explanation': f'On the 1st of every month, {time_str}',\n 'confidence': 'HIGH'}\n\n # \"every weekday at Xam\"\n if hour is not None and dow != '*':\n m = minute if minute is not None else 0\n cron = f'{m} {hour} * * {dow}'\n dow_names = {'1-5': 'Monday–Friday', '0,6': 'Saturday and Sunday',\n '0': 'Sunday', '1': 'Monday', '*': 'every day'}\n dow_str = dow_names.get(str(dow), f'day {dow}')\n return {'cron': cron,\n 'explanation': f'At {hour:02d}:{m:02d}, {dow_str}',\n 'confidence': 'HIGH'}\n\n # Simple \"every day at X\"\n if hour is not None:\n m = minute if minute is not None else 0\n cron = f'{m} {hour} * * *'\n am_pm = 'AM' if hour \u003c 12 else 'PM'\n h12 = hour % 12 or 12\n return {'cron': cron,\n 'explanation': f'Every day at {h12}:{m:02d} {am_pm}',\n 'confidence': 'HIGH'}\n\n return {\n 'cron': None,\n 'explanation': 'Could not parse — try: \"every weekday at 9am\", \"every 5 minutes\", \"first day of month at midnight\"',\n 'confidence': 'NONE',\n }\n```\n\n---\n\n## Step 4: Scan Project Files for Cron Schedules\n\n```python\nimport glob\nimport re\nimport yaml # pyyaml\nfrom pathlib import Path\n\n\ndef scan_for_cron_schedules(root: str = '.') -> list[dict]:\n \"\"\"Find all cron expressions in GitHub Actions, Kubernetes, and crontab files.\"\"\"\n schedules = []\n\n # GitHub Actions schedule triggers\n for fpath in glob.glob(f'{root}/.github/workflows/*.yml', recursive=False):\n fpath += '' if fpath.endswith('.yml') else ''\n for path in glob.glob(f'{root}/.github/workflows/*.yml') + glob.glob(f'{root}/.github/workflows/*.yaml'):\n try:\n content = Path(path).read_text(errors='replace')\n doc = yaml.safe_load(content)\n if not isinstance(doc, dict):\n continue\n on = doc.get('on', doc.get(True, {}))\n if isinstance(on, dict):\n schedule = on.get('schedule', [])\n if isinstance(schedule, list):\n for item in schedule:\n if isinstance(item, dict) and 'cron' in item:\n schedules.append({\n 'file': path,\n 'source': 'github-actions',\n 'expression': item['cron'],\n 'context': doc.get('name', 'workflow'),\n })\n except Exception:\n continue\n\n # Kubernetes CronJob specs\n for path in glob.glob(f'{root}/**/*.yaml', recursive=True) + glob.glob(f'{root}/**/*.yml', recursive=True):\n if any(skip in path for skip in ['.git', 'node_modules', 'vendor']):\n continue\n try:\n content = Path(path).read_text(errors='replace')\n if 'CronJob' not in content:\n continue\n doc = yaml.safe_load(content)\n if isinstance(doc, dict) and doc.get('kind') == 'CronJob':\n schedule = (doc.get('spec', {}) or {}).get('schedule')\n if schedule:\n schedules.append({\n 'file': path,\n 'source': 'kubernetes-cronjob',\n 'expression': schedule,\n 'context': doc.get('metadata', {}).get('name', 'cronjob'),\n })\n except Exception:\n continue\n\n # crontab files\n for crontab_path in ['crontab', 'crontab.txt', 'etc/cron.d', 'docker/cron']:\n full = f'{root}/{crontab_path}'\n if not Path(full).exists():\n continue\n try:\n for i, line in enumerate(Path(full).read_text().splitlines(), 1):\n line = line.strip()\n if line.startswith('#') or not line:\n continue\n # crontab line format: min hour dom month dow command\n parts = line.split()\n if len(parts) >= 6:\n expr = ' '.join(parts[:5])\n command = ' '.join(parts[5:])\n schedules.append({\n 'file': crontab_path,\n 'source': 'crontab',\n 'expression': expr,\n 'context': command[:60],\n 'line': i,\n })\n except Exception:\n continue\n\n return schedules\n```\n\n---\n\n## Step 5: Output Report\n\n```markdown\n## Cron Expression Analysis\n\n---\n\n### Expression: `*/5 9-17 * * 1-5`\n\n**Translation:** Every 5 minutes, between 9:00 AM and 5:00 PM, on Monday through Friday\n\n**Flavor:** Standard Unix (5-field)\n\n**Field Breakdown:**\n| Field | Value | Meaning |\n|-------|-------|---------|\n| Minute | `*/5` | Every 5 minutes |\n| Hour | `9-17` | Between 9:00 AM and 5:00 PM |\n| Day-of-month | `*` | Every day |\n| Month | `*` | Every month |\n| Day-of-week | `1-5` | Monday through Friday |\n\n**Next 5 scheduled runs:**\n```\n2026-03-19 09:00:00 UTC (in 3 minutes)\n2026-03-19 09:05:00 UTC (in 8 minutes)\n2026-03-19 09:10:00 UTC (in 13 minutes)\n2026-03-19 09:15:00 UTC (in 18 minutes)\n2026-03-19 09:20:00 UTC (in 23 minutes)\n```\n\n**Issues:** None ✅\n\n---\n\n### Expression: `0 0 30 2 *`\n\n**Translation:** At midnight, on the 30th of February\n\n**Issues:**\n- 🔴 ERROR: `day-of-month=30 in month=2 (February)` — **this date never occurs**. Job will never run.\n\n**Fix:** Did you mean the last day of every month?\n```\n# Last day of every month (Quartz): 0 0 L * *\n# 28th of February specifically: 0 0 28 2 *\n# Every month on the 30th: 0 0 30 * * (skips Feb, Apr, Jun, Sep, Nov)\n```\n\n---\n\n### English → Cron Examples\n\n| Input | Output | Explanation |\n|-------|--------|-------------|\n| `\"every weekday at 9am\"` | `0 9 * * 1-5` | At 09:00, Monday–Friday |\n| `\"every 15 minutes during business hours\"` | `*/15 9-17 * * 1-5` | Every 15 min, 9am–5pm, Mon–Fri |\n| `\"first day of every month at midnight\"` | `0 0 1 * *` | At midnight on the 1st |\n| `\"every Sunday at 2:30am\"` | `30 2 * * 0` | At 02:30, Sunday |\n| `\"every 6 hours\"` | `0 */6 * * *` | At minute 0, every 6 hours |\n\n---\n\n### Project Scan: .github/workflows/\n\nFound 3 cron schedules:\n\n| File | Schedule | Explanation | Issues |\n|------|----------|-------------|--------|\n| `.github/workflows/deploy.yml` | `0 2 * * 1` | Every Monday at 2:00 AM | ✅ OK |\n| `.github/workflows/cleanup.yml` | `*/5 * * * *` | Every 5 minutes | ⚠️ Very frequent — confirm intentional |\n| `.github/workflows/report.yml` | `0 0 30 2 *` | Feb 30th at midnight | 🔴 NEVER RUNS (Feb 30 doesn't exist) |\n\n---\n\n### Flavor Reference\n\n| Flavor | Fields | Example | Notes |\n|--------|--------|---------|-------|\n| Standard Unix | `min hour dom month dow` | `0 2 * * 1` | `cron`, `crontab` |\n| 6-field (seconds) | `sec min hour dom month dow` | `0 0 2 * * 1` | Spring, Quartz |\n| GitHub Actions | `min hour dom month dow` | `'0 2 * * 1'` | Under `on.schedule.cron` |\n| AWS EventBridge | `min hour dom month dow year` | `0 2 ? * MON *` | Uses `?` for DOM/DOW |\n| Kubernetes | `min hour dom month dow` | `0 2 * * 1` | `.spec.schedule` |\n```\n\n---\n\n## Quick Mode Output\n\n```\nCron: */5 9-17 * * 1-5\n\nEvery 5 minutes, between 9:00 AM and 5:00 PM, Monday–Friday\n\nNext run: 2026-03-19 09:00:00 UTC (in 3 minutes)\nIssues: none\n\nTo convert English → cron:\n \"every weekday at 9am\" → 0 9 * * 1-5\n \"first of month midnight\" → 0 0 1 * *\n```\n---","attachment_filenames":["_meta.json"],"attachments":[{"filename":"_meta.json","content":"{\n \"owner\": \"phy041\",\n \"slug\": \"phy-cron-explainer\",\n \"displayName\": \"Phy Cron Explainer\",\n \"latest\": {\n \"version\": \"1.0.0\",\n \"publishedAt\": 1774120466578,\n \"commit\": \"https://github.com/openclaw/skills/commit/bc809f995e64b79ce05b5c106095661309af6682\"\n },\n \"history\": []\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":288,"content_sha256":"ef5616da8aef70accb9d4666f953b4d324ac1940b15664ca897de30037adc286"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Cron Explainer","type":"text"}]},{"type":"paragraph","content":[{"text":"0 2 * * 1","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"What does that mean? Every Monday at 2am. How about ","type":"text"},{"text":"*/5 9-17 * * 1-5","type":"text","marks":[{"type":"code_inline"}]},{"text":"? Every 5 minutes between 9am and 5pm on weekdays.","type":"text"}]},{"type":"paragraph","content":[{"text":"You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot.","type":"text"}]},{"type":"paragraph","content":[{"text":"All major cron flavors. Zero external API.","type":"text","marks":[{"type":"strong"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Trigger Phrases","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"cron expression\", \"explain this cron\", \"what does cron mean\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"cron to English\", \"translate cron\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"cron schedule\", \"next run time\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"English to cron\", \"convert to cron\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"cron syntax\", \"cron debug\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"kubernetes cronjob\", \"github actions schedule\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"/cron\"","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Provide Input","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Option 1: Explain a cron expression\n/cron \"0 2 * * 1\"\n/cron \"*/5 9-17 * * 1-5\"\n/cron \"0 0 1,15 * *\"\n\n# Option 2: Explain a special string\n/cron \"@daily\"\n/cron \"@reboot\"\n/cron \"@every 6h\" # Kubernetes/Go format\n\n# Option 3: Convert English to cron\n/cron \"every weekday at 9am\"\n/cron \"every 5 minutes during business hours\"\n/cron \"first day of every month at midnight\"\n/cron \"every 15 minutes from 8am to 6pm on weekdays\"\n\n# Option 4: Show next N run times\n/cron \"0 */6 * * *\" --next 10\n\n# Option 5: Validate an expression\n/cron \"0 25 * * *\" --validate # 25th hour — invalid\n\n# Option 6: Scan project files for cron schedules\n/cron --scan . # scans GitHub Actions, Kubernetes YAML, crontab\n/cron --scan .github/workflows/\n\n# Option 7: Detect flavor\n/cron \"0/5 * * * ? *\" --flavor aws-eventbridge","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1: Parse and Explain Cron Expression","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from datetime import datetime, timedelta\nimport re\nfrom typing import Optional\n\n\n# ── Special strings ──────────────────────────────────────────────────────────\n\nSPECIAL_STRINGS = {\n '@yearly': ('0 0 1 1 *', 'Once a year, at midnight on January 1st'),\n '@annually': ('0 0 1 1 *', 'Once a year, at midnight on January 1st'),\n '@monthly': ('0 0 1 * *', 'Once a month, at midnight on the 1st'),\n '@weekly': ('0 0 * * 0', 'Once a week, at midnight on Sunday'),\n '@daily': ('0 0 * * *', 'Every day at midnight'),\n '@midnight': ('0 0 * * *', 'Every day at midnight'),\n '@hourly': ('0 * * * *', 'Every hour, at the top of the hour'),\n '@reboot': (None, 'Once, on system startup/reboot'),\n '@every_1m': ('* * * * *', 'Every minute'),\n '@every_5m': ('*/5 * * * *','Every 5 minutes'),\n '@every_1h': ('0 * * * *', 'Every hour'),\n}\n\nWEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',\n 'Thursday', 'Friday', 'Saturday']\nMONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December']\n\n\ndef detect_flavor(expression: str) -> str:\n \"\"\"Detect cron flavor from expression structure.\"\"\"\n parts = expression.strip().split()\n if len(parts) == 6:\n # Could be 6-field with seconds (Quartz/Spring) or 6-field Linux\n if re.search(r'[?L#W]', parts[5]):\n return 'quartz'\n return '6field-seconds'\n if len(parts) == 7:\n return 'quartz-7field'\n if len(parts) == 5:\n return 'standard'\n return 'unknown'\n\n\ndef explain_field(value: str, field: str, names: Optional[list] = None) -> str:\n \"\"\"Convert a single cron field to English.\"\"\"\n\n if value == '*':\n return None # \"every\" is implied\n\n if value == '?':\n return None # Quartz \"any\" — implied\n\n # Step: */N or start/N\n step_match = re.match(r'^(\\*|\\d+)/(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if step_match:\n start = step_match.group(1)\n step = int(step_match.group(2))\n if field == 'minute':\n return f'every {step} minute{\"s\" if step > 1 else \"\"}'\n if field == 'hour':\n return f'every {step} hour{\"s\" if step > 1 else \"\"}'\n if field == 'day_of_month':\n return f'every {step} day{\"s\" if step > 1 else \"\"}'\n if field == 'month':\n return f'every {step} month{\"s\" if step > 1 else \"\"}'\n if field == 'second':\n return f'every {step} second{\"s\" if step > 1 else \"\"}'\n return f'every {step}'\n\n # Range: N-M\n range_match = re.match(r'^(\\d+)-(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if range_match:\n lo, hi = int(range_match.group(1)), int(range_match.group(2))\n if names:\n lo_name = names[lo] if lo \u003c len(names) else str(lo)\n hi_name = names[hi] if hi \u003c len(names) else str(hi)\n return f'{lo_name} through {hi_name}'\n if field == 'hour':\n lo_fmt = f'{lo}:00 {\"AM\" if lo \u003c 12 else \"PM\"}'\n hi_fmt = f'{hi % 12 or 12}:00 {\"AM\" if hi \u003c 12 else \"PM\"}'\n return f'between {lo_fmt} and {hi_fmt}'\n return f'{lo} to {hi}'\n\n # Range with step: N-M/S\n range_step_match = re.match(r'^(\\d+)-(\\d+)/(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if range_step_match:\n lo = int(range_step_match.group(1))\n hi = int(range_step_match.group(2))\n step = int(range_step_match.group(3))\n range_desc = explain_field(f'{lo}-{hi}', field, names)\n return f'every {step} {field.replace(\"_\", \" \")}{\"s\" if step > 1 else \"\"} {range_desc}'\n\n # List: N,M,P\n if ',' in value:\n parts = [p.strip() for p in value.split(',')]\n if names:\n named = [names[int(p)] if p.isdigit() and int(p) \u003c len(names) else p for p in parts]\n else:\n named = parts\n if len(named) == 2:\n return f'{named[0]} and {named[1]}'\n return ', '.join(named[:-1]) + f', and {named[-1]}'\n\n # Single value\n if value.isdigit():\n n = int(value)\n if field == 'hour':\n if n == 0: return 'midnight'\n if n == 12: return 'noon'\n if n \u003c 12: return f'{n}:00 AM'\n return f'{n - 12}:00 PM'\n if field == 'minute':\n return f'minute {n}' if n != 0 else 'on the hour'\n if field == 'second':\n return f'second {n}'\n if field == 'day_of_month':\n suffix = 'st' if n % 10 == 1 and n != 11 else \\\n 'nd' if n % 10 == 2 and n != 12 else \\\n 'rd' if n % 10 == 3 and n != 13 else 'th'\n return f'the {n}{suffix}'\n if field == 'month' and names:\n return names[n - 1] if 1 \u003c= n \u003c= 12 else str(n)\n if field == 'day_of_week' and names:\n return names[n % 7]\n return str(n)\n\n # Special Quartz: L (last), W (nearest weekday), # (Nth weekday)\n if value.upper() == 'L' and field == 'day_of_month':\n return 'the last day of the month'\n if value.upper() == 'L' and field == 'day_of_week':\n return 'the last weekday of the month'\n lw_match = re.match(r'^(\\d+)W

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value, re.I)\n if lw_match:\n return f'the weekday nearest the {lw_match.group(1)}th'\n hash_match = re.match(r'^(\\d+)#(\\d+)

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…

, value)\n if hash_match:\n day = int(hash_match.group(1))\n nth = int(hash_match.group(2))\n ordinals = ['', '1st', '2nd', '3rd', '4th', '5th']\n day_name = WEEKDAY_NAMES[day % 7]\n return f'the {ordinals[nth]} {day_name} of the month'\n\n return value # fallback — return as-is\n\n\ndef explain_cron(expression: str) -> dict:\n \"\"\"\n Parse a cron expression and return a human-readable explanation.\n Returns dict with: explanation, flavor, issues, fields\n \"\"\"\n expr = expression.strip()\n\n # Handle special strings\n expr_lower = expr.lower()\n if expr_lower in SPECIAL_STRINGS:\n cron_equiv, desc = SPECIAL_STRINGS[expr_lower]\n return {\n 'expression': expr,\n 'flavor': 'special',\n 'explanation': desc,\n 'next_runs': [] if expr_lower == '@reboot' else None,\n 'issues': [],\n 'fields': {},\n }\n\n # Normalize: replace month/weekday names with numbers\n expr = expr.upper()\n month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,\n 'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}\n dow_map = {'SUN':0,'MON':1,'TUE':2,'WED':3,'THU':4,'FRI':5,'SAT':6}\n for name, num in {**month_map, **dow_map}.items():\n expr = expr.replace(name, str(num))\n\n parts = expr.split()\n flavor = detect_flavor(expression)\n issues = []\n\n # Map parts to field names by flavor\n if flavor == '6field-seconds':\n if len(parts) != 6:\n issues.append({'severity': 'ERROR', 'message': f'Expected 6 fields, got {len(parts)}'})\n return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}\n field_names = ['second', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week']\n field_values = dict(zip(field_names, parts))\n elif len(parts) == 5:\n field_names = ['minute', 'hour', 'day_of_month', 'month', 'day_of_week']\n field_values = dict(zip(field_names, parts))\n else:\n issues.append({'severity': 'ERROR', 'message': f'Unrecognized format: {len(parts)} fields'})\n return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}\n\n # Explain each field\n minute_desc = explain_field(field_values.get('minute', '*'), 'minute')\n hour_desc = explain_field(field_values.get('hour', '*'), 'hour')\n dom_desc = explain_field(field_values.get('day_of_month', '*'), 'day_of_month')\n month_desc = explain_field(field_values.get('month', '*'), 'month', MONTH_NAMES)\n dow_desc = explain_field(field_values.get('day_of_week', '*'), 'day_of_week', WEEKDAY_NAMES)\n sec_desc = explain_field(field_values.get('second', None) or '0', 'second') if flavor == '6field-seconds' else None\n\n # Compose natural language description\n parts_en = []\n\n # Frequency\n if minute_desc and minute_desc.startswith('every'):\n parts_en.append(minute_desc.capitalize())\n elif minute_desc and minute_desc.startswith('minute'):\n if hour_desc:\n parts_en.append(f'At {hour_desc}, {minute_desc}')\n else:\n parts_en.append(f'Every hour, {minute_desc}')\n elif not minute_desc and not hour_desc:\n parts_en.append('Every minute')\n elif not minute_desc and hour_desc:\n parts_en.append(f'Every hour at {hour_desc}')\n elif minute_desc == 'on the hour' and hour_desc:\n parts_en.append(f'At {hour_desc}')\n else:\n if hour_desc and minute_desc:\n # Format as clock time if both are single values\n parts_en.append(f'At {hour_desc}')\n if minute_desc and not minute_desc.startswith('on the hour'):\n parts_en[-1] += f', {minute_desc}'\n elif hour_desc:\n parts_en.append(f'At {hour_desc}')\n elif minute_desc:\n parts_en.append(f'Every hour, at {minute_desc}')\n\n if sec_desc and sec_desc != 'second 0':\n parts_en.append(f'at {sec_desc}')\n\n # Day constraints\n if dom_desc and dow_desc:\n parts_en.append(f'on {dom_desc} or {dow_desc}')\n elif dom_desc:\n parts_en.append(f'on {dom_desc}')\n elif dow_desc:\n parts_en.append(f'on {dow_desc}{\"s\" if not dow_desc.endswith(\"day\") else \"\"}')\n\n # Month constraint\n if month_desc:\n parts_en.append(f'in {month_desc}')\n\n explanation = ', '.join(parts_en) if parts_en else 'Every minute'\n\n # ── Validation ────────────────────────────────────────────────────────────\n\n # Every minute in production\n if field_values.get('minute', '') == '*' and field_values.get('hour', '') == '*':\n issues.append({\n 'severity': 'WARNING',\n 'message': 'Runs every minute — confirm this is intentional for a production schedule',\n })\n\n # Validate numeric bounds\n BOUNDS = {\n 'minute': (0, 59), 'hour': (0, 23),\n 'day_of_month': (1, 31), 'month': (1, 12), 'day_of_week': (0, 7),\n 'second': (0, 59),\n }\n for fname, fval in field_values.items():\n if fname not in BOUNDS or fval in ('*', '?'):\n continue\n lo, hi = BOUNDS[fname]\n # Extract all numeric values\n nums = re.findall(r'\\d+', fval)\n for n_str in nums:\n n = int(n_str)\n if n \u003c lo or n > hi:\n issues.append({\n 'severity': 'ERROR',\n 'message': f'Invalid {fname} value: {n} (valid range: {lo}-{hi})',\n })\n\n # February 30 / 31 impossibility\n month_val = field_values.get('month', '*')\n dom_val = field_values.get('day_of_month', '*')\n if month_val == '2' and dom_val not in ('*', '?'):\n days = re.findall(r'\\d+', dom_val)\n for d in days:\n if int(d) > 29:\n issues.append({\n 'severity': 'ERROR',\n 'message': f'day-of-month={d} in month=2 (February) — this date never occurs',\n })\n elif int(d) == 29:\n issues.append({\n 'severity': 'WARNING',\n 'message': 'day-of-month=29 in February — only runs on leap years',\n })\n\n # Detect conflicting DOW + DOM (both specified — runs on OR condition, often surprising)\n if dom_val not in ('*', '?') and field_values.get('day_of_week', '*') not in ('*', '?'):\n issues.append({\n 'severity': 'INFO',\n 'message': (\n 'Both day-of-month and day-of-week are specified — '\n 'standard cron uses OR (runs if EITHER matches). '\n 'Use ? for one field if you want AND behavior (Quartz only).'\n ),\n })\n\n return {\n 'expression': expression,\n 'flavor': flavor,\n 'explanation': explanation,\n 'fields': field_values,\n 'issues': issues,\n }","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 2: Calculate Next Run Times","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from datetime import datetime, timezone\n\ndef next_runs(expression: str, count: int = 5, after: Optional[datetime] = None) -> list[datetime]:\n \"\"\"\n Calculate the next N run times for a cron expression.\n Uses croniter if available, otherwise falls back to manual calculation.\n \"\"\"\n after = after or datetime.now(tz=timezone.utc)\n\n try:\n from croniter import croniter\n cron = croniter(expression, after)\n return [cron.get_next(datetime) for _ in range(count)]\n except ImportError:\n pass\n\n # Fallback: install croniter\n import subprocess\n subprocess.run(['pip', 'install', 'croniter', '-q'], check=True)\n from croniter import croniter\n cron = croniter(expression, after)\n return [cron.get_next(datetime) for _ in range(count)]\n\n\ndef format_run_times(runs: list[datetime], timezone_name: str = 'UTC') -> list[str]:\n \"\"\"Format run times for display.\"\"\"\n now = datetime.now(tz=timezone.utc)\n result = []\n for dt in runs:\n diff = dt - now\n if diff.total_seconds() \u003c 60:\n rel = 'in less than a minute'\n elif diff.total_seconds() \u003c 3600:\n mins = int(diff.total_seconds() / 60)\n rel = f'in {mins} minute{\"s\" if mins > 1 else \"\"}'\n elif diff.total_seconds() \u003c 86400:\n hours = int(diff.total_seconds() / 3600)\n rel = f'in {hours} hour{\"s\" if hours > 1 else \"\"}'\n else:\n days = int(diff.total_seconds() / 86400)\n rel = f'in {days} day{\"s\" if days > 1 else \"\"}'\n\n result.append(f'{dt.strftime(\"%Y-%m-%d %H:%M:%S\")} UTC ({rel})')\n return result","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 3: English → Cron Converter","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import re\n\ndef english_to_cron(description: str) -> dict:\n \"\"\"\n Convert a plain-English schedule description to cron expression.\n Returns {'cron': str, 'explanation': str, 'confidence': str}\n \"\"\"\n desc = description.lower().strip()\n\n # Special patterns\n if re.search(r'every\\s+minute', desc):\n return {'cron': '* * * * *', 'explanation': 'Every minute', 'confidence': 'HIGH'}\n\n if re.search(r'every\\s+hour', desc):\n return {'cron': '0 * * * *', 'explanation': 'Every hour', 'confidence': 'HIGH'}\n\n if re.search(r'midnight|every\\s+(?:day\\s+at\\s+)?12\\s*am', desc):\n return {'cron': '0 0 * * *', 'explanation': 'Every day at midnight', 'confidence': 'HIGH'}\n\n if re.search(r'noon|every\\s+(?:day\\s+at\\s+)?12\\s*pm', desc):\n return {'cron': '0 12 * * *', 'explanation': 'Every day at noon', 'confidence': 'HIGH'}\n\n # Extract time: \"at 9am\", \"at 3:30pm\", \"at 14:00\"\n time_match = re.search(\n r'at\\s+(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?',\n desc, re.I\n )\n hour = minute = None\n if time_match:\n hour = int(time_match.group(1))\n minute = int(time_match.group(2)) if time_match.group(2) else 0\n ampm = (time_match.group(3) or '').lower()\n if ampm == 'pm' and hour != 12:\n hour += 12\n elif ampm == 'am' and hour == 12:\n hour = 0\n\n # Day of week\n dow_map = {\n 'sunday': 0, 'monday': 1, 'tuesday': 2, 'wednesday': 3,\n 'thursday': 4, 'friday': 5, 'saturday': 6,\n 'weekday': '1-5', 'weekdays': '1-5',\n 'weekend': '0,6', 'weekends': '0,6',\n 'every day': '*', 'daily': '*',\n }\n\n dow = '*'\n for key, val in dow_map.items():\n if key in desc:\n dow = str(val)\n break\n\n # Interval: \"every N minutes/hours\"\n interval_match = re.search(r'every\\s+(\\d+)\\s+(minute|hour|min|hr)s?', desc, re.I)\n if interval_match:\n n = int(interval_match.group(1))\n unit = interval_match.group(2).lower()\n\n # Business hours modifier\n if re.search(r'business\\s+hours?|work\\s+hours?|9\\s*(?:am|to)\\s*5', desc):\n if unit in ('minute', 'min'):\n cron = f'*/{n} 9-17 * * 1-5'\n return {'cron': cron,\n 'explanation': f'Every {n} minutes, between 9am and 5pm, Monday–Friday',\n 'confidence': 'HIGH'}\n if unit in ('hour', 'hr'):\n cron = f'0 */{ n} * * 1-5' if n > 1 else f'0 9-17 * * 1-5'\n return {'cron': cron,\n 'explanation': f'Every {n} hour{\"s\" if n>1 else \"\"}, weekdays only',\n 'confidence': 'MEDIUM'}\n\n if unit in ('minute', 'min'):\n cron_min = f'*/{n}' if n > 1 else '*'\n cron = f'{cron_min} * * * *'\n return {'cron': cron,\n 'explanation': f'Every {n} minute{\"s\" if n>1 else \"\"}',\n 'confidence': 'HIGH'}\n\n if unit in ('hour', 'hr'):\n cron = f'0 */{n} * * *'\n return {'cron': cron,\n 'explanation': f'Every {n} hour{\"s\" if n>1 else \"\"}',\n 'confidence': 'HIGH'}\n\n # \"first day of every month\"\n if re.search(r'first\\s+day\\s+of\\s+(?:every|each)\\s+month', desc):\n h = hour if hour is not None else 0\n m = minute if minute is not None else 0\n cron = f'{m} {h} 1 * *'\n time_str = f'at {h:02d}:{m:02d}' if hour is not None else 'at midnight'\n return {'cron': cron,\n 'explanation': f'On the 1st of every month, {time_str}',\n 'confidence': 'HIGH'}\n\n # \"every weekday at Xam\"\n if hour is not None and dow != '*':\n m = minute if minute is not None else 0\n cron = f'{m} {hour} * * {dow}'\n dow_names = {'1-5': 'Monday–Friday', '0,6': 'Saturday and Sunday',\n '0': 'Sunday', '1': 'Monday', '*': 'every day'}\n dow_str = dow_names.get(str(dow), f'day {dow}')\n return {'cron': cron,\n 'explanation': f'At {hour:02d}:{m:02d}, {dow_str}',\n 'confidence': 'HIGH'}\n\n # Simple \"every day at X\"\n if hour is not None:\n m = minute if minute is not None else 0\n cron = f'{m} {hour} * * *'\n am_pm = 'AM' if hour \u003c 12 else 'PM'\n h12 = hour % 12 or 12\n return {'cron': cron,\n 'explanation': f'Every day at {h12}:{m:02d} {am_pm}',\n 'confidence': 'HIGH'}\n\n return {\n 'cron': None,\n 'explanation': 'Could not parse — try: \"every weekday at 9am\", \"every 5 minutes\", \"first day of month at midnight\"',\n 'confidence': 'NONE',\n }","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 4: Scan Project Files for Cron Schedules","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import glob\nimport re\nimport yaml # pyyaml\nfrom pathlib import Path\n\n\ndef scan_for_cron_schedules(root: str = '.') -> list[dict]:\n \"\"\"Find all cron expressions in GitHub Actions, Kubernetes, and crontab files.\"\"\"\n schedules = []\n\n # GitHub Actions schedule triggers\n for fpath in glob.glob(f'{root}/.github/workflows/*.yml', recursive=False):\n fpath += '' if fpath.endswith('.yml') else ''\n for path in glob.glob(f'{root}/.github/workflows/*.yml') + glob.glob(f'{root}/.github/workflows/*.yaml'):\n try:\n content = Path(path).read_text(errors='replace')\n doc = yaml.safe_load(content)\n if not isinstance(doc, dict):\n continue\n on = doc.get('on', doc.get(True, {}))\n if isinstance(on, dict):\n schedule = on.get('schedule', [])\n if isinstance(schedule, list):\n for item in schedule:\n if isinstance(item, dict) and 'cron' in item:\n schedules.append({\n 'file': path,\n 'source': 'github-actions',\n 'expression': item['cron'],\n 'context': doc.get('name', 'workflow'),\n })\n except Exception:\n continue\n\n # Kubernetes CronJob specs\n for path in glob.glob(f'{root}/**/*.yaml', recursive=True) + glob.glob(f'{root}/**/*.yml', recursive=True):\n if any(skip in path for skip in ['.git', 'node_modules', 'vendor']):\n continue\n try:\n content = Path(path).read_text(errors='replace')\n if 'CronJob' not in content:\n continue\n doc = yaml.safe_load(content)\n if isinstance(doc, dict) and doc.get('kind') == 'CronJob':\n schedule = (doc.get('spec', {}) or {}).get('schedule')\n if schedule:\n schedules.append({\n 'file': path,\n 'source': 'kubernetes-cronjob',\n 'expression': schedule,\n 'context': doc.get('metadata', {}).get('name', 'cronjob'),\n })\n except Exception:\n continue\n\n # crontab files\n for crontab_path in ['crontab', 'crontab.txt', 'etc/cron.d', 'docker/cron']:\n full = f'{root}/{crontab_path}'\n if not Path(full).exists():\n continue\n try:\n for i, line in enumerate(Path(full).read_text().splitlines(), 1):\n line = line.strip()\n if line.startswith('#') or not line:\n continue\n # crontab line format: min hour dom month dow command\n parts = line.split()\n if len(parts) >= 6:\n expr = ' '.join(parts[:5])\n command = ' '.join(parts[5:])\n schedules.append({\n 'file': crontab_path,\n 'source': 'crontab',\n 'expression': expr,\n 'context': command[:60],\n 'line': i,\n })\n except Exception:\n continue\n\n return schedules","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 5: Output Report","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"## Cron Expression Analysis\n\n---\n\n### Expression: `*/5 9-17 * * 1-5`\n\n**Translation:** Every 5 minutes, between 9:00 AM and 5:00 PM, on Monday through Friday\n\n**Flavor:** Standard Unix (5-field)\n\n**Field Breakdown:**\n| Field | Value | Meaning |\n|-------|-------|---------|\n| Minute | `*/5` | Every 5 minutes |\n| Hour | `9-17` | Between 9:00 AM and 5:00 PM |\n| Day-of-month | `*` | Every day |\n| Month | `*` | Every month |\n| Day-of-week | `1-5` | Monday through Friday |\n\n**Next 5 scheduled runs:**","type":"text"}]},{"type":"paragraph","content":[{"text":"2026-03-19 09:00:00 UTC (in 3 minutes) 2026-03-19 09:05:00 UTC (in 8 minutes) 2026-03-19 09:10:00 UTC (in 13 minutes) 2026-03-19 09:15:00 UTC (in 18 minutes) 2026-03-19 09:20:00 UTC (in 23 minutes)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n**Issues:** None ✅\n\n---\n\n### Expression: `0 0 30 2 *`\n\n**Translation:** At midnight, on the 30th of February\n\n**Issues:**\n- 🔴 ERROR: `day-of-month=30 in month=2 (February)` — **this date never occurs**. Job will never run.\n\n**Fix:** Did you mean the last day of every month?","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Last day of every month (Quartz): 0 0 L * *","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"28th of February specifically: 0 0 28 2 *","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Every month on the 30th: 0 0 30 * * (skips Feb, Apr, Jun, Sep, Nov)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n---\n\n### English → Cron Examples\n\n| Input | Output | Explanation |\n|-------|--------|-------------|\n| `\"every weekday at 9am\"` | `0 9 * * 1-5` | At 09:00, Monday–Friday |\n| `\"every 15 minutes during business hours\"` | `*/15 9-17 * * 1-5` | Every 15 min, 9am–5pm, Mon–Fri |\n| `\"first day of every month at midnight\"` | `0 0 1 * *` | At midnight on the 1st |\n| `\"every Sunday at 2:30am\"` | `30 2 * * 0` | At 02:30, Sunday |\n| `\"every 6 hours\"` | `0 */6 * * *` | At minute 0, every 6 hours |\n\n---\n\n### Project Scan: .github/workflows/\n\nFound 3 cron schedules:\n\n| File | Schedule | Explanation | Issues |\n|------|----------|-------------|--------|\n| `.github/workflows/deploy.yml` | `0 2 * * 1` | Every Monday at 2:00 AM | ✅ OK |\n| `.github/workflows/cleanup.yml` | `*/5 * * * *` | Every 5 minutes | ⚠️ Very frequent — confirm intentional |\n| `.github/workflows/report.yml` | `0 0 30 2 *` | Feb 30th at midnight | 🔴 NEVER RUNS (Feb 30 doesn't exist) |\n\n---\n\n### Flavor Reference\n\n| Flavor | Fields | Example | Notes |\n|--------|--------|---------|-------|\n| Standard Unix | `min hour dom month dow` | `0 2 * * 1` | `cron`, `crontab` |\n| 6-field (seconds) | `sec min hour dom month dow` | `0 0 2 * * 1` | Spring, Quartz |\n| GitHub Actions | `min hour dom month dow` | `'0 2 * * 1'` | Under `on.schedule.cron` |\n| AWS EventBridge | `min hour dom month dow year` | `0 2 ? * MON *` | Uses `?` for DOM/DOW |\n| Kubernetes | `min hour dom month dow` | `0 2 * * 1` | `.spec.schedule` |","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Mode Output","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Cron: */5 9-17 * * 1-5\n\nEvery 5 minutes, between 9:00 AM and 5:00 PM, Monday–Friday\n\nNext run: 2026-03-19 09:00:00 UTC (in 3 minutes)\nIssues: none\n\nTo convert English → cron:\n \"every weekday at 9am\" → 0 9 * * 1-5\n \"first of month midnight\" → 0 0 1 * *","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","author":"@skillopedia","source":{"stars":2012,"repo_name":"openclaw-master-skills","origin_url":"https://github.com/leoyeai/openclaw-master-skills/blob/HEAD/skills/phy-cron-explainer/SKILL.md","repo_owner":"leoyeai","body_sha256":"484b2309e273a1b6f78a64d05036cc5dd04a2298abf8299cb2347cc063334e91","cluster_key":"77a4213d0a18b194044841fe1ab49835db3bbac8e8dcffcbd98965bebc577817","clean_bundle":{"format":"clean-skill-bundle-v1","source":"leoyeai/openclaw-master-skills/skills/phy-cron-explainer/SKILL.md","attachments":[{"id":"9c59619c-bdda-5f08-be60-c326479b96d9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c59619c-bdda-5f08-be60-c326479b96d9/attachment.json","path":"_meta.json","size":288,"sha256":"ef5616da8aef70accb9d4666f953b4d324ac1940b15664ca897de30037adc286","contentType":"application/json; charset=utf-8"}],"bundle_sha256":"e6a5960824c01aff912d2ea993b2a9a3d7bfb1263a2faf7770619bb5077116a6","attachment_count":1,"text_attachments":1,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/phy-cron-explainer/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"general","import_tag":"clean-skills-v1","_yaml_error":"YAMLException: bad indentation of a mapping entry (2:539)\n\n 1 | ... \n 2 | ... 5\"). Supports all major flavors: standard Unix 5-field, 6-field ...\n-----------------------------------------^\n 3 | ..."}},"renderedAt":1782979659747}

Cron Explainer What does that mean? Every Monday at 2am. How about ? Every 5 minutes between 9am and 5pm on weekdays. You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot. All major cron flavors. Zero external API. --- Trigger Phrases - "cron expression", "explain this cron", "what does cron mean" - "cron to English", "translate cron" - "…