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

{label} must be a string, got {type(value).__name__}

Error message

{label} must be a string, got {type(value).__name__}

What it means

`_require_string` is the shared validator render_skill.py uses for every config/customization value that must be text. If the resolved value is not a Python `str` (it is an int, float, bool, list, dict, or None), it raises naming the label (e.g. `config.project.name`) and the actual type. TOML's typed scalars make this common: an unquoted number, a bare `true`/`false`, or an inline table where a string was expected.

Source

Thrown at src/scripts/render_skill.py:57

def _canonical_json(value: Any) -> bytes:
    return json.dumps(
        value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
    ).encode("utf-8")


def _lookup(data: dict[str, Any], dotted_path: str, label: str) -> Any:
    current: Any = data
    for part in dotted_path.split("."):
        if not isinstance(current, dict) or part not in current:
            raise RenderError(f"missing {label} `{dotted_path}`")
        current = current[part]
    return current


def _require_string(value: Any, label: str, *, allow_empty: bool = False) -> str:
    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]] = []

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Quote the value in the TOML layer so TOML yields a string: `name = "BMAD"` not `name = BMAD`.
  2. If a number/bool is genuinely wanted, change the consuming template to stringify it (but the renderer currently requires strings, so prefer quoting).
  3. Audit the merged config to find which layer introduced the non-string.
  4. For booleans, write the intended text explicitly: `enabled = "true"`.

Example fix

# before (config.toml)
[project]
port = 8080          # int
flag = true          # bool

# after
[project]
port = "8080"
flag = "true"
Defensive patterns

Strategy: type-guard

Validate before calling

def all_scalar_values_are_str(d, prefix=''):
    bad = []
    for k,v in d.items():
        p = f'{prefix}.{k}' if prefix else k
        if isinstance(v, dict):
            bad += all_scalar_values_are_str(v, p)
        elif not isinstance(v, list) and not isinstance(v, str):
            bad.append(p)
    return bad

Type guard

def is_str_or_list(v: object) -> bool:
    return isinstance(v, str) or isinstance(v, list)

Try / catch

from render_skill import RenderError
try:
    _require_string(value, label)
except RenderError as e:
    print(f"error: {e}", file=sys.stderr); sys.exit(2)

Prevention

When it happens

Trigger: A config value written as `port = 8080` where the token expects a string; `enabled = true` consumed by a template that needs text; a key set to an inline table `{ ... }` or array where a scalar string is required; a value inherited as `None` from an optional layer.

Common situations: TOML's native typing biting a template author who expected YAML-style everything-is-a-string; a customization override that changed a string default to a number; a layer merge that replaced a string with a non-string.

Related errors


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