odysseus-dev/odysseus · error · HTTPException
Chained task not found
Error message
Chained task not found
What it means
Raised by _validate_then_task_id when then_task_id names a task that does not exist in the ScheduledTask table (optionally additionally filtered by owner). The chained target must already exist before it can be referenced. Returned as HTTP 404.
Solutions
- Verify the target id exists: GET /api/tasks/{then_task_id} must return 200 before submitting the chain.
- If it returns 404/403, re-create the target task or pick a task owned by the same user.
- Create the target task first, capture its id from the response, then set then_task_id on the parent.
Example fix
# before
task = client.create_task({...})
client.create_task({..., "then_task_id": guessed_id})
# after
child = client.create_task({...})
assert child["id"], "child task must exist"
parent = client.create_task({..., "then_task_id": child["id"]}) Defensive patterns
Strategy: validation
Validate before calling
async function resolveChainTarget(thenTaskId, owner) {
if (!thenTaskId) return null;
const res = await fetch(`/api/tasks/${encodeURIComponent(thenTaskId.trim())}`);
if (res.status === 404) throw new ValidationError('Chained task not found');
if (res.status === 403) throw new ValidationError('Chained task belongs to another user');
return thenTaskId.trim();
} Try / catch
try { await api.createTask(payload); }
catch (e) {
if (e.status === 404 && /Chained task/.test(e.message)) { refreshTaskList(); return; }
throw e;
} Prevention
- Populate chain targets from the live owner-scoped task list, not stored ids.
- Create the child task first and use the id from its response.
- Handle task-deleted events by pruning stale then_task_id references in local state.
When it happens
Trigger: POST/PUT a task with then_task_id set to an id that was deleted, belongs to another owner (when the owner filter applies), or is a typo/truncated paste (leading/trailing spaces are stripped, so mismatch must be in the id itself).
Common situations: Chaining to a task deleted earlier; two browser tabs where the target task was removed; copying task ids between instances/environments where ids differ; race where the target task is created asynchronously and referenced before commit.
Related errors
- Task cannot chain to itself
- Action name is required for action tasks
- Cannot chain to another user's task
- Cron expression is required for cron schedule
- Event name is required for event-triggered tasks
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/c8deeb0938371c6a.
Report an issue: GitHub.
Appendix: source
Thrown at routes/task_routes.py:448
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
# uses subprocess.run(shell=True) and ssh_command / run_script run
# arbitrary commands.
_require_admin_for_task_action(user, req.task_type, req.action)
if req.trigger_type == "schedule" and not req.schedule:
raise HTTPException(400, "Schedule is required for schedule-triggered tasks")
if req.trigger_type == "schedule" and req.schedule == "cron" and not req.cron_expression:View on GitHub (pinned to f9235ebbf1)