{"record":{"id":"df2afffcb307e171","repo":"odysseus-dev/odysseus","slug":"invalid-cron-expression","errorCode":null,"errorMessage":"Invalid cron expression","messagePattern":"Invalid cron expression","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/task_routes.py","lineNumber":473,"sourceCode":"        # Validate\n        if req.task_type in (\"llm\", \"research\") and not req.prompt:\n            raise HTTPException(400, \"Prompt is required for LLM/research tasks\")\n        if req.task_type == \"action\" and not req.action:\n            raise HTTPException(400, \"Action name is required for action tasks\")\n        # Block shell-executing action types for non-admins. action_run_local\n        # uses subprocess.run(shell=True) and ssh_command / run_script run\n        # arbitrary commands.\n        _require_admin_for_task_action(user, req.task_type, req.action)\n        if req.trigger_type == \"schedule\" and not req.schedule:\n            raise HTTPException(400, \"Schedule is required for schedule-triggered tasks\")\n        if req.trigger_type == \"schedule\" and req.schedule == \"cron\" and not req.cron_expression:\n            raise HTTPException(400, \"Cron expression is required for cron schedule\")\n        if req.trigger_type == \"schedule\" and req.schedule == \"cron\" and req.cron_expression:\n            try:\n                from croniter import croniter\n                croniter(req.cron_expression)\n            except Exception:\n                raise HTTPException(400, \"Invalid cron expression\")\n        if req.trigger_type == \"event\" and not req.trigger_event:\n            raise HTTPException(400, \"Event name is required for event-triggered tasks\")\n        if req.trigger_type == \"event\" and not req.trigger_count:\n            raise HTTPException(400, \"Trigger count is required for event-triggered tasks\")\n\n        # Auto-generate name\n        name = req.name\n        if not name:\n            if req.task_type == \"action\":\n                from src.builtin_actions import BUILTIN_ACTION_INFO\n                name = BUILTIN_ACTION_INFO.get(req.action, req.action or \"Action Task\")\n            elif req.prompt:\n                name = await _generate_task_name(req.prompt, owner=user)\n            else:\n                name = \"Untitled Task\"\n\n        # Compute next_run for schedule-triggered tasks\n        next_run = None","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L455-L491","documentation":"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'.","triggerScenarios":"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 * * * *').","commonSituations":"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.","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."],"exampleFix":"# before\nreq = {\"trigger_type\": \"schedule\", \"schedule\": \"cron\", \"cron_expression\": \"every 5 min\"}\n\n# after\nfrom croniter import croniter\nexpr = \"*/5 * * * *\"\ncroniter(expr)  # raises here, before the API call\nreq = {\"trigger_type\": \"schedule\", \"schedule\": \"cron\", \"cron_expression\": expr}","handlingStrategy":"validation","validationCode":"const CRON_RE = /^(\\S+\\s+){4}\\S+$/;\nfunction validCron(expr) {\n  if (!CRON_RE.test(expr.trim())) return false;\n  // strongest check: run croniter server-side / via a cron parser lib client-side\n  try { cronParser.parseExpression(expr); return true; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try { await api.createTask(p); }\ncatch (e) {\n  if (e.status === 400 && /cron/i.test(e.message)) { highlightCronField(e.message); return; }\n  throw e;\n}","preventionTips":["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."],"tags":["tasks","cron","validation","http-400","croniter"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}