Agent skill

Phy Cron Explainer

by LeoYeAI in LeoYeAI/openclaw-master-skills

Cron expression explainer, validator, and converter. An agent skill from LeoYeAI/openclaw-master-skills.

Apache-2.0Auto-check passedProductivity & Automation

Install Phy Cron Explainer

skills CLI
$ npx skills add LeoYeAI/openclaw-master-skills --skill phy-cron-explainer -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install LeoYeAI/openclaw-master-skills phy-cron-explainer --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/LeoYeAI/openclaw-master-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/phy-cron-explainer .claude/skills/phy-cron-explainer && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
phy-cron-explainer
GitHub stars
2.2k
Token cost
~7k tokens
SKILL.md length
218 words
Files
2
Skills in repo
1,215
Repo updated
First seen
Licence
Apache-2.0

At a glance

Cron expression explainer, validator, and converter. An agent skill from LeoYeAI/openclaw-master-skills.

  • Works in 5 steps: Parse and Explain Cron Expression → Calculate Next Run Times → English → Cron Converter → …
  • Cron expression
  • SKILL.md covers Trigger Phrases, How to Provide Input, Step 1: Parse and Explain Cron… and Step 2: Calculate Next Run Times, plus 4 more sections
  • Calls aws

What it does

Phy Cron Explainer is an agent skill from LeoYeAI/openclaw-master-skills. Cron expression explainer, validator, and converter. Translates any cron expression into plain English ("0 /6 " → "Every 6 hours, at minute 0"), shows the next 5 scheduled run times with exact dates, detects impossible/conflicting expressions (Feb 30, conflicting day-of-week + day-of-month), identifies suspicious patterns (every minute in production, very-frequent schedules on expensive operations), and converts plain English descriptions to cron ("every weekday at 9am" → "0 9 1-5"). Supports all major flavors…

Its SKILL.md is about 7k tokens, which your agent loads only when the skill is triggered. The skill folder holds 1 other file (for example `_meta.json`).

It sits in Productivity & Automation, covering Scheduled and recurring tasks, Plain language and style rules and Container orchestration. It works with GitHub Actions, Kubernetes and Amazon Web Services. The repository describes itself as: 🧠 Curated collection of 1209+ best OpenClaw skills — weekly updated by MyClaw.ai. The licence is Apache-2.0.

When your agent uses it

  • Cron expression
  • What does this cron mean
  • Cron to English

Example prompts

  • “Every 6 hours, at minute 0”
  • “every weekday at 9am”
  • “cron expression”
  • “/phy-cron-explainer”

Requirements

  • Python 3

Workflow steps

5 steps, taken from the step headings in SKILL.md.

  1. Parse and Explain Cron Expression
  2. Calculate Next Run Times
  3. English → Cron Converter
  4. Scan Project Files for Cron Schedules
  5. Output Report

What it can do on your machine

Read from SKILL.md and the folder at commit e5199b5. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • aws

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use aws, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Phy Cron Explainer loads about 7k tokens when it runs. Until then it costs about 250 tokens; SKILL.md has 218 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~250
When it runs · the whole SKILL.md, loaded when a task matches
~7k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from LeoYeAI/openclaw-master-skills at commit e5199b5, republished under its Apache-2.0 licence (© LeoYeAI). 218 words, ~6,997 tokens.

Download SKILL.mdSave it as .claude/skills/phy-cron-explainer/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
phy-cron-explainer
description
Cron expression explainer, validator, and converter. Translates any cron expression into plain English ("0 */6 * * *" → "Every 6 hours, at minute 0"), shows the next 5 scheduled run times with exact dates, detects impossible/conflicting expressions (Feb 30, conflicting day-of-week + day-of-month), identifies suspicious patterns (every minute in production, very-frequent schedules on expensive operations), and converts plain English descriptions to cron ("every weekday at 9am" → "0 9 * * 1-5"). Supports all major flavors: standard Unix 5-field, 6-field with seconds, AWS EventBridge, GitHub Actions schedule, Kubernetes CronJob, Spring @Scheduled, and special strings (@yearly, @monthly, @daily, @hourly, @reboot). Also scans .github/workflows/ and Kubernetes YAML files for cron schedules and audits them. Zero external API — pure local parsing. Triggers on "cron expression", "what does this cron mean", "cron to English", "cron schedule", "explain cron", "@reboot", "/cron".
license
Apache-2.0
metadata
author: PHY041 version: "1.0.0" tags: - cron - scheduling - devops - developer-tools - kubernetes - github-actions - aws - automation - ci-cd - utilities

Cron Explainer

0 2 * * 1

