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'.
Solutions
- Use exactly five space-separated fields: minute hour day-of-month month day-of-week (e.g. '0 9 * * *').
- Validate locally with the same library: pip install croniter; croniter(expr) must not raise before submitting.
- If you need seconds or years, check whether the deployment's croniter/scheduler supports extended syntax; otherwise simplify to 5 fields.
- 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
- Use a cron builder UI (minute/hour/day pickers) instead of a free-text field.
- Validate with the same engine (croniter) in CI for all seeded cron expressions.
- Reject Quartz/systemd-timer syntax at the input layer with a clear hint about 5-field cron.
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
- Cron expression is required for cron schedule
- Action name is required for action tasks
- Event name is required for event-triggered tasks
- Invalid scheduled_date format
- Prompt is required for LLM/research tasks
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/df2afffcb307e171.
Report an issue: GitHub.
Appendix: 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 = NoneView on GitHub (pinned to f9235ebbf1)