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 thoseView on GitHub (pinned to b70486b9bd)
Solutions
- Do not call _resolve_replacements directly; use the public render(project_root, skill_dir) entry point.
- If you must call it, pass a non-None defaults loaded from customize.toml whenever sources contain {workflow.*} tokens.
- 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
- Call the public render() entry point, not internal helpers.
- Always ship customize.toml alongside any skill whose sources use {workflow.*} tokens.
- Don't bypass the has_customization / defaults coupling.
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
- unsupported render token: {token}
- {label} must be a list of tables
- {item_label} must be a table
- duplicate review layer id `{identifier}`
- {label} has unsupported default type {type(default).__name__
AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13).
Data as JSON: /api/errors/c941d0c3e89bdbb7.
Report an issue: GitHub.