{"record":{"id":"e8451b36e200642a","repo":"odysseus-dev/odysseus","slug":"action-name-is-required-for-action-tasks","errorCode":null,"errorMessage":"Action name is required for action tasks","messagePattern":"Action name is required for action tasks","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/task_routes.py","lineNumber":459,"sourceCode":"        if current_task_id and target_id == current_task_id:\n            raise HTTPException(400, \"Task cannot chain to itself\")\n        q = db.query(ScheduledTask).filter(ScheduledTask.id == target_id)\n        if user:\n            q = q.filter(ScheduledTask.owner == user)\n        target = q.first()\n        if not target:\n            raise HTTPException(404, \"Chained task not found\")\n        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\")","sourceCodeStart":441,"sourceCodeEnd":477,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L441-L477","documentation":"Create-task validation: task_type 'action' requires a non-empty action name naming a built-in or registered action. Rejected with HTTP 400 before the admin-privilege check for admin-only actions runs.","triggerScenarios":"POST /api/tasks with {\"task_type\": \"action\"} and action null/empty/whitespace.","commonSituations":"Action dropdown left on its placeholder option whose value is ''; action key renamed between versions so the client sends the old empty default; JSON import missing the action field.","solutions":["Set action to a valid action name (see BUILTIN_ACTION_INFO keys served by the actions listing endpoint).","Make the client action selector required and disable submit until a real action is chosen.","If the action name comes from a config, verify it matches a key in src.builtin_actions.BUILTIN_ACTION_INFO."],"exampleFix":"// before\nawait api.createTask({task_type: 'action'}); // no action\n\n// after\nif (!actionSelect.value) return showError('Pick an action');\nawait api.createTask({task_type: 'action', action: actionSelect.value});","handlingStrategy":"validation","validationCode":"function validateActionTask(p) {\n  if (p.task_type === 'action' && !(p.action ?? '').trim())\n    throw new ValidationError('Action name is required');\n  return p;\n}","typeGuard":"function hasActionName(p: unknown): p is {task_type: 'action'; action: string} {\n  return typeof p === 'object' && p !== null &&\n    (p as any).task_type === 'action' &&\n    typeof (p as any).action === 'string' && (p as any).action.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Require a non-placeholder selection in the action dropdown before enabling submit.","Validate the action name against BUILTIN_ACTION_INFO keys fetched from the server."],"tags":["tasks","validation","http-400","actions"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}