usestrix/strix · warning · TypeError
Updates must be a list of update objects
Error message
Updates must be a list of update objects
What it means
After optional JSON decoding, _normalize_bulk_updates requires the updates payload to be a list (a single dict is auto-wrapped into a one-element list). Any other JSON type — string, number, null after decoding, boolean — raises TypeError('Updates must be a list of update objects'). Note: a JSON string input that decodes to a non-list also lands here.
Source
Thrown at strix/tools/todo/tools.py:169
def _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:
if raw_updates is None:
return []
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 normalizedView on GitHub (pinned to 8551339130)
Solutions
- Shape the payload as a list of objects: [{"todo_id": "t1", "status": "done"}].
- A single object is fine — it will be wrapped automatically — but bare scalars/strings are not.
- Check for double-encoded JSON (a string that itself contains a JSON string) when the decoded value is still a str.
Example fix
# before
bulk_update_todos(updates='"mark done"')
# after
bulk_update_todos(updates=[{"todo_id": "t1", "status": "done"}]) Defensive patterns
Strategy: type-guard
Validate before calling
import json
def coerce_updates(raw):
data = json.loads(raw) if isinstance(raw, str) and raw.strip() else raw
if isinstance(data, dict):
data = [data]
if not isinstance(data, list):
raise TypeError("updates must decode to a list")
return data Type guard
def is_update_list(data: object) -> bool:
return isinstance(data, list) or isinstance(data, dict) # dict gets auto-wrapped Try / catch
try:
bulk_update_todos(updates=updates)
except TypeError as exc:
if "list of update objects" in str(exc):
updates = [updates] if isinstance(updates, dict) else updates
raise # scalars/strings cannot be repaired mechanically — surface it
raise Prevention
- Always send an array (or a single object) — never a bare scalar or string.
- Watch for double-encoded JSON where the decoded value is still a string.
- Validate the decoded shape with isinstance checks at your boundary.
When it happens
Trigger: Passing updates='"done"', updates='42', or a decoded non-list value. Passing a single dict {"todo_id": ...} is accepted (wrapped); a list of objects is accepted; everything else raises TypeError.
Common situations: LLM sends a bare string status instead of an object; caller JSON-encodes twice so the decoded value is a string; null/None slipped through after earlier processing.
Related errors
- Each update must be an object with todo_id
- Updates must be valid JSON
- approved must be a boolean
- run.json at {path} is not an object
- Invalid priority. Must be one of: {', '.join(VALID_PRIORITIE
AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15).
Data as JSON: /api/errors/5a733918db1e8823.
Report an issue: GitHub.