usestrix/strix · warning · TypeError

Each update must be an object with todo_id

Error message

Each update must be an object with todo_id

What it means

Inside the updates list, every element must be a dict; a non-dict element raises TypeError('Each update must be an object with todo_id'). This runs before the todo_id presence check, so ['t1'] or [42] fails here even though the message also mentions todo_id. Only after passing this check is item.get('todo_id')/'id' looked up.

Source

Thrown at strix/tools/todo/tools.py:174

    data: Any = raw_updates
    if isinstance(raw_updates, str):
        stripped = raw_updates.strip()
        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 []

View on GitHub (pinned to 8551339130)

Solutions

  1. Wrap every element as an object: [{"todo_id": "t1", ...}, {"todo_id": "t2", ...}].
  2. If starting from bare ids, map them: ids.map(id => ({"todo_id": id})).
  3. For LLM tool schemas, require an array of objects and show an example in the description.

Example fix

# before
bulk_update_todos(updates="[\"t1\", \"t2\"]")

# after
bulk_update_todos(updates='[{"todo_id": "t1"}, {"todo_id": "t2"}]')
Defensive patterns

Strategy: type-guard

Validate before calling

def all_elements_are_objects(data: list) -> bool:
    return all(isinstance(item, dict) for item in data)

if not all_elements_are_objects(updates):
    updates = [{"todo_id": item} if isinstance(item, str) else item for item in updates]

Type guard

def is_normalized_updates(items: object) -> bool:
    return (
        isinstance(items, list)
        and all(isinstance(i, dict) and (i.get("todo_id") or i.get("id")) for i in items)
    )

Try / catch

try:
    bulk_update_todos(updates=updates)
except TypeError as exc:
    if "object with todo_id" in str(exc):
        updates = [u if isinstance(u, dict) else {"todo_id": u} for u in updates]
        bulk_update_todos(updates=updates)
    else:
        raise

Prevention

When it happens

Trigger: Passing a mixed list like [{...}, "t2"] or a list of id strings ['t1','t2']. Each element must be an object; ids must be wrapped: [{"todo_id": "t2"}].

Common situations: LLM abbreviating the schema to bare ids; callers converting between list-of-ids and list-of-patches; JSON arrays of strings from external tooling.

Related errors


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