What does that mean? Every Monday at 2am. How about */5 9-17 * * 1-5? 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"
  • "cron schedule", "next run time"
  • "English to cron", "convert to cron"
  • "cron syntax", "cron debug"
  • "kubernetes cronjob", "github actions schedule"
  • "/cron"

How to Provide Input

bash
# Option 1: Explain a cron expression
/cron "0 2 * * 1"
/cron "*/5 9-17 * * 1-5"
/cron "0 0 1,15 * *"

# Option 2: Explain a special string
/cron "@daily"
/cron "@reboot"
/cron "@every 6h"    # Kubernetes/Go format

# Option 3: Convert English to cron
/cron "every weekday at 9am"
/cron "every 5 minutes during business hours"
/cron "first day of every month at midnight"
/cron "every 15 minutes from 8am to 6pm on weekdays"

# Option 4: Show next N run times
/cron "0 */6 * * *" --next 10

# Option 5: Validate an expression
/cron "0 25 * * *" --validate    # 25th hour — invalid

# Option 6: Scan project files for cron schedules
/cron --scan .                    # scans GitHub Actions, Kubernetes YAML, crontab
/cron --scan .github/workflows/

# Option 7: Detect flavor
/cron "0/5 * * * ? *" --flavor aws-eventbridge

Step 1: Parse and Explain Cron Expression

python
from datetime import datetime, timedelta
import re
from typing import Optional


# ── Special strings ──────────────────────────────────────────────────────────

SPECIAL_STRINGS = {
    '@yearly':   ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@annually': ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@monthly':  ('0 0 1 * *',  'Once a month, at midnight on the 1st'),
    '@weekly':   ('0 0 * * 0',  'Once a week, at midnight on Sunday'),
    '@daily':    ('0 0 * * *',  'Every day at midnight'),
    '@midnight': ('0 0 * * *',  'Every day at midnight'),
    '@hourly':   ('0 * * * *',  'Every hour, at the top of the hour'),
    '@reboot':   (None,         'Once, on system startup/reboot'),
    '@every_1m': ('* * * * *',  'Every minute'),
    '@every_5m': ('*/5 * * * *','Every 5 minutes'),
    '@every_1h': ('0 * * * *',  'Every hour'),
}

WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
                 'Thursday', 'Friday', 'Saturday']
MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
               'July', 'August', 'September', 'October', 'November', 'December']


def detect_flavor(expression: str) -> str:
    """Detect cron flavor from expression structure."""
    parts = expression.strip().split()
    if len(parts) == 6:
        # Could be 6-field with seconds (Quartz/Spring) or 6-field Linux
        if re.search(r'[?L#W]', parts[5]):
            return 'quartz'
        return '6field-seconds'
    if len(parts) == 7:
        return 'quartz-7field'
    if len(parts) == 5:
        return 'standard'
    return 'unknown'


def explain_field(value: str, field: str, names: Optional[list] = None) -> str:
    """Convert a single cron field to English."""

    if value == '*':
        return None  # "every" is implied

    if value == '?':
        return None  # Quartz "any" — implied

    # Step: */N or start/N
    step_match = re.match(r'^(\*|\d+)/(\d+)$', value)
    if step_match:
        start = step_match.group(1)
        step = int(step_match.group(2))
        if field == 'minute':
            return f'every {step} minute{"s" if step > 1 else ""}'
        if field == 'hour':
            return f'every {step} hour{"s" if step > 1 else ""}'
        if field == 'day_of_month':
            return f'every {step} day{"s" if step > 1 else ""}'
        if field == 'month':
            return f'every {step} month{"s" if step > 1 else ""}'
        if field == 'second':
            return f'every {step} second{"s" if step > 1 else ""}'
        return f'every {step}'

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

    # Range with step: N-M/S
    range_step_match = re.match(r'^(\d+)-(\d+)/(\d+)$', value)
    if range_step_match:
        lo = int(range_step_match.group(1))
        hi = int(range_step_match.group(2))
        step = int(range_step_match.group(3))
        range_desc = explain_field(f'{lo}-{hi}', field, names)
        return f'every {step} {field.replace("_", " ")}{"s" if step > 1 else ""} {range_desc}'

    # List: N,M,P
    if ',' in value:
        parts = [p.strip() for p in value.split(',')]
        if names:
            named = [names[int(p)] if p.isdigit() and int(p) < len(names) else p for p in parts]
        else:
            named = parts
        if len(named) == 2:
            return f'{named[0]} and {named[1]}'
        return ', '.join(named[:-1]) + f', and {named[-1]}'

    # Single value
    if value.isdigit():
        n = int(value)
        if field == 'hour':
            if n == 0: return 'midnight'
            if n == 12: return 'noon'
            if n < 12: return f'{n}:00 AM'
            return f'{n - 12}:00 PM'
        if field == 'minute':
            return f'minute {n}' if n != 0 else 'on the hour'
        if field == 'second':
            return f'second {n}'
        if field == 'day_of_month':
            suffix = 'st' if n % 10 == 1 and n != 11 else \
                     'nd' if n % 10 == 2 and n != 12 else \
                     'rd' if n % 10 == 3 and n != 13 else 'th'
            return f'the {n}{suffix}'
        if field == 'month' and names:
            return names[n - 1] if 1 <= n <= 12 else str(n)
        if field == 'day_of_week' and names:
            return names[n % 7]
        return str(n)

    # Special Quartz: L (last), W (nearest weekday), # (Nth weekday)
    if value.upper() == 'L' and field == 'day_of_month':
        return 'the last day of the month'
    if value.upper() == 'L' and field == 'day_of_week':
        return 'the last weekday of the month'
    lw_match = re.match(r'^(\d+)W$', value, re.I)
    if lw_match:
        return f'the weekday nearest the {lw_match.group(1)}th'
    hash_match = re.match(r'^(\d+)#(\d+)$', value)
    if hash_match:
        day = int(hash_match.group(1))
        nth = int(hash_match.group(2))
        ordinals = ['', '1st', '2nd', '3rd', '4th', '5th']
        day_name = WEEKDAY_NAMES[day % 7]
        return f'the {ordinals[nth]} {day_name} of the month'

    return value  # fallback — return as-is


