NousResearch/hermes-agent · warning · ValueError

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

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).

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)

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.