BMAD internals, from the source

This page opens BMAD v6.10.0 and reads how it actually works, from the source tree, not the docs. When you type a skill, a specific sequence of file reads, Python resolver calls, and just-in-time step loads happens; this is that sequence, with the real file paths and the load-bearing rules quoted from the code. It is the deepest page in the guide and assumes the map and anatomy pages. Paths are relative to the BMAD repo root; line references are from the v6.10.0 tree.

On this page

The one structural fact: everything is a skill

There is no separate "agent type" or "workflow type" in the source. Agents, workflows, tasks, and tools are all skills: a directory containing a SKILL.md. Modules (src/core-skills/, src/bmm-skills/) are just groupings of skill directories. The installer copies these into a project and generates config; at runtime the model reads a SKILL.md, shells out to small Python resolvers to merge configuration, and for complex skills loads numbered step files one at a time. There is no workflow.yaml anywhere in the tree (confirmed: find . -name workflow.yaml returns nothing); the old workflow-engine schema was replaced by the [workflow] table in customize.toml plus a step-file architecture.

SKILL.md anatomy

The frontmatter is the discovery gate

Every SKILL.md opens with YAML frontmatter carrying exactly name and description. The installer's parseSkillMd (tools/installer/core/manifest-generator.js) requires both to be non-empty and enforces a hard rule: the frontmatter name must equal the directory basename, or the skill is rejected (SKILL.md name "..." does not match directory name "..." — skipping). The description is where trigger phrases live, e.g. bmad-dev-story's description embeds Use when the user says "dev this story [story file]". That is how a host matches your natural language to a skill.

The path-variable vocabulary

A near-identical ## Conventions block appears across skills and defines the placeholder namespace. From bmad-dev-story/SKILL.md, verbatim:

- Bare paths (e.g. `steps/step-01-init.md`) resolve from the skill root.
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
- `{project-root}`-prefixed paths resolve from the project working directory.
- `{skill-name}` resolves to the skill directory's basename.