def explain_cron(expression: str) -> dict:
    """
    Parse a cron expression and return a human-readable explanation.
    Returns dict with: explanation, flavor, issues, fields
    """
    expr = expression.strip()

    # Handle special strings
    expr_lower = expr.lower()
    if expr_lower in SPECIAL_STRINGS:
        cron_equiv, desc = SPECIAL_STRINGS[expr_lower]
        return {
            'expression': expr,
            'flavor': 'special',
            'explanation': desc,
            'next_runs': [] if expr_lower == '@reboot' else None,
            'issues': [],
            'fields': {},
        }

    # Normalize: replace month/weekday names with numbers
    expr = expr.upper()
    month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,
                 'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}
    dow_map = {'SUN':0,'MON':1,'TUE':2,'WED':3,'THU':4,'FRI':5,'SAT':6}
    for name, num in {**month_map, **dow_map}.items():
        expr = expr.replace(name, str(num))

    parts = expr.split()
    flavor = detect_flavor(expression)
    issues = []

    # Map parts to field names by flavor
    if flavor == '6field-seconds':
        if len(parts) != 6:
            issues.append({'severity': 'ERROR', 'message': f'Expected 6 fields, got {len(parts)}'})
            return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}
        field_names = ['second', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week']
        field_values = dict(zip(field_names, parts))
    elif len(parts) == 5:
        field_names = ['minute', 'hour', 'day_of_month', 'month', 'day_of_week']
        field_values = dict(zip(field_names, parts))
    else:
        issues.append({'severity': 'ERROR', 'message': f'Unrecognized format: {len(parts)} fields'})
        return {'expression': expression, 'flavor': flavor, 'explanation': 'Invalid', 'issues': issues, 'fields': {}}

    # Explain each field
    minute_desc = explain_field(field_values.get('minute', '*'), 'minute')
    hour_desc = explain_field(field_values.get('hour', '*'), 'hour')
    dom_desc = explain_field(field_values.get('day_of_month', '*'), 'day_of_month')
    month_desc = explain_field(field_values.get('month', '*'), 'month', MONTH_NAMES)
    dow_desc = explain_field(field_values.get('day_of_week', '*'), 'day_of_week', WEEKDAY_NAMES)
    sec_desc = explain_field(field_values.get('second', None) or '0', 'second') if flavor == '6field-seconds' else None

    # Compose natural language description
    parts_en = []

    # Frequency
    if minute_desc and minute_desc.startswith('every'):
        parts_en.append(minute_desc.capitalize())
    elif minute_desc and minute_desc.startswith('minute'):
        if hour_desc:
            parts_en.append(f'At {hour_desc}, {minute_desc}')
        else:
            parts_en.append(f'Every hour, {minute_desc}')
    elif not minute_desc and not hour_desc:
        parts_en.append('Every minute')
    elif not minute_desc and hour_desc:
        parts_en.append(f'Every hour at {hour_desc}')
    elif minute_desc == 'on the hour' and hour_desc:
        parts_en.append(f'At {hour_desc}')
    else:
        if hour_desc and minute_desc:
            # Format as clock time if both are single values
            parts_en.append(f'At {hour_desc}')
            if minute_desc and not minute_desc.startswith('on the hour'):
                parts_en[-1] += f', {minute_desc}'
        elif hour_desc:
            parts_en.append(f'At {hour_desc}')
        elif minute_desc:
            parts_en.append(f'Every hour, at {minute_desc}')

    if sec_desc and sec_desc != 'second 0':
        parts_en.append(f'at {sec_desc}')

    # Day constraints
    if dom_desc and dow_desc:
        parts_en.append(f'on {dom_desc} or {dow_desc}')
    elif dom_desc:
        parts_en.append(f'on {dom_desc}')
    elif dow_desc:
        parts_en.append(f'on {dow_desc}{"s" if not dow_desc.endswith("day") else ""}')

    # Month constraint
    if month_desc:
        parts_en.append(f'in {month_desc}')

    explanation = ', '.join(parts_en) if parts_en else 'Every minute'

    # ── Validation ────────────────────────────────────────────────────────────

    # Every minute in production
    if field_values.get('minute', '') == '*' and field_values.get('hour', '') == '*':
        issues.append({
            'severity': 'WARNING',
            'message': 'Runs every minute — confirm this is intentional for a production schedule',
        })

    # Validate numeric bounds
    BOUNDS = {
        'minute': (0, 59), 'hour': (0, 23),
        'day_of_month': (1, 31), 'month': (1, 12), 'day_of_week': (0, 7),
        'second': (0, 59),
    }
    for fname, fval in field_values.items():
        if fname not in BOUNDS or fval in ('*', '?'):
            continue
        lo, hi = BOUNDS[fname]
        # Extract all numeric values
        nums = re.findall(r'\d+', fval)
        for n_str in nums:
            n = int(n_str)
            if n < lo or n > hi:
                issues.append({
                    'severity': 'ERROR',
                    'message': f'Invalid {fname} value: {n} (valid range: {lo}-{hi})',
                })

    # February 30 / 31 impossibility
    month_val = field_values.get('month', '*')
    dom_val = field_values.get('day_of_month', '*')
    if month_val == '2' and dom_val not in ('*', '?'):
        days = re.findall(r'\d+', dom_val)
        for d in days:
            if int(d) > 29:
                issues.append({
                    'severity': 'ERROR',
                    'message': f'day-of-month={d} in month=2 (February) — this date never occurs',
                })
            elif int(d) == 29:
                issues.append({
                    'severity': 'WARNING',
                    'message': 'day-of-month=29 in February — only runs on leap years',
                })

    # Detect conflicting DOW + DOM (both specified — runs on OR condition, often surprising)
    if dom_val not in ('*', '?') and field_values.get('day_of_week', '*') not in ('*', '?'):
        issues.append({
            'severity': 'INFO',
            'message': (
                'Both day-of-month and day-of-week are specified — '
                'standard cron uses OR (runs if EITHER matches). '
                'Use ? for one field if you want AND behavior (Quartz only).'
            ),
        })

    return {
        'expression': expression,
        'flavor': flavor,
        'explanation': explanation,
        'fields': field_values,
        'issues': issues,
    }

Step 2: Calculate Next Run Times

python
from datetime import datetime, timezone

def next_runs(expression: str, count: int = 5, after: Optional[datetime] = None) -> list[datetime]:
    """
    Calculate the next N run times for a cron expression.
    Uses croniter if available, otherwise falls back to manual calculation.
    """
    after = after or datetime.now(tz=timezone.utc)

    try:
        from croniter import croniter
        cron = croniter(expression, after)
        return [cron.get_next(datetime) for _ in range(count)]
    except ImportError:
        pass

    # Fallback: install croniter
    import subprocess
    subprocess.run(['pip', 'install', 'croniter', '-q'], check=True)
    from croniter import croniter
    cron = croniter(expression, after)
    return [cron.get_next(datetime) for _ in range(count)]


def format_run_times(runs: list[datetime], timezone_name: str = 'UTC') -> list[str]:
    """Format run times for display."""
    now = datetime.now(tz=timezone.utc)
    result = []
    for dt in runs:
        diff = dt - now
        if diff.total_seconds() < 60:
            rel = 'in less than a minute'
        elif diff.total_seconds() < 3600:
            mins = int(diff.total_seconds() / 60)
            rel = f'in {mins} minute{"s" if mins > 1 else ""}'
        elif diff.total_seconds() < 86400:
            hours = int(diff.total_seconds() / 3600)
            rel = f'in {hours} hour{"s" if hours > 1 else ""}'
        else:
            days = int(diff.total_seconds() / 86400)
            rel = f'in {days} day{"s" if days > 1 else ""}'

        result.append(f'{dt.strftime("%Y-%m-%d %H:%M:%S")} UTC ({rel})')
    return result

Step 3: English → Cron Converter

python
import re

def english_to_cron(description: str) -> dict:
    """
    Convert a plain-English schedule description to cron expression.
    Returns {'cron': str, 'explanation': str, 'confidence': str}
    """
    desc = description.lower().strip()

    # Special patterns
    if re.search(r'every\s+minute', desc):
        return {'cron': '* * * * *', 'explanation': 'Every minute', 'confidence': 'HIGH'}

    if re.search(r'every\s+hour', desc):
        return {'cron': '0 * * * *', 'explanation': 'Every hour', 'confidence': 'HIGH'}

    if re.search(r'midnight|every\s+(?:day\s+at\s+)?12\s*am', desc):
        return {'cron': '0 0 * * *', 'explanation': 'Every day at midnight', 'confidence': 'HIGH'}

    if re.search(r'noon|every\s+(?:day\s+at\s+)?12\s*pm', desc):
        return {'cron': '0 12 * * *', 'explanation': 'Every day at noon', 'confidence': 'HIGH'}

    # Extract time: "at 9am", "at 3:30pm", "at 14:00"
    time_match = re.search(
        r'at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?',
        desc, re.I
    )
    hour = minute = None
    if time_match:
        hour = int(time_match.group(1))
        minute = int(time_match.group(2)) if time_match.group(2) else 0
        ampm = (time_match.group(3) or '').lower()
        if ampm == 'pm' and hour != 12:
            hour += 12
        elif ampm == 'am' and hour == 12:
            hour = 0

    # Day of week
    dow_map = {
        'sunday': 0, 'monday': 1, 'tuesday': 2, 'wednesday': 3,
        'thursday': 4, 'friday': 5, 'saturday': 6,
        'weekday': '1-5', 'weekdays': '1-5',
        'weekend': '0,6', 'weekends': '0,6',
        'every day': '*', 'daily': '*',
    }

    dow = '*'
    for key, val in dow_map.items():
        if key in desc:
            dow = str(val)
            break

    # Interval: "every N minutes/hours"
    interval_match = re.search(r'every\s+(\d+)\s+(minute|hour|min|hr)s?', desc, re.I)
    if interval_match:
        n = int(interval_match.group(1))
        unit = interval_match.group(2).lower()

        # Business hours modifier
        if re.search(r'business\s+hours?|work\s+hours?|9\s*(?:am|to)\s*5', desc):
            if unit in ('minute', 'min'):
                cron = f'*/{n} 9-17 * * 1-5'
                return {'cron': cron,
                        'explanation': f'Every {n} minutes, between 9am and 5pm, Monday–Friday',
                        'confidence': 'HIGH'}
            if unit in ('hour', 'hr'):
                cron = f'0 */{ n} * * 1-5' if n > 1 else f'0 9-17 * * 1-5'
                return {'cron': cron,
                        'explanation': f'Every {n} hour{"s" if n>1 else ""}, weekdays only',
                        'confidence': 'MEDIUM'}

        if unit in ('minute', 'min'):
            cron_min = f'*/{n}' if n > 1 else '*'
            cron = f'{cron_min} * * * *'
            return {'cron': cron,
                    'explanation': f'Every {n} minute{"s" if n>1 else ""}',
                    'confidence': 'HIGH'}

        if unit in ('hour', 'hr'):
            cron = f'0 */{n} * * *'
            return {'cron': cron,
                    'explanation': f'Every {n} hour{"s" if n>1 else ""}',
                    'confidence': 'HIGH'}

    # "first day of every month"
    if re.search(r'first\s+day\s+of\s+(?:every|each)\s+month', desc):
        h = hour if hour is not None else 0
        m = minute if minute is not None else 0
        cron = f'{m} {h} 1 * *'
        time_str = f'at {h:02d}:{m:02d}' if hour is not None else 'at midnight'
        return {'cron': cron,
                'explanation': f'On the 1st of every month, {time_str}',
                'confidence': 'HIGH'}

    # "every weekday at Xam"
    if hour is not None and dow != '*':
        m = minute if minute is not None else 0
        cron = f'{m} {hour} * * {dow}'
        dow_names = {'1-5': 'Monday–Friday', '0,6': 'Saturday and Sunday',
                     '0': 'Sunday', '1': 'Monday', '*': 'every day'}
        dow_str = dow_names.get(str(dow), f'day {dow}')
        return {'cron': cron,
                'explanation': f'At {hour:02d}:{m:02d}, {dow_str}',
                'confidence': 'HIGH'}

    # Simple "every day at X"
    if hour is not None:
        m = minute if minute is not None else 0
        cron = f'{m} {hour} * * *'
        am_pm = 'AM' if hour < 12 else 'PM'
        h12 = hour % 12 or 12
        return {'cron': cron,
                'explanation': f'Every day at {h12}:{m:02d} {am_pm}',
                'confidence': 'HIGH'}

    return {
        'cron': None,
        'explanation': 'Could not parse — try: "every weekday at 9am", "every 5 minutes", "first day of month at midnight"',
        'confidence': 'NONE',
    }

Step 4: Scan Project Files for Cron Schedules

python
import glob
import re
import yaml  # pyyaml
from pathlib import Path


def scan_for_cron_schedules(root: str = '.') -> list[dict]:
    """Find all cron expressions in GitHub Actions, Kubernetes, and crontab files."""
    schedules = []

    # GitHub Actions schedule triggers
    for fpath in glob.glob(f'{root}/.github/workflows/*.yml', recursive=False):
        fpath += '' if fpath.endswith('.yml') else ''
        for path in glob.glob(f'{root}/.github/workflows/*.yml') + glob.glob(f'{root}/.github/workflows/*.yaml'):
            try:
                content = Path(path).read_text(errors='replace')
                doc = yaml.safe_load(content)
                if not isinstance(doc, dict):
                    continue
                on = doc.get('on', doc.get(True, {}))
                if isinstance(on, dict):
                    schedule = on.get('schedule', [])
                    if isinstance(schedule, list):
                        for item in schedule:
                            if isinstance(item, dict) and 'cron' in item:
                                schedules.append({
                                    'file': path,
                                    'source': 'github-actions',
                                    'expression': item['cron'],
                                    'context': doc.get('name', 'workflow'),
                                })
            except Exception:
                continue

    # Kubernetes CronJob specs
    for path in glob.glob(f'{root}/**/*.yaml', recursive=True) + glob.glob(f'{root}/**/*.yml', recursive=True):
        if any(skip in path for skip in ['.git', 'node_modules', 'vendor']):
            continue
        try:
            content = Path(path).read_text(errors='replace')
            if 'CronJob' not in content:
                continue
            doc = yaml.safe_load(content)
            if isinstance(doc, dict) and doc.get('kind') == 'CronJob':
                schedule = (doc.get('spec', {}) or {}).get('schedule')
                if schedule:
                    schedules.append({
                        'file': path,
                        'source': 'kubernetes-cronjob',
                        'expression': schedule,
                        'context': doc.get('metadata', {}).get('name', 'cronjob'),
                    })
        except Exception:
            continue

    # crontab files
    for crontab_path in ['crontab', 'crontab.txt', 'etc/cron.d', 'docker/cron']:
        full = f'{root}/{crontab_path}'
        if not Path(full).exists():
            continue
        try:
            for i, line in enumerate(Path(full).read_text().splitlines(), 1):
                line = line.strip()
                if line.startswith('#') or not line:
                    continue
                # crontab line format: min hour dom month dow command
                parts = line.split()
                if len(parts) >= 6:
                    expr = ' '.join(parts[:5])
                    command = ' '.join(parts[5:])
                    schedules.append({
                        'file': crontab_path,
                        'source': 'crontab',
                        'expression': expr,
                        'context': command[:60],
                        'line': i,
                    })
        except Exception:
            continue

    return schedules

Step 5: Output Report

markdown
## Cron Expression Analysis

---

### Expression: `*/5 9-17 * * 1-5`

**Translation:** Every 5 minutes, between 9:00 AM and 5:00 PM, on Monday through Friday

**Flavor:** Standard Unix (5-field)

**Field Breakdown:**
| Field | Value | Meaning |
|-------|-------|---------|
| Minute | `*/5` | Every 5 minutes |
| Hour | `9-17` | Between 9:00 AM and 5:00 PM |
| Day-of-month | `*` | Every day |
| Month | `*` | Every month |
| Day-of-week | `1-5` | Monday through Friday |

**Next 5 scheduled runs:**

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)


