usestrix/strix · error · ValueError
Each update must include 'todo_id'
Error message
Each update must include 'todo_id'
What it means
Thrown by the todo-update normalizer in Strix's TodoWrite/TodoUpdate tool when an update object carries neither a 'todo_id' nor an 'id' key (or both are empty/None). The tool accepts 'id' as an alias, stringifies and strips it, then stores it as 'todo_id'. An item without an identifying key cannot be routed to _apply_single_update, so it is rejected before any state changes.
Source
Thrown at strix/tools/todo/tools.py:177
if not stripped:
return []
try:
data = json.loads(stripped)
except json.JSONDecodeError as e:
raise ValueError("Updates must be valid JSON") from e
if isinstance(data, dict):
data = [data]
if not isinstance(data, list):
raise TypeError("Updates must be a list of update objects")
normalized: list[dict[str, Any]] = []
for item in data:
if not isinstance(item, dict):
raise TypeError("Each update must be an object with todo_id")
todo_id = item.get("todo_id") or item.get("id")
if not todo_id:
raise ValueError("Each update must include 'todo_id'")
normalized.append(
{
"todo_id": str(todo_id).strip(),
"title": item.get("title"),
"description": item.get("description"),
"priority": item.get("priority"),
"status": item.get("status"),
},
)
return normalized
def _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:
if raw_todos is None:
return []
data: Any = raw_todos
if isinstance(raw_todos, str):
stripped = raw_todos.strip()View on GitHub (pinned to 8551339130)
Solutions
- Include 'todo_id' (or 'id') in every update object, e.g. {'todo_id': 'abc', 'status': 'completed'}
- Verify ids come from a prior read of the todo list rather than being invented
- Trim the id string before sending; an empty-after-strip id is treated as missing
Example fix
# before
updates = [{"title": "finish report"}]
# after
updates = [{"todo_id": "todo-3", "title": "finish report"}] Defensive patterns
Strategy: validation
Validate before calling
def valid_updates(updates: list) -> bool:
return all(
isinstance(u, dict)
and bool(str(u.get("todo_id") or u.get("id") or "").strip())
for u in updates
)
if not valid_updates(updates):
raise ValueError("each update needs a non-empty todo_id/id") Type guard
def is_update_object(v) -> bool:
return isinstance(v, dict) and bool(str(v.get("todo_id") or v.get("id") or "").strip()) Try / catch
try:
tool.update_todos(updates)
except ValueError as e:
if "todo_id" in str(e):
rebuild updates from the last list_todos() result and retry once Prevention
- Always fetch ids from a prior list call; never synthesize them
- Add a pre-send assertion that every update object has a non-empty todo_id
- In agent prompts, require the id field in the update schema
When it happens
Trigger: Calling the todos update tool with updates=[{'title': 'new title'}] (no id), with {'todo_id': ''} or {'id': None}, or with a dict whose id key is misspelled ('todoid', 'task_id', 'uuid').
Common situations: An LLM agent composing update payloads from memory and dropping the id field; ids fetched from a prior list call but serialized as null; whitespace-only ids after copy-paste.
Related errors
- Todos must be provided as a list, dict, or JSON string
- Each todo entry must be a string or object with a title
- Each todo entry must include a non-empty 'title'
- extra_system_prompt_context cannot override built-in scope k
- Cannot resume scan {scan_id}: agents.json has no root agent
AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15).
Data as JSON: /api/errors/54e7a90e84a8e526.
Report an issue: GitHub.