Workflow skills add {workflow.<name>} (a field from customize.toml's [workflow] table) and {doc_workspace} (the bound run folder). Config-sourced variables ({user_name}, {communication_language}, {document_output_language}, {planning_artifacts}, {implementation_artifacts}, {project_name}, {user_skill_level}, {output_folder}, {date}) are filled in the "Load config" activation step. Two placeholder syntaxes coexist: single-curly {workflow.x}, resolved at runtime by the model or inlined by a renderer, and double-curly {{.var}}, used only in quick-dev's pre-rendered step files (below), baked at compile time.

The activation ladder

The ## On Activation sequence is where a skill boots. The canonical ladder (from bmad-agent-dev/SKILL.md):

  1. Resolve the customization block by running a script: python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent (workflows use --key workflow). Every skill ships a verbatim fallback: if the script fails, read the three TOML layers yourself in base → team → user order and apply the same merge rules.
  2. Execute prepend steps (activation_steps_prepend).
  3. Adopt the persona (agents) and load persistent facts (entries prefixed file: load the referenced file's contents; all others are literal facts).
  4. Load config from {project-root}/_bmad/bmm/config.yaml and resolve the config variables.
  5. Greet the user by name, in their language (agents lead with the persona's icon).
  6. Execute append steps (activation_steps_append).
  7. A closing invariant repeated in every skill: "Do not begin the main workflow until all activation steps have been completed."

Agents add step 8, render the menu ({agent.menu} as a numbered Code / Description / Action table) and dispatch on a number, a menu code, or a fuzzy match. bmad-prd adds a Forwarded activation clause: if a caller (a shim, or Mary's menu) supplied a stated intent and pre-resolved fields, honor them verbatim and skip its own inference. That clause is the receiving half of the v6-shims (below).

What a skill directory contains

The contract is SKILL.md (required) plus customize.toml (the customization surface) plus any of steps/, templates/, assets/, references/, checklist.md, scripts/, render.py. Three real shapes:

  • bmad-prd/ inlines its logic in SKILL.md and carries assets/ (templates, checklist, HTML report template) and references/. No steps/.
  • bmad-create-epics-and-stories/ is step-sharded: steps/step-01-... through step-04-... plus a template.
  • bmad-dev-story/ embeds its whole procedure as an inline XML <workflow> in SKILL.md and carries only a checklist.md.

Just-in-time step loading

The clearest statement is bmad-create-epics-and-stories/SKILL.md's ## WORKFLOW ARCHITECTURE block, verbatim:

- Micro-file Design: Each step is a self-contained instruction file
- Just-In-Time Loading: Only 1 current step file will be loaded ... never load
  future step files until told to do so
- Sequential Enforcement: ... no skipping or optimization allowed
...
- NEVER load multiple step files simultaneously
- ALWAYS read entire step file before execution
- NEVER create mental todo lists from future steps

The mechanism: SKILL.md hands off to ./steps/step-01-...; the instruction to load the next step is embedded at the bottom of each step file, behind a menu gate ("ONLY WHEN C is selected and all requirements are saved ... will you then read fully and follow ./step-02-..."). State is tracked in the output document's frontmatter, not the step files (SAVE STATE: Update stepsCompleted in frontmatter before loading next step). This is virtual memory for an agent: keep the active working set to one step, persist completed work to disk, load the next dependency only when control reaches it.

bmad-dev-story is the other flavor: instead of separate files, its whole procedure is an inline XML <workflow> with <step n="1..10">, <check if=...>, <action>, <goto step=...>, <ask>, <output>, and <critical> tags. It is just-in-time in the sense of <goto> control flow (step 8 loops back with <goto step="5">), not file loading.

The resolver scripts

Three stdlib-only Python scripts under src/scripts/ do the deterministic work the model should never do by hand. resolve_customization.py and resolve_config.py need Python 3.11+ (tomllib); memlog.py targets 3.8+.

resolve_customization.py: the three-layer merge

It merges a skill's customization from three layers, lowest to highest:

{skill-root}/customize.toml                       skill defaults   (lowest)
{project-root}/_bmad/custom/{name}.toml            team, committed
{project-root}/_bmad/custom/{name}.user.toml       personal, gitignored (highest)

The merge is structural, by value shape (no field-name special-casing):

  • both dicts → deep-merge (recurse);
  • both lists → _merge_arrays: if every item is a dict with a code (or every item has an id), merge by that key (matching key replaces in place, new keys append); otherwise plain append;
  • otherwise → the override wins (scalars replace).

The load-bearing invariant, from the docstring: "No removal mechanism, overrides cannot delete base items. To suppress a default, fork the skill or override the item by code with a no-op." This is why every customize.toml says "DO NOT EDIT" and why overrides append rather than replace wholesale. Project-root discovery walks up until it finds a directory containing _bmad or .git, explicitly to avoid a stray _bmad/ in an ancestor of the cwd.

resolve_config.py: the four-layer central config

Same merge code, four layers (highest last):

{project-root}/_bmad/config.toml              installer team   (base, required)
{project-root}/_bmad/config.user.toml         installer user
{project-root}/_bmad/custom/config.toml       human team, committed
{project-root}/_bmad/custom/config.user.toml  human user, gitignored (highest)

This reads [core], [modules.<code>], and [agents.<code>] tables. It is the resolver the party-mode and forge scripts call with --key agents to get the installed roster.

memlog.py: append-only run memory

The ledger behind .memlog.md (see the memory page), with three invariants stated in its docstring: append-only ("There is no edit or delete subcommand by design; history is never rewritten"), write-only/blind (every command is atomic and echoes state as one JSON line, so the caller never re-reads mid-session), and no lifecycle status (completion is a logged event, never mutated frontmatter). Writes are atomic (temp file, flush, os.fsync, os.replace). Subcommands: init (errors if the file exists, --field KEY=VALUE seeds frontmatter), append (--text required, optional --type and --by, collapsed to one line), set (replace one frontmatter field). Skills invoke it as uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type <...> --text "...".

Two runner conventions coexist in the source: newer skills (bmad-prd, bmad-party-mode) call uv run ...; others (bmad-dev-story, bmad-dev-auto, epics) call python3 .... Both scripts document uv as the standardization target with python3 as the transitional fallback.

module.yaml and central config

A module declares its config prompts and (for BMM) its agent roster. src/core-skills/module.yaml declares five prompts, each with a prompt, optional scope (user vs team-default), default, and result: user_name, project_name, communication_language, document_output_language, and output_folder (default _bmad-output, with result: "{project-root}/{value}" baking an absolute path). It creates exactly one directory at install (output_folder); everything else is "created lazily by the first skill that writes there." src/bmm-skills/module.yaml adds user_skill_level (beginner/intermediate/expert), planning_artifacts, implementation_artifacts, and project_knowledge (default docs), plus the six-agent roster as essence only (code, name, title, icon, team, description); the full persona lives in each agent's customize.toml, and party-mode / retrospective / help read these descriptors "to route, display, and embody agents."

At install, each prompt's scope routes the answer: scope: user lands in config.user.toml, everything else in config.toml. Core keys are stripped from non-core module buckets (a workflow reads {user_name} from [core] and {implementation_artifacts} from [modules.bmm]).

The installer

Entry point tools/installer/bmad-cli.js (a commander CLI aliased bmad / bmad-method). The pipeline (core/installer.js):

  1. Build config, detect any existing install, back up custom/modified files by comparing against _config/files-manifest.csv hashes.
  2. Install shared scripts (wipe and re-copy src/scripts/*, excluding tests and caches; seed _bmad/custom/.gitignore with *.user.toml on a fresh install).
  3. Install each module's skills; resolve each external module's version/channel (GitHub-API tag fetching lives in modules/channel-resolver.js, which refuses silent major bumps).
  4. Generate configs: per-module config.yaml (legacy), then the central config.toml / config.user.toml and manifests, then apply --set overrides.
  5. Copy skills into per-IDE directories, then _cleanupSkillDirs removes the skill copies from _bmad/ ("Skills are now in IDE directories, remove redundant copies"). _bmad/ keeps only module-level files, _config/, and scripts/.

Two findings worth internalizing. First, the installer does not generate SKILL.md from templates, the source SKILL.md files are the shipped artifacts, copied verbatim; collectSkills discovers a directory as a skill iff its SKILL.md frontmatter validates and the name matches the directory. Second, files-manifest.csv carries sha256 hashes, which is how a re-install detects files you modified and backs them up before overwriting. This is the mechanism behind the "installer-owned vs yours" rule on the anatomy page.

quick-dev and dev-auto: two flavors of one pipeline

Both run the same spec → implement → review pipeline; quick-dev is interactive (human checkpoints, five step files plus a one-shot branch), dev-auto is one unattended iteration (no human, a HALT-with-status protocol).

quick-dev's render.py is unique. Its SKILL.md is a stub that runs uv run {skill-root}/render.py, a compile-time template renderer. render.py finds the project root, four-layer-merges the central config, resolves the skill's [workflow] block with the same structural merge as the resolver, substitutes {{.var}} (config) and {workflow.<key>} (customization) into every .md except SKILL.md, writes the rendered files to _bmad/render/bmad-quick-dev/, and prints read and follow .../workflow.md. It HALTs before rendering if any referenced {{.var}} is missing ("a corrupted workflow"). Notably, _render_review_layers materializes each [[workflow.review_layers]] table into an invocation block, and a layer with an empty instruction is dropped, which is how an override turns off a default reviewer. dev-auto has no render.py; it must run unattended, so it resolves customization at runtime in its own activation.

The spec's frozen kernel. Both write a spec file whose intent section is human-owned and read-only. The tag differs (dev-auto uses <intent-contract>, quick-dev uses <frozen-after-approval reason="human-owned intent">) but the content is the same three parts: Intent (Problem, Approach), Boundaries (Always / Block-If-or-Ask-First / Never), and an I/O and Edge-Case Matrix. Outside the frozen block: a Code Map, Tasks and Acceptance (Given/When/Then), an append-only Spec Change Log, Design Notes, and Verification. Both target 900 to 1600 tokens ("above 1600 = high risk of context rot"), and every downstream step re-states that the frozen block is read-only.

Status lifecycle. Frontmatter status: draftready-for-devin-progressin-reviewdone (dev-auto adds blocked). Step 1 routes an existing spec by its status. dev-auto's HALT protocol replaces human checkpoints: it writes a terminal status and a blocking condition back to the spec, runs any on_complete, and stops. Blocking conditions are enumerated throughout (unclear intent, intent gap, no subagents, review repair loop exceeded 5 iterations (non-convergence), no epic spec found, ...).

The review loop is the core mechanism. Set in-review, build a diff since the baseline revision (read-only, "do NOT git add"), run the resolved review_layers in parallel, then classify each finding by consequence (discarding the subagent's own severity, since reviewers run under deliberate information asymmetry) and route it into exactly one of five triage categories:

CategoryMeaning
intent_gaproot cause is inside the frozen contract; halt/ask a human
bad_specthe spec should have prevented it; revert, amend the spec, re-derive
patchtrivially fixable now; the only findings that survive loopbacks
deferpre-existing, appended to deferred-work.md
rejectnoise, dropped silently

Findings cascade (an intent_gap moots lower findings; a bad_spec moots them because the code will be re-derived), and the loopback counter HALTs after 5 iterations to guarantee convergence. dev-auto also runs a Matrix Test Audit: every I/O-matrix row must have a covering test that actually ran and passed ("a covering test that exists but did not run ... counts as missing ... never edit the expectation to match the code").

Subagents. dev-auto: "Using subagents when instructed is mandatory. If you cannot, HALT with status blocked and blocking condition no subagents," and it requires synchronous invocation, no detached/background execution, because "there is no event loop to resume a yielded turn" in an unattended run. quick-dev degrades to inline execution on hosts without subagents, or writes prompt files for the human to run.

v6-shims: how old skill names keep working

src/*/v6-shims/ hold forwarders: skills with no logic that forward to their replacement. bmad-create-prd / bmad-edit-prd / bmad-validate-prd forward to bmad-prd (create/update/validate intents); bmad-market-research / bmad-domain-research / bmad-technical-research forward to bmad-deep-recon; bmad-create-architecture forwards to bmad-architecture. A forwarder resolves its own (legacy) customization, emits a deprecation notice, then "invokes the target with a stated intent and pre-resolved fields, skip[ping] the target's own intent detection." The folder is grouping only, the installer discovers each forwarder under its own name, so nesting does not change any installed path. Their removal "rides the v7 cut, never a 6.x minor."

Two things that will trip an expert

The dual-config transition. There are two live config systems. SKILL.md activation steps say "Load config from _bmad/bmm/config.yaml" (the legacy per-module YAML), but the resolver scripts and render.py read the central config.toml + config.user.toml + custom/config.toml + custom/config.user.toml (the newer four-layer TOML). Both are generated from the same install answers so they agree; the TOML path is the newer, resolver-backed one that agent-roster consumers (party-mode, forge) rely on. When something reads stale config, know which of the two it is looking at.

The party/forge persona resolvers (resolve_party.py, resolve_personas.py) are the pattern to copy if you build your own multi-agent skill: Python does the deterministic roster resolution once (shelling into resolve_config.py --key agents for the installed roster and resolve_customization.py for custom personas, projecting only what a moment needs), and the LLM orchestrator consumes the resolved JSON instead of re-deriving the roster every session.

The design invariants worth stealing

Reading the source, the same handful of moves recur, and they are the real lessons for anyone building agent tooling:

  • customize.toml is machine-owned; overrides are sparse and append-only. There is no delete operator, on purpose.
  • Append-only artifacts everywhere. The memlog, the Spec Change Log, the Review Triage Log, and deferred-work.md are all append-only; resume state is learned by reading the tail, never by mutating history.
  • Extract, don't ingest. Source documents go to subagents; the parent context assembles from digests. The 900-to-1600-token spec ceiling and quick-dev's cached epic context are the same context-budget discipline.
  • Deterministic work goes to Python; judgment stays in the model. The resolvers, memlog.py, render.py, and recon_kit.py are all exact, repeatable jobs pulled out of the LLM's hands.

Hands-on: run the resolver and the memlog yourself

The two Python scripts this page describes are stdlib-only, so you can run them against a real BMAD checkout and watch the deterministic layer work. Everything below was run against the v6.10.0 tree from the repo root; the output is real, not illustrative.

The three-layer merge. Point resolve_customization.py at any skill directory and it prints the merged customization as JSON. Against bmad-forge-idea with no project overrides, you get the skill's own defaults:

python3 src/scripts/resolve_customization.py --skill src/core-skills/bmad-forge-idea
{
  "workflow": {
    "activation_steps_prepend": [],
    "activation_steps_append": [],
    "persistent_facts": [
      "file:{project-root}/**/project-context.md"
    ],
    "on_complete": [],
    "forge_output_path": "{output_folder}/forge",
    "run_folder_pattern": "{slug}"
  }
}

Now add the two override layers an installed project would have, a committed team file and a gitignored personal file, both under the project's _bmad/custom/:

# _bmad/custom/bmad-forge-idea.toml        (team, committed)
[workflow]
forge_output_path = "{output_folder}/team-forge"
persistent_facts = ["Our kill-criteria template lives at docs/kill-criteria.md"]
# _bmad/custom/bmad-forge-idea.user.toml   (personal, gitignored)
[workflow]
forge_output_path = "{output_folder}/my-forge"

The resolver walks up from the skill directory to find the project root (the nearest ancestor with a _bmad/ or .git/), so with those two files in place the same command returns the merged result, and the merge rules from earlier fire: the persistent_facts array is unioned (base glob plus the team fact), and the scalar forge_output_path takes the highest layer present (yours):

{
  "workflow": {
    "activation_steps_prepend": [],
    "activation_steps_append": [],
    "persistent_facts": [
      "file:{project-root}/**/project-context.md",
      "Our kill-criteria template lives at docs/kill-criteria.md"
    ],
    "on_complete": [],
    "forge_output_path": "{output_folder}/my-forge",
    "run_folder_pattern": "{slug}"
  }
}

That is the whole override system in one command: arrays merge, scalars take the top layer, nothing is deleted. Ask for a single field with --key:

python3 src/scripts/resolve_customization.py \
  -s src/core-skills/bmad-forge-idea -k workflow.forge_output_path
{
  "workflow.forge_output_path": "{output_folder}/my-forge"
}

The append-only memlog. The other script is the working memory a skill keeps during a run. Initialize a run folder, append entries the way a skill would, and read the ledger:

python3 src/scripts/memlog.py init --workspace run/ --field title="Forge: transcribe-on-ingest"
python3 src/scripts/memlog.py append --workspace run/ --type idea --by user \
  --text "Transcribe every saved episode on ingest"
python3 src/scripts/memlog.py append --workspace run/ --type decision --by coach \
  --text "Too costly on a solo budget; transcribe on first open instead"
python3 src/scripts/memlog.py append --workspace run/ --type event --text "session complete"

Each command echoes the new state as one line of JSON ({"ok": true, ..., "entries": N}) so the caller never re-reads the file mid-run. The result, run/.memlog.md:

---
title: Forge: transcribe-on-ingest
updated: 2026-07-26T16:55
---

- (idea by user) Transcribe every saved episode on ingest
- (decision by coach) Too costly on a solo budget; transcribe on first open instead
- (event) session complete

There is no edit or delete subcommand by design: history is appended, never rewritten, which is what lets a resume trust it. Run these two scripts once and the rest of this page stops being abstract: at runtime the model is just calling them and consuming the JSON they print.

Under Claude Code: two just-in-time systems, stacked

Read this page's mechanics against the harness and a pattern falls out: BMAD's design mirrors the host it runs on, so two just-in-time layers stack. Claude Code loads a SKILL.md the same way BMAD loads a step. At session start only the frontmatter name and description sit in context (progressive disclosure, about 50 tokens each); the body loads the moment the skill is invoked, then persists for the rest of the session. Under later auto-compaction it keeps roughly the first 5k tokens per skill against a shared 25k budget. So BMAD's one-step-at-a-time step loading runs inside Claude Code's one-skill-at-a-time skill loading: JIT nested in JIT.

The rest lines up nearly one to one:

BMAD primitiveClaude Code mechanism
Read a step, do it, load the nextThe agent loop (read context, pick a tool, run it, read the result, repeat)
python3 .../resolve_customization.py, memlog.pyA Bash tool call: the harness shells out, the model does not simulate it
Mandatory-synchronous subagentsAgent tool: fresh context window, only the final result returns
customize.toml DO-NOT-EDIT, installer-owned vs yoursThe settings and permission layer wrapping every tool call

The agent loop is what walks BMAD's markdown procedure step by step: the "load the next step file only when control reaches it" rule holds because each turn is a real read, decide, act cycle, not a plan built up front. When a step says run resolve_customization.py, that is a literal Bash invocation; the three-layer TOML merge happens in Python on disk and the model consumes the JSON it prints. When dev-auto demands a synchronous subagent, the host's Agent tool supplies the isolated window and the results-only return that the "extract, don't ingest" discipline (above) assumes.

Net: BMAD is a markdown-and-Python method that borrows its loading, execution, and isolation from whatever host runs it. On Claude Code those primitives happen to line up almost exactly, which is why the source reads like a description of the harness underneath it.

That is BMAD with the lid off: a set of markdown procedures over a thin deterministic Python spine, resolved through layered TOML, loaded just in time, and persisted to append-only files. The next page turns to how you spend model capability and tokens across all of this: which model per phase, and at what thinking level. 👉