NousResearch/hermes-agent · warning · ValueError

Invalid cron expression

Error message

Invalid cron expression '{schedule}': {e}

What it means

ValueError from parse_schedule's cron branch: croniter IS available, the string shaped like a cron expression (5-6 fields of digits/*/-/,/), but croniter's own validation rejected it. The underlying croniter exception text is appended, typically naming the invalid field (e.g. minute values out of range, bad step).

Solutions

  1. Fix the field croniter names in the message — minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-7
  2. Validate locally first: from croniter import croniter; croniter(expr) in a try/except before submitting
  3. For simple daily jobs consider a duration or blueprint schedule instead of raw cron

Example fix

# before
create_job(schedule="61 9 * * *")
# ValueError: Invalid cron expression '61 9 * * *': [minute] ... 

# after
create_job(schedule="1 9 * * *")
Defensive patterns

Strategy: validation

Validate before calling

from croniter import croniter

def is_valid_cron(expr: str) -> bool:
    try:
        croniter(expr)
        return True
    except Exception:
        return False

Try / catch

try:
    parsed = parse_schedule(expr)
except ValueError as e:
    if str(e).startswith("Invalid cron expression"):
        expr = fix_cron_fields(expr)  # e.g. clamp minute 61 -> 1, prompt user
        parsed = parse_schedule(expr)
    else:
        raise

Prevention

When it happens

Trigger: create_job(schedule='61 9 * * *') (minute > 59), schedule='0 25 * * *' (hour > 23), schedule='*/0 * * * *' (zero step), or malformed field lists like '1,,2 * * * *'.

Common situations: Hand-written cron expressions with out-of-range fields; 6-field expressions with a bad year; copy-paste crontabs with usernames or @-tokens (those don't match the field regex and route elsewhere or fail differently); assuming 24:00 or 32nd day are accepted.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/97ddd07a27a580b9. Report an issue: GitHub.

Appendix: source

Thrown at cron/jobs.py:656

        return {
            "kind": "interval",
            "minutes": minutes,
            "display": f"every {minutes}m"
        }
    
    # Check for cron expression (5 or 6 space-separated fields)
    # Cron fields: minute hour day month weekday [year]
    parts = schedule.split()
    if len(parts) >= 5 and all(
        re.match(r'^[\d\*\-,/]+$', p) for p in parts[:5]
    ):
        if not _ensure_croniter():
            raise ValueError("Cron expressions require 'croniter' package. Install with: pip install croniter")
        # Validate cron expression
        try:
            croniter(schedule)
        except Exception as e:
            raise ValueError(f"Invalid cron expression '{schedule}': {e}")
        return {
            "kind": "cron",
            "expr": schedule,
            "display": schedule
        }
    
    # ISO timestamp (contains T or looks like date)
    if 'T' in schedule or re.match(r'^\d{4}-\d{2}-\d{2}', schedule):
        try:
            # Parse and validate
            dt = datetime.fromisoformat(schedule.replace('Z', '+00:00'))
            # Make naive timestamps timezone-aware at parse time so the stored
            # value doesn't depend on the system timezone matching at check time.
            #
            # Anchor to the CONFIGURED Hermes timezone, not the server's local
            # timezone. The due-check (`get_due_jobs`) compares `next_run_at`
            # against `hermes_time.now()`, which uses the configured zone. If a
            # naive "20:07" were interpreted as server-local (e.g. UTC) while

View on GitHub (pinned to c896c09c42)