**Issues:** None ✅

---

### Expression: `0 0 30 2 *`

**Translation:** At midnight, on the 30th of February

**Issues:**
- 🔴 ERROR: `day-of-month=30 in month=2 (February)` — **this date never occurs**. Job will never run.

**Fix:** Did you mean the last day of every month?

Last day of every month (Quartz): 0 0 L * *

28th of February specifically: 0 0 28 2 *

Every month on the 30th: 0 0 30 * * (skips Feb, Apr, Jun, Sep, Nov)


---

### English → Cron Examples

| Input | Output | Explanation |
|-------|--------|-------------|
| `"every weekday at 9am"` | `0 9 * * 1-5` | At 09:00, Monday–Friday |
| `"every 15 minutes during business hours"` | `*/15 9-17 * * 1-5` | Every 15 min, 9am–5pm, Mon–Fri |
| `"first day of every month at midnight"` | `0 0 1 * *` | At midnight on the 1st |
| `"every Sunday at 2:30am"` | `30 2 * * 0` | At 02:30, Sunday |
| `"every 6 hours"` | `0 */6 * * *` | At minute 0, every 6 hours |

---

### Project Scan: .github/workflows/

Found 3 cron schedules:

| File | Schedule | Explanation | Issues |
|------|----------|-------------|--------|
| `.github/workflows/deploy.yml` | `0 2 * * 1` | Every Monday at 2:00 AM | ✅ OK |
| `.github/workflows/cleanup.yml` | `*/5 * * * *` | Every 5 minutes | ⚠️ Very frequent — confirm intentional |
| `.github/workflows/report.yml` | `0 0 30 2 *` | Feb 30th at midnight | 🔴 NEVER RUNS (Feb 30 doesn't exist) |

---

### Flavor Reference

| Flavor | Fields | Example | Notes |
|--------|--------|---------|-------|
| Standard Unix | `min hour dom month dow` | `0 2 * * 1` | `cron`, `crontab` |
| 6-field (seconds) | `sec min hour dom month dow` | `0 0 2 * * 1` | Spring, Quartz |
| GitHub Actions | `min hour dom month dow` | `'0 2 * * 1'` | Under `on.schedule.cron` |
| AWS EventBridge | `min hour dom month dow year` | `0 2 ? * MON *` | Uses `?` for DOM/DOW |
| Kubernetes | `min hour dom month dow` | `0 2 * * 1` | `.spec.schedule` |

Quick Mode Output

Cron: */5 9-17 * * 1-5

Every 5 minutes, between 9:00 AM and 5:00 PM, Monday–Friday

Next run: 2026-03-19 09:00:00 UTC (in 3 minutes)
Issues: none

To convert English → cron:
  "every weekday at 9am" → 0 9 * * 1-5
  "first of month midnight" → 0 0 1 * *

© LeoYeAI, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in skills/phy-cron-explainer of LeoYeAI/openclaw-master-skills.

  • SKILL.md
  • _meta.json

Open the folder on GitHubat commit e5199b5

Compare with similar skills

Phy Cron Explainer next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Phy Cron Explainer compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Phy Cron Explainer this skillLeoYeAI/openclaw-master-skills2.2k—~7kAutomated safety check: PassApache-2.0
Deployment Automationaiskillstore/marketplace4301 repos~3kAutomated safety check: NotesNone
Ak Cloud Deployyaalalabs/agent-kernel191—~14kAutomated safety check: PassApache-2.0
Project Referencesaiskillstore/marketplace430—~2.3kAutomated safety check: NotesNone
Tsh Managing SecretsTheSoftwareHouse/copilot-collections284—~877Automated safety check: NotesMIT
Senior DevOps Toolkitmaslennikov-ig/claude-code-orchestrator-kit2596 repos~1.1kAutomated safety check: NotesCustom licence

Similar skills

  • Deployment Automation

    aiskillstore/marketplace

    Automate application deployment to cloud platforms and servers.

    430 GitHub starsUsed in 1 repo~3k tokens
    DevOps & CloudAuto-check: notes
  • Ak Cloud Deploy

    yaalalabs/agent-kernel

    Deploy an Agent Kernel project to AWS, Azure, or GCP using Terraform modules, or to any Kubernetes cluster (on-prem, baremetal, EKS) using the official Helm chart.

    191 GitHub stars~14k tokensUpdated today
    Backend & APIsAuto-check passed
  • Project References

    aiskillstore/marketplace

    Look up conventions, patterns, and concrete implementations from your own GitHub repositories checked out locally under ~/projects/referenzen/.

    430 GitHub stars~2.3k tokensUpdated today
    DevOps & CloudAuto-check: notes
  • Tsh Managing Secrets

    TheSoftwareHouse/copilot-collections

    Secrets management patterns for cloud and Kubernetes environments.

    284 GitHub stars~877 tokensUpdated 2 days ago
    DevOps & CloudAuto-check: notes
  • Senior DevOps Toolkit

    maslennikov-ig/claude-code-orchestrator-kit

    Comprehensive DevOps skill for CI/CD, infrastructure automation, containerization, and cloud platforms (AWS, GCP, Azure). Includes pipeline setup…

    259 GitHub starsUsed in 6 repos~1.1k tokens
    DevOps & CloudAuto-check: notes
  • Deep static code review of an mql provider for logic errors, nil-handling bugs, pagination truncation, caching/id collisions, and other defects that silently give users wrong data.

    411 GitHub stars~2.9k tokensUpdated today
    DevOps & CloudAuto-check passed

More from LeoYeAI/openclaw-master-skills

All 1,215 skills in this repo
  • DevOps Pipeline Management

    LeoYeAI/openclaw-master-skills

    Manages pipelines on a DevOps quality and efficiency platform through its OpenAPI: list workspaces and templates, create, update, run and cancel pipelines, and read run records.

    2.2k GitHub stars~4.2k tokensUpdated 2 mo ago
    Auto-check: notes
  • Feishu Document Collaboration

    LeoYeAI/openclaw-master-skills

    Patches OpenClaw's Feishu extension so an edited document triggers an isolated agent session that reads the doc and replies inline, turning it into a live chat space.

    2.2k GitHub stars~2k tokensUpdated 2 mo ago
    Auto-check passed
  • Files Memory System

    LeoYeAI/openclaw-master-skills

    Multi-context memory management system for OpenClaw agents with group-isolated storage, global shared memory, workspace organization, and group-specific skills isolation.

    2.2k GitHub stars~3.8k tokensUpdated 2 mo ago
    Auto-check passed
  • GEO-Claw AI Visibility Agent

    LeoYeAI/openclaw-master-skills

    Runs a brand's AI-search visibility work end to end: diagnosing how AI platforms represent it, repositioning it, producing AI-optimized content and monitoring ongoing mentions.

    2.2k GitHub stars~4.7k tokensUpdated 2 mo ago
    Auto-check passed
  • Google Workspace CLI

    LeoYeAI/openclaw-master-skills

    Installs and authenticates the gws CLI, then automates Gmail, Drive, Sheets, Calendar, Docs, Chat and Tasks with ready-made recipes, persona bundles and security audits.

    2.2k GitHub stars~2.6k tokensUpdated 2 mo ago
    Auto-check: notes
  • HealthFit Health Advisors

    LeoYeAI/openclaw-master-skills

    Runs four advisor roles, a fitness coach, nutritionist, data analyst and TCM practitioner, to build a health profile and track workouts, diet and wellness over time.

    2.2k GitHub stars~4.4k tokensUpdated 2 mo ago
    Auto-check passed

Questions about Phy Cron Explainer

What does Phy Cron Explainer do?

Cron expression explainer, validator, and converter. An agent skill from LeoYeAI/openclaw-master-skills. Phy Cron Explainer is an agent skill from LeoYeAI/openclaw-master-skills. Cron expression explainer, validator, and converter.

When should I use Phy Cron Explainer?

Phy Cron Explainer fits situations like: cron expression; what does this cron mean; cron to English.

How do I install Phy Cron Explainer in Claude Code?

Run `npx skills add LeoYeAI/openclaw-master-skills --skill phy-cron-explainer -a claude-code`. Or copy the skill folder (skills/phy-cron-explainer in LeoYeAI/openclaw-master-skills) into .claude/skills/phy-cron-explainer in your project. Claude Code loads it when a task matches its description.

How do I install Phy Cron Explainer in Codex?

Run `npx skills add LeoYeAI/openclaw-master-skills --skill phy-cron-explainer -a codex`. Or copy the skill folder (skills/phy-cron-explainer in LeoYeAI/openclaw-master-skills) into .agents/skills/phy-cron-explainer in your project. Codex loads it when a task matches its description.

Can I use Phy Cron Explainer in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add LeoYeAI/openclaw-master-skills --skill phy-cron-explainer -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/phy-cron-explainer, .gemini/skills/phy-cron-explainer, .github/skills/phy-cron-explainer and .opencode/skills/phy-cron-explainer in your project.

What does Phy Cron Explainer need to run?

Going by SKILL.md and its folder, Phy Cron Explainer needs the command-line tools its instructions call (aws). Our summary lists: Python 3.

Does Phy Cron Explainer access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Phy Cron Explainer safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Phy Cron Explainer use?

Phy Cron Explainer is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Phy Cron Explainer use?

About 7k tokens (SKILL.md is roughly 28k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Phy Cron Explainer?

Skills that share tags, products or a category with Phy Cron Explainer: Deployment Automation (aiskillstore/marketplace, 430 stars), Ak Cloud Deploy (yaalalabs/agent-kernel, 191 stars), Project References (aiskillstore/marketplace, 430 stars) and Tsh Managing Secrets (TheSoftwareHouse/copilot-collections, 284 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Phy Cron Explainer?

LeoYeAI (a GitHub user) maintains it in LeoYeAI/openclaw-master-skills, which has 2,158 GitHub stars. The repository holds 1,215 skills in this directory. The repository was last updated on July 20, 2026.

Source: LeoYeAI/openclaw-master-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.