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

render entry is missing

Error message

render entry is missing: {skill_dir / 'workflow.md'}

What it means

After collecting sources, _load_sources requires a workflow.md entry -- it is the entry point returned by render() (destination / "workflow.md"). If no workflow.md exists at the skill root, rendering aborts because the snapshot would have no defined entry file.

Solutions

  1. Create workflow.md at the root of the skill directory.
  2. Verify exact lowercase case: workflow.md (not Workflow.md).
  3. Ensure workflow.md is a real file, not an escaped symlink (see the escape guard).

Example fix

# before: only Workflow.md (wrong case) or workfow.md (typo) exists
# after
touch skills/my-skill/workflow.md
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def assert_workflow_entry(skill_dir: Path) -> None:
    entry = skill_dir / "workflow.md"
    if not entry.is_file():
        raise SystemExit(f"missing required entry: {entry}")

Prevention

When it happens

Trigger: A skill directory containing .md files but none named workflow.md at its root; workflow.md is a symlink that escaped (rejected by the earlier guard and thus absent from sources); a case mismatch such as Workflow.md on a case-sensitive filesystem.

Common situations: New skill missing its workflow entry; renamed file; case-sensitivity mismatch between macOS (development) and Linux (CI).

Related errors


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

Appendix: source

Thrown at src/scripts/render_skill.py:114


def _load_sources(skill_dir: Path) -> dict[str, str]:
    sources: dict[str, str] = {}
    for candidate in sorted(skill_dir.rglob("*.md")):
        if candidate.name == "SKILL.md":
            continue
        name = candidate.relative_to(skill_dir).as_posix()
        path = candidate.resolve(strict=True)
        if not path.is_relative_to(skill_dir):
            raise RenderError(f"render source escapes skill directory: {name}")
        if not path.is_file():
            raise RenderError(f"render source is missing or not a file: {path}")
        try:
            sources[name] = path.read_text(encoding="utf-8")
        except (OSError, UnicodeError) as error:
            raise RenderError(f"failed to read render source {path}: {error}") from error
    if "workflow.md" not in sources:
        raise RenderError(f"render entry is missing: {skill_dir / 'workflow.md'}")
    return sources


def _resolve_config_value(value: Any, label: str, project_root: Path) -> str:
    text = _require_string(value, label)
    if "{project-root}" not in text:
        return text
    resolved = text.replace("{project-root}", str(project_root))
    if not Path(resolved).is_absolute():
        raise RenderError(f"{label} must resolve to an absolute path: {resolved}")
    return resolved


def _find_config_values(data: Any, key: str, prefix: str = "") -> list[tuple[str, Any]]:
    matches: list[tuple[str, Any]] = []
    if not isinstance(data, dict):
        return matches
    for name, value in data.items():

View on GitHub (pinned to b70486b9bd)