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

  1. Include 'todo_id' (or 'id') in every update object, e.g. {'todo_id': 'abc', 'status': 'completed'}
  2. Verify ids come from a prior read of the todo list rather than being invented
  3. 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

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


AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15). Data as JSON: /api/errors/54e7a90e84a8e526. Report an issue: GitHub.