odysseus-dev/odysseus · warning · HTTPException
Task cannot chain to itself
Error message
Task cannot chain to itself
What it means
Raised during task validation when a task's then_task_id (the task to chain/trigger after completion) equals the task's own id, producing an infinite self-trigger loop. Detected in _validate_then_task_id via current_task_id comparison and returned as HTTP 400.
Source
Thrown at routes/task_routes.py:442
# Actions that execute shell/SSH commands or cross into admin-only
# Cookbook serving surfaces — restricted to admins.
# Non-admin users cannot create tasks with these action types via the
# API. See review CRIT-C.
_ADMIN_ONLY_ACTIONS = ADMIN_ONLY_TASK_ACTIONS
def _is_admin(user: str | None) -> bool:
return owner_has_admin_task_privileges(user)
def _require_admin_for_task_action(user: str | None, task_type: str | None, action: str | None) -> None:
if is_admin_only_task_action(task_type, action) and not _is_admin(user):
raise HTTPException(403, f"Action '{action}' requires admin privileges")
def _validate_then_task_id(db, then_task_id: Optional[str], user: Optional[str], current_task_id: Optional[str] = None) -> Optional[str]:
target_id = (then_task_id or "").strip()
if not target_id:
return None
if current_task_id and target_id == current_task_id:
raise HTTPException(400, "Task cannot chain to itself")
q = db.query(ScheduledTask).filter(ScheduledTask.id == target_id)
if user:
q = q.filter(ScheduledTask.owner == user)
target = q.first()
if not target:
raise HTTPException(404, "Chained task not found")
return target.id
@router.post("")
async def create_task(request: Request, req: TaskCreate):
user = _owner(request)
# 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_localView on GitHub (pinned to f9235ebbf1)
Solutions
- Clear or change then_task_id so it points to a different task id.
- Fix the edit form to exclude the current task from the chain-target picker.
- If self-chaining (recurring re-trigger) is genuinely wanted, use a schedule/cron trigger instead of chaining.
Example fix
// before form.then_task_id = task.id; // edit form prefilled with itself // after form.then_task_id = tasks.find(t => t.id !== task.id)?.id ?? null;
Defensive patterns
Strategy: validation
Validate before calling
function validateChain(form, currentTaskId) {
const t = (form.then_task_id ?? '').trim();
if (!t) return null;
if (currentTaskId && t === currentTaskId)
throw new ValidationError('Task cannot chain to itself');
return t;
} Try / catch
try { await api.updateTask(id, patch); }
catch (e) {
if (e.status === 400 && /chain to itself/.test(e.message)) { form.then_task_id = null; return; }
throw e;
} Prevention
- Exclude the current task id from the chain-target dropdown in edit forms.
- Trim then_task_id before submit (also avoids the duplicate-check mismatch).
- Use cron/daily schedules for self-repeating behavior instead of self-chaining.
When it happens
Trigger: PUT /api/tasks/{id} with then_task_id set to the same id in the URL/body; or creating a task whose generated id is then edited to chain to itself; on create the current_task_id may be None so this fires mainly on updates.
Common situations: UI pre-populates the chain field with the task being edited; copy-paste of a task JSON that includes its own id; frontend bug that defaults then_task_id to the selected row in an edit form.
Related errors
- Chained task not found
- Prompt is required for LLM/research tasks
- Action name is required for action tasks
- Schedule is required for schedule-triggered tasks
- Cron expression is required for cron schedule
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/a654bfa872fe1743.
Report an issue: GitHub.