bmad-code-org/BMAD-METHOD · error · RenderError
{label} must be a list of tables
Error message
{label} must be a list of tables What it means
Thrown by _require_review_layers when a customization value whose customize.toml default is a list-of-tables (review layers) is not itself a list. The renderer only accepts an array of tables for review-layer fields because each layer needs id/name/instruction, so a scalar, a single inline table, or a bare string is rejected. The {label} identifies the offending customization path (e.g. customization.workflow.review_layers).
Source
Thrown at src/scripts/render_skill.py:74
if not isinstance(value, str):
raise RenderError(f"{label} must be a string, got {type(value).__name__}")
if not allow_empty and not value.strip():
raise RenderError(f"{label} must not be empty")
return value
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:View on GitHub (pinned to b70486b9bd)
Solutions
- Declare review layers as an array of tables using one [[workflow.review_layers]] header per layer, not a single [workflow.review_layers] table.
- Verify the field path in the {workflow.<path>} token matches a key whose merged value is a list.
- Inspect the parsed structure with python -c "import tomllib;print(tomllib.load(open('your.toml','rb')))" and confirm the value is a list of dicts.
Example fix
# before (single table -- wrong) [workflow.review_layers] id = "architect" name = "Architect" instruction = "..." # after (array of tables -- correct) [[workflow.review_layers]] id = "architect" name = "Architect" instruction = "..."
Defensive patterns
Strategy: validation
Validate before calling
import tomllib
from pathlib import Path
def validate_review_layers_is_list(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("."):
if not isinstance(cur, dict) or part not in cur:
raise SystemExit(f"missing {dotted}")
cur = cur[part]
if not isinstance(cur, list):
raise SystemExit(f"{dotted} must be a list of tables, got {type(cur).__name__}") Type guard
def is_review_layers(value: object) -> bool:
return isinstance(value, list) and all(isinstance(x, dict) for x in value) Prevention
- Use [[...]] TOML headers for every array-of-tables customization field.
- Parse your customization TOML with tomllib and assert the list-of-tables shape before rendering.
- Keep a unit test that loads customize.toml and asserts review-layer structure.
When it happens
Trigger: A source contains a {workflow.<x>} token whose customize.toml default is [[...]] array-of-tables. _resolve_customization_value sees default is a non-empty list of dicts and calls _require_review_layers(value, label), but the merged value (from customize.toml / _bmad/custom/<skill>.toml / <skill>.user.toml) is not a list -- e.g. it was authored as a single [workflow.review_layers] table or a string.
Common situations: Authoring TOML and using a single [section] table header instead of [[...]] array-of-tables headers; pasting a JSON object instead of an array; a merge layer overriding the array with a scalar.
Related errors
- {item_label} must be a table
- 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/f5c31ea21a0f9ed1.
Report an issue: GitHub.