{"record":{"id":"3ce5bb054f6c06b0","repo":"odysseus-dev/odysseus","slug":"cron-expression-is-required-for-cron-schedule","errorCode":null,"errorMessage":"Cron expression is required for cron schedule","messagePattern":"Cron expression is required for cron schedule","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/task_routes.py","lineNumber":467,"sourceCode":"        return target.id\n\n    @router.post(\"\")\n    async def create_task(request: Request, req: TaskCreate):\n        user = _owner(request)\n\n        # 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:","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L449-L485","documentation":"Create-task validation: for trigger_type 'schedule' with schedule == 'cron', a cron_expression must be supplied. Without it the scheduler cannot compute next_run, so the request is rejected with HTTP 400 before the croniter validity check.","triggerScenarios":"POST /api/tasks with {\"trigger_type\": \"schedule\", \"schedule\": \"cron\"} and cron_expression null/empty.","commonSituations":"Cron input hidden behind a toggle that was flipped after submit state was captured; advanced-cron UI mode not persisted; JSON import that strips the cron field.","solutions":["Send a cron_expression (standard 5-field cron syntax accepted by croniter, e.g. '*/15 * * * *').","Make cron_expression required client-side when schedule === 'cron'.","If you wanted a fixed time instead, use the daily/time schedule mode rather than 'cron'."],"exampleFix":"// before\n{trigger_type: 'schedule', schedule: 'cron'}\n\n// after\n{trigger_type: 'schedule', schedule: 'cron', cron_expression: '0 9 * * 1-5'}","handlingStrategy":"validation","validationCode":"function validateCronTask(p) {\n  if (p.trigger_type === 'schedule' && p.schedule === 'cron' && !(p.cron_expression ?? '').trim())\n    throw new ValidationError('Cron expression is required');\n  return p;\n}","typeGuard":"function isCronTask(p: unknown): p is {schedule: 'cron'; cron_expression: string} {\n  return typeof p === 'object' && p !== null &&\n    (p as any).schedule === 'cron' &&\n    typeof (p as any).cron_expression === 'string' && (p as any).cron_expression.trim().length > 0;\n}","tryCatchPattern":null,"preventionTips":["Show the cron input (required) whenever schedule === 'cron' is selected.","Re-validate the whole trigger block client-side whenever schedule mode changes."],"tags":["tasks","validation","http-400","cron","scheduling"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}