{"record":{"id":"f5c31ea21a0f9ed1","repo":"bmad-code-org/BMAD-METHOD","slug":"label-must-be-a-list-of-tables","errorCode":null,"errorMessage":"{label} must be a list of tables","messagePattern":"(.+?) must be a list of tables","errorType":"validation","errorClass":"RenderError","httpStatus":null,"severity":"error","filePath":"src/scripts/render_skill.py","lineNumber":74,"sourceCode":"    if not isinstance(value, str):\n        raise RenderError(f\"{label} must be a string, got {type(value).__name__}\")\n    if not allow_empty and not value.strip():\n        raise RenderError(f\"{label} must not be empty\")\n    return value\n\n\ndef _require_string_list(value: Any, label: str) -> list[str]:\n    if not isinstance(value, list):\n        raise RenderError(f\"{label} must be a list, got {type(value).__name__}\")\n    result = []\n    for index, item in enumerate(value):\n        result.append(_require_string(item, f\"{label}[{index}]\"))\n    return result\n\n\ndef _require_review_layers(value: Any, label: str) -> list[dict[str, str]]:\n    if not isinstance(value, list):\n        raise RenderError(f\"{label} must be a list of tables\")\n    result: list[dict[str, str]] = []\n    seen: set[str] = set()\n    for index, item in enumerate(value):\n        item_label = f\"{label}[{index}]\"\n        if not isinstance(item, dict):\n            raise RenderError(f\"{item_label} must be a table\")\n        identifier = _require_string(item.get(\"id\"), f\"{item_label}.id\")\n        if identifier in seen:\n            raise RenderError(f\"duplicate review layer id `{identifier}`\")\n        seen.add(identifier)\n        layer = {\n            \"id\": identifier,\n            \"name\": _require_string(item.get(\"name\", identifier), f\"{item_label}.name\"),\n            \"instruction\": _require_string(\n                item.get(\"instruction\"), f\"{item_label}.instruction\", allow_empty=True\n            ),\n        }\n        if \"when\" in item:","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/render_skill.py#L56-L92","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before (single table -- wrong)\n[workflow.review_layers]\nid = \"architect\"\nname = \"Architect\"\ninstruction = \"...\"\n\n# after (array of tables -- correct)\n[[workflow.review_layers]]\nid = \"architect\"\nname = \"Architect\"\ninstruction = \"...\"","handlingStrategy":"validation","validationCode":"import tomllib\nfrom pathlib import Path\n\ndef validate_review_layers_is_list(toml_path: Path, dotted: str) -> None:\n    data = tomllib.loads(Path(toml_path).read_text(encoding=\"utf-8\"))\n    cur: object = data\n    for part in dotted.split(\".\"):\n        if not isinstance(cur, dict) or part not in cur:\n            raise SystemExit(f\"missing {dotted}\")\n        cur = cur[part]\n    if not isinstance(cur, list):\n        raise SystemExit(f\"{dotted} must be a list of tables, got {type(cur).__name__}\")","typeGuard":"def is_review_layers(value: object) -> bool:\n    return isinstance(value, list) and all(isinstance(x, dict) for x in value)","tryCatchPattern":null,"preventionTips":["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."],"tags":["python","toml","customization","validation","review-layers"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}