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_local

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Clear or change then_task_id so it points to a different task id.
  2. Fix the edit form to exclude the current task from the chain-target picker.
  3. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/a654bfa872fe1743. Report an issue: GitHub.