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

customization tokens require customize.toml

Error message

customization tokens require customize.toml

What it means

Defensive invariant: a {workflow.*} token in source requires the customization default table (customize.toml). The guard fires only if _CUSTOM_TOKEN matched in a source while defaults is None. Under the normal render() flow this is unreachable, because has_customization (computed by scanning sources for the same token) gates the loading of defaults -- a match implies defaults was loaded. Reaching it indicates a regression or a direct call to the internal _resolve_replacements with defaults=None.

Source

Thrown at src/scripts/render_skill.py:218

    input_values: dict[str, Any] = {}
    for content in sources.values():
        for match in _SHORT_CONFIG_TOKEN.finditer(content):
            token, key = match.group(0), match.group(1)
            path, resolved = _resolve_short_config(central, key, project_root)
            source = f"config.{path}"
            replacements[token] = resolved
            input_values[source] = resolved
        for match in _CONFIG_TOKEN.finditer(content):
            token, path = match.group(0), match.group(1)
            source = f"config.{path}"
            resolved = _resolve_config_value(
                _lookup(central, path, "config value"), source, project_root
            )
            replacements[token] = resolved
            input_values[source] = resolved
        for match in _CUSTOM_TOKEN.finditer(content):
            if defaults is None:
                raise RenderError("customization tokens require customize.toml")
            token, relative_path = match.group(0), match.group(1)
            path = f"workflow.{relative_path}"
            source = f"customization.{path}"
            resolved, rendered = _resolve_customization_value(
                _lookup(customization, path, "customization value"),
                _lookup(defaults, path, "customization default"),
                source,
            )
            replacements[token] = rendered
            input_values[source] = resolved
    return replacements, input_values


def _render_sources(
    sources: dict[str, str], replacements: dict[str, str], destination: Path
) -> dict[str, str]:
    """Resolve only tokens authored in installed sources in one opaque pass."""
    # Workflow customization may reference installed skill files; bind those

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Do not call _resolve_replacements directly; use the public render(project_root, skill_dir) entry point.
  2. If you must call it, pass a non-None defaults loaded from customize.toml whenever sources contain {workflow.*} tokens.
  3. Ensure customize.toml exists in the skill directory so defaults can be loaded.

Example fix

# before (direct internal call, defaults=None)
_resolve_replacements(sources, central, customization, None, project_root)

# after
defaults = load_toml(skill_dir / "customize.toml", required=True)
_resolve_replacements(sources, central, customization, defaults, project_root)
Defensive patterns

Strategy: try-catch

Validate before calling

from pathlib import Path
import re
CUSTOM = re.compile(r"\{workflow\.([A-Za-z0-9_.-]+)\}")

def ensure_customize_toml_if_needed(sources: dict[str,str], skill_dir: Path) -> None:
    has_custom = any(CUSTOM.search(c) for c in sources.values())
    if has_custom and not (skill_dir / "customize.toml").is_file():
        raise SystemExit("customization tokens present but customize.toml missing")

Try / catch

from render_skill import render, RenderError

try:
    entry = render(project_root, skill_dir)
except RenderError as e:
    if "customization tokens require" in str(e):
        # ensure customize.toml exists, then retry via the public entry point
        ...
    raise

Prevention

When it happens

Trigger: Calling the internal _resolve_replacements(..., None, ...) directly while source content contains {workflow.x}. Not reachable through the public render() entry point, which always loads customize.toml when has_customization is True.

Common situations: Third-party code invoking renderer internals directly; a refactor that breaks the has_customization / defaults coupling.

Related errors


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