bmad-code-org/BMAD-METHOD · error · RenderError

{item_label} must be a table

Error message

{item_label} must be a table

What it means

Inside _require_review_layers, each element of the review-layers array must be a TOML table. This fires when one element is a scalar, string, or nested array. The {item_label} pinpoints the offending index, e.g. customization.workflow.review_layers[2].

Source

Thrown at src/scripts/render_skill.py:80

def _require_string_list(value: Any, label: str) -> list[str]:
    if not isinstance(value, list):
        raise RenderError(f"{label} must be a list, got {type(value).__name__}")
    result = []
    for index, item in enumerate(value):
        result.append(_require_string(item, f"{label}[{index}]"))
    return result


def _require_review_layers(value: Any, label: str) -> list[dict[str, str]]:
    if not isinstance(value, list):
        raise RenderError(f"{label} must be a list of tables")
    result: list[dict[str, str]] = []
    seen: set[str] = set()
    for index, item in enumerate(value):
        item_label = f"{label}[{index}]"
        if not isinstance(item, dict):
            raise RenderError(f"{item_label} must be a table")
        identifier = _require_string(item.get("id"), f"{item_label}.id")
        if identifier in seen:
            raise RenderError(f"duplicate review layer id `{identifier}`")
        seen.add(identifier)
        layer = {
            "id": identifier,
            "name": _require_string(item.get("name", identifier), f"{item_label}.name"),
            "instruction": _require_string(
                item.get("instruction"), f"{item_label}.instruction", allow_empty=True
            ),
        }
        if "when" in item:
            layer["when"] = _require_string(item["when"], f"{item_label}.when")
        result.append(layer)
    return result


def _load_sources(skill_dir: Path) -> dict[str, str]:

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Open the file at the index reported by {item_label} and make that entry a table (its own [[workflow.review_layers]] block or an inline table).
  2. Remove any scalar/stray entries from the array.
  3. Re-parse with tomllib and assert every element is a dict containing at least an id key.

Example fix

# before
[[workflow.review_layers]]
id = "architect"

workflow.review_layers = ["pm"]   # string, not a table

# after
[[workflow.review_layers]]
id = "architect"

[[workflow.review_layers]]
id = "pm"
Defensive patterns

Strategy: validation

Validate before calling

import tomllib
from pathlib import Path

def validate_all_layers_are_tables(toml_path: Path, dotted: str) -> None:
    data = tomllib.loads(Path(toml_path).read_text(encoding="utf-8"))
    cur: object = data
    for part in dotted.split("."):
        cur = cur[part]
    assert isinstance(cur, list), f"{dotted} is not a list"
    for i, item in enumerate(cur):
        if not isinstance(item, dict):
            raise SystemExit(f"{dotted}[{i}] must be a table, got {type(item).__name__}")

Type guard

def all_tables(value: list) -> bool:
    return all(isinstance(x, dict) for x in value)

Prevention

When it happens

Trigger: The merged review-layers array contains a non-dict element -- e.g. review_layers = ["architect", {id="pm"}] mixing strings and tables, or a malformed [[...]] block. isinstance(item, dict) is False for that index.

Common situations: Hand-editing TOML and forgetting the [[...]] header for one layer; copy-paste leaving a stray string; a non-keyed merge appending a scalar into the array.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/bcab26f507a73ad0. Report an issue: GitHub.