odysseus-dev/odysseus · warning · HTTPException

Invalid cron expression

Error message

Invalid cron expression

What it means

Create-task validation: the supplied cron_expression was handed to croniter and raised (CroniterBadCronError or similar), meaning it is not parseable cron syntax. The bare except Exception converts it to HTTP 400 'Invalid cron expression'.

Source

Thrown at routes/task_routes.py:473

        # Validate
        if req.task_type in ("llm", "research") and not req.prompt:
            raise HTTPException(400, "Prompt is required for LLM/research tasks")
        if req.task_type == "action" and not req.action:
            raise HTTPException(400, "Action name is required for action tasks")
        # Block shell-executing action types for non-admins. action_run_local
        # uses subprocess.run(shell=True) and ssh_command / run_script run
        # arbitrary commands.
        _require_admin_for_task_action(user, req.task_type, req.action)
        if req.trigger_type == "schedule" and not req.schedule:
            raise HTTPException(400, "Schedule is required for schedule-triggered tasks")
        if req.trigger_type == "schedule" and req.schedule == "cron" and not req.cron_expression:
            raise HTTPException(400, "Cron expression is required for cron schedule")
        if req.trigger_type == "schedule" and req.schedule == "cron" and req.cron_expression:
            try:
                from croniter import croniter
                croniter(req.cron_expression)
            except Exception:
                raise HTTPException(400, "Invalid cron expression")
        if req.trigger_type == "event" and not req.trigger_event:
            raise HTTPException(400, "Event name is required for event-triggered tasks")
        if req.trigger_type == "event" and not req.trigger_count:
            raise HTTPException(400, "Trigger count is required for event-triggered tasks")

        # Auto-generate name
        name = req.name
        if not name:
            if req.task_type == "action":
                from src.builtin_actions import BUILTIN_ACTION_INFO
                name = BUILTIN_ACTION_INFO.get(req.action, req.action or "Action Task")
            elif req.prompt:
                name = await _generate_task_name(req.prompt, owner=user)
            else:
                name = "Untitled Task"

        # Compute next_run for schedule-triggered tasks
        next_run = None

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Use exactly five space-separated fields: minute hour day-of-month month day-of-week (e.g. '0 9 * * *').
  2. Validate locally with the same library: pip install croniter; croniter(expr) must not raise before submitting.
  3. If you need seconds or years, check whether the deployment's croniter/scheduler supports extended syntax; otherwise simplify to 5 fields.
  4. For Quartz-style expressions, translate to 5-field cron first.

Example fix

# before
req = {"trigger_type": "schedule", "schedule": "cron", "cron_expression": "every 5 min"}

# after
from croniter import croniter
expr = "*/5 * * * *"
croniter(expr)  # raises here, before the API call
req = {"trigger_type": "schedule", "schedule": "cron", "cron_expression": expr}
Defensive patterns

Strategy: validation

Validate before calling

const CRON_RE = /^(\S+\s+){4}\S+$/;
function validCron(expr) {
  if (!CRON_RE.test(expr.trim())) return false;
  // strongest check: run croniter server-side / via a cron parser lib client-side
  try { cronParser.parseExpression(expr); return true; } catch { return false; }
}

Try / catch

try { await api.createTask(p); }
catch (e) {
  if (e.status === 400 && /cron/i.test(e.message)) { highlightCronField(e.message); return; }
  throw e;
}

Prevention

When it happens

Trigger: POST /api/tasks with schedule 'cron' and a malformed expression: wrong field count ('0 9 * *' has 4), non-numeric garbage ('every 5 minutes'), 6+ fields, or ranges/steps croniter rejects ('5-1 * * * *').

Common situations: Human-language schedule typed into a cron field; copy-paste from systemd timers or Quartz (6/7-field) syntax that croniter rejects by default; locale decimal separators or smart quotes in the expression; UI builder generating '0 9 * * * ' with stray whitespace variants.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/df2afffcb307e171. Report an issue: GitHub.