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
- 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).
- Remove any scalar/stray entries from the array.
- 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
- Give every review layer its own [[workflow.review_layers]] block.
- After editing TOML, re-parse and check each element is a dict.
- Add a pre-render lint that rejects mixed-type arrays in customization fields.
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
- {label} must be a list of tables
- duplicate review layer id `{identifier}`
- {label} has unsupported default type {type(default).__name__
- {label} must resolve to an absolute path: {resolved}
- missing config value `{key}`
AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13).
Data as JSON: /api/errors/bcab26f507a73ad0.
Report an issue: GitHub.