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
- SKILL.md anatomy
- Just-in-time step loading
- The resolver scripts
- module.yaml and central config
- The installer
- quick-dev and dev-auto: two flavors of one pipeline
- v6-shims: how old skill names keep working
- Two things that will trip an expert
- The design invariants worth stealing
- Hands-on: run the resolver and the memlog yourself
- Under Claude Code: two just-in-time systems, stacked
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):
- 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. - Execute prepend steps (
activation_steps_prepend). - Adopt the persona (agents) and load persistent facts (entries prefixed
file:load the referenced file's contents; all others are literal facts). - Load config from
{project-root}/_bmad/bmm/config.yamland resolve the config variables. - Greet the user by name, in their language (agents lead with the persona's icon).
- Execute append steps (
activation_steps_append). - 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 inSKILL.mdand carriesassets/(templates, checklist, HTML report template) andreferences/. Nosteps/.bmad-create-epics-and-stories/is step-sharded:steps/step-01-...throughstep-04-...plus a template.bmad-dev-story/embeds its whole procedure as an inline XML<workflow>inSKILL.mdand carries only achecklist.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 acode(or every item has anid), 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) calluv run ...; others (bmad-dev-story,bmad-dev-auto, epics) callpython3 .... Both scripts documentuvas the standardization target withpython3as 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):
- Build config, detect any existing install, back up custom/modified files by
comparing against
_config/files-manifest.csvhashes. - Install shared scripts (wipe and re-copy
src/scripts/*, excluding tests and caches; seed_bmad/custom/.gitignorewith*.user.tomlon a fresh install). - 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). - Generate configs: per-module
config.yaml(legacy), then the centralconfig.toml/config.user.tomland manifests, then apply--setoverrides. - Copy skills into per-IDE directories, then
_cleanupSkillDirsremoves the skill copies from_bmad/("Skills are now in IDE directories, remove redundant copies")._bmad/keeps only module-level files,_config/, andscripts/.
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: draft → ready-for-dev →
in-progress → in-review → done (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:
| Category | Meaning |
|---|---|
intent_gap | root cause is inside the frozen contract; halt/ask a human |
bad_spec | the spec should have prevented it; revert, amend the spec, re-derive |
patch | trivially fixable now; the only findings that survive loopbacks |
defer | pre-existing, appended to deferred-work.md |
reject | noise, 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.tomlis 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.mdare 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, andrecon_kit.pyare 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 primitive | Claude Code mechanism |
|---|---|
| Read a step, do it, load the next | The agent loop (read context, pick a tool, run it, read the result, repeat) |
python3 .../resolve_customization.py, memlog.py | A Bash tool call: the harness shells out, the model does not simulate it |
| Mandatory-synchronous subagents | Agent tool: fresh context window, only the final result returns |
customize.toml DO-NOT-EDIT, installer-owned vs yours | The 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. 👉