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

  1. Declare review layers as an array of tables using one [[workflow.review_layers]] header per layer, not a single [workflow.review_layers] table.
  2. Verify the field path in the {workflow.<path>} token matches a key whose merged value is a list.
  3. 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

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


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