usestrix/strix · error · TypeError

Todos must be provided as a list, dict, or JSON string

Error message

Todos must be provided as a list, dict, or JSON string

What it means

Thrown when the raw todos argument passed to Strix's todo tool is not a list, dict, or JSON string after parsing. Strings are first JSON-parsed (with a markdown-bullet fallback); dicts are wrapped into a single-element list. Anything else (int, None, tuple, nested non-list) hits this TypeError.

Source

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

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()
        if not stripped:
            return []
        try:
            data = json.loads(stripped)
        except json.JSONDecodeError:
            entries = [line.strip(" -*\t") for line in stripped.splitlines() if line.strip(" -*\t")]
            return [{"title": entry} for entry in entries]

    if isinstance(data, dict):
        data = [data]
    if not isinstance(data, list):
        raise TypeError("Todos must be provided as a list, dict, or JSON string")

    normalized: list[dict[str, Any]] = []
    for item in data:
        if isinstance(item, str):
            title = item.strip()
            if title:
                normalized.append({"title": title})
            continue
        if not isinstance(item, dict):
            raise TypeError("Each todo entry must be a string or object with a title")
        title = item.get("title", "")
        if not isinstance(title, str) or not title.strip():
            raise ValueError("Each todo entry must include a non-empty 'title'")
        normalized.append(
            {
                "title": title.strip(),
                "description": (item.get("description") or "").strip() or None,
                "priority": item.get("priority"),

View on GitHub (pinned to 8551339130)

Solutions

  1. Pass a JSON array of objects: '[{"title": "buy milk"}]' or a plain Python list
  2. A single dict is fine — it is auto-wrapped into a one-item list
  3. For plain text, use markdown bullets ('- task one') which the line-splitter fallback accepts

Example fix

# before
todos = json.dumps(json.dumps([{"title": "x"}]))  # double-encoded

# after
todos = json.dumps([{"title": "x"}])
Defensive patterns

Strategy: validation

Validate before calling

import json

def normalize_todos(raw):
    if isinstance(raw, str):
        raw = json.loads(raw)
    if isinstance(raw, dict):
        raw = [raw]
    if not isinstance(raw, list):
        raise TypeError("expected list/dict/JSON string")
    return raw

Type guard

def is_todos_payload(v) -> bool:
    return isinstance(v, (list, dict)) or (isinstance(v, str) and v.lstrip().startswith(("[", "{", "-")))

Try / catch

try:
    tool.write_todos(raw)
except TypeError as e:
    if "list, dict, or JSON string" in str(e):
        log raw payload type and re-serialize the source data as a JSON array

Prevention

When it happens

Trigger: Passing todos as a JSON-encoded number ('42'), a bare None after JSON-parsing, a tuple of dicts, or a JSON string like '"just a title"' that parses to a scalar string rather than a list.

Common situations: An agent wrapping a single string in extra quotes so json.loads returns a str; passing a generator/iterator that lost list-ness; double-encoding the payload (json.dumps applied twice).

Related errors


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