# Swarms of sub-agents

> When to run a panel, a jury or a fanout of sub-agents, the four files that define one, seeded dealing, the 26-unit budget, honest tallies, and a worked buyer panel with real output.

Canonical: https://bmad-plus.rochetta.fr/docs/concepts/swarm/
Language: en
BMAD+ version: 0.23.1
Documentation reviewed: 2026-10-19
Author: Laurent Rochetta
License: MIT

BMAD+ documentation › [Concepts](https://bmad-plus.rochetta.fr/docs/concepts/) › Swarms of sub-agents

A swarm is many sub-agents doing the same kind of work once each: thirty simulated buyers reading a pitch, three jurors reading it through one lens each, one writer per page of a site. The moderator is your assistant; it never does a unit’s work. `bmad-plus swarm` is the bookkeeper: it validates the files, deals the units, writes one brief per sub-agent, checks every answer file and counts only the valid ones. It never calls a model, never opens a network connection and never starts a process. This page is for anyone who wants to test a message, a page or a plan against many readers, or to split repeated work across sub-agents under a budget.

<a id="video-ask-then-swarm"></a>
Video: [Ask first, then swarm](https://bmad-plus.rochetta.fr/media/ask-then-swarm-en.mp4) — The intake questions, the budget of 26 and the approval step, waves of sub-agents, and a tally that says simulated units are not people.
[Captions (WebVTT)](https://bmad-plus.rochetta.fr/media/ask-then-swarm-en.vtt)

Transcript:

> Before a pack acts, it asks. The intake puts a few questions at a time and marks the must-haves. Where you have no answer, it proposes a default, or records the question as unknown with its risk. The answers become a brief, and nothing in it is invented. Next, the swarm. By default, a run may use 26 sub-agents before the assistant must ask you. Ask for more, and the run waits, marked needs approval, until a person says yes. Once approved, sub-agents work in waves of ten by default, each reading one brief and writing one answer. The runtime validates every answer file and counts only the valid ones. This tally is demo data, and it opens with its disclosure: simulated units are not people. Read the rates as an upper bound, and confirm them with real people.

Described transcript:

- **0:00** On screen: A single amber point of light in a dark teal cloud, with faint rays spreading from it. The caption reads “Ask first”. Narration: Before a pack acts, it asks.
- **0:04** On screen: A terminal window titled “~/app”. The command bmad-plus intake plan is typed, pointing at the Design pack’s intake.yaml and an answers file. The view zooms in on the result: “design-read (design): not ready — 0 answered, 0 defaulted, 0 unknown” and, outlined in orange, “Must-haves still open: surface_kind, audience, job”. Below is the first batch of questions (at most 6 per message), each with its type and a “must-have” mark where it applies: surface_kind, audience, job, brand_assets, design_system and aesthetic_family. Then bmad-plus intake record is typed with three answers (surface_kind=dashboard; audience=“Accountants in a practice, on a desktop, at month end”; job=UNKNOWN) and a default for design_system. The next batch of questions follows: brand_assets, aesthetic_family, variance, motion, density and stack. The screen ends on a yellow line, outlined: “Unknown: job — risk: There is no goal to review against, so hierarchy and the primary action cannot be judged, only decorated.” Narration: The intake puts a few questions at a time and marks the must-haves. Where you have no answer, it proposes a default, or records the question as unknown with its risk.
- **0:19** On screen: Two terminal panes. On the left, bmad-plus intake render is typed and prints “Brief written to _bmad-output/design/brief.md (3238 bytes); the intake is ready.” The right pane opens brief.md and scrolls through it. Under headings such as “Who and why”, each question appears with its answer (“dashboard”; “Accountants in a practice, on a desktop, at month end”) or with “Answer: not asked yet.”, and a line “Feeds: brief.md#…”. The pane stops on a heading, “Open questions”. The first item is outlined: “job (must-have): unknown — risk: There is no goal to review against, so hierarchy and the primary action cannot be judged, only decorated.” Below it, a list of other items, each marked “not asked yet”: brand_assets, aesthetic_family, variance, motion, density, stack, accessibility_target, languages and more. Narration: The answers become a brief, and nothing in it is invented.
- **0:29** On screen: A terminal. The command bmad-plus swarm init is typed with a swarm definition, a profile, a context file, 40 units (--n 40) and a seed of 7, followed by an echo of the exit code. The output: “buyer-panel-20261018-asuq: needs approval.” (the words “needs approval” are outlined) “The run needs 40 sub-agents and the budget is 26: ask the user, then re-run init with --approved --seed 7.” and “exit code: 2” (outlined). The same command is run again with --approved. The view zooms in on the result, with “panel of 40 units” outlined: “buyer-panel-20261018-zho8: panel of 40 units, budget 26, approval user. Seed 7 (same seed, same cards).” and “Next: bmad-plus swarm briefs buyer-panel-20261018-zho8”. Narration: Next, the swarm. By default, a run may use 26 sub-agents before the assistant must ask you. Ask for more, and the run waits, marked needs approval, until a person says yes.
- **0:46** On screen: Amber particles fly outward from a bright centre and form a widening ring against a dark purple background. The caption reads “Waves of 10”. Narration: Once approved, sub-agents work in waves of ten by default, each reading one brief and writing one answer.
- **0:56** On screen: A terminal. bmad-plus swarm briefs buyer-panel-20261018-zho8 prints “40 of 40 units need a sub-agent, in waves of 10.” and lists the briefs wave by wave, each with the path of a brief file (B001.md, B011.md, B021.md, B031.md). Below are the instruction “Each sub-agent: Read the brief and follow it exactly; it is your whole instruction.” and the disclosure line “Simulated units are not people. Read rates as an upper bound and confirm with real ones.” Then bmad-plus swarm check prints “8 of 40 valid answers.” and lists the problems: “B009 invalid: field “buys” must be true or false” (outlined), “B010 missing: no answer file”, “B011 missing: no answer file”, then “B012 … B040 (missing)”. Narration: The runtime validates every answer file and counts only the valid ones.
- **1:07** On screen: Two terminal panes. The command bmad-plus swarm tally buyer-panel-20261018-zho8 is typed on the left and prints the path of tally.md, “8 of 40 units counted; 32 skipped.” and the “Disclosure:” line. The right pane opens tally.md and scrolls through short quoted replies grouped by segment (mid career, high earner) and behaviour (occasional, explorer, daily regular), then stops on “Numbers: price_floor”: a table with the columns group, count, median, mean, min and max. The first row, “all”, reads 8, 3.65, 3.7, 3 and 4.5. Rows for three segments and three behaviours follow. Narration: This tally is demo data, and it opens with its disclosure: simulated units are not people.
- **1:16** On screen: Under water: many translucent bubbles drift in blue light, and two of them glow amber and move together until they overlap. The caption reads “Simulated units are not people.” Narration: Read the rates as an upper bound, and confirm them with real people.

> **Warning · Simulated units are not people** A panel gives you the reading of a model playing buyers. Treat its rates as an upper bound to confirm with real people, and its quotes as what a simulated unit wrote, never as testimonials or user interviews. Every tally starts with this sentence.

<a id="when-to-use"></a>
## When to use a swarm

**The three kinds of swarm**

| Kind | Units come from | Typical use | What the tally reports |
| --- | --- | --- | --- |
| `panel` | A profile: units are dealt by exact quotas, from a seed. | Simulated buyers, readers or users reading a pitch, a price, a page. | A rate by group, counts, verbatim quotes, numbers. |
| `jury` | A list in the definition: one unit per entry, each with a `lens`. | An adversarial read of a pitch, a page or a plan, one angle per juror. | Counts and quotes per lens. |
| `fanout` | A units file you write at init: one unit per entry, each with a disjoint write `scope`. | Pages to write, modules to document, scenes to prepare, one sub-agent each. | A rate and numbers; can be exported as a durable Nexus plan. |

A panel and a jury run as session sub-agents: your assistant launches them, waits, and reads their files. A fanout can run the same way, or be exported with `swarm plan` as a durable Nexus plan when the work must survive the session, needs an independent check per unit, or edits the repository.

<a id="when-not"></a>
### When not to use one

- When you can ask real people. A swarm rehearses; it does not measure.
- For two or three tasks. Ask the agent directly, or use Nexus `parallel`.
- For work where one result depends on another. Units are independent by construction.
- To generate paid media. Image, video and audio generation is never covered by the budget: the assistant asks every time.

In 0.23.0 no pack ships a swarm definition: the runtime and the moderator skill are Core, and the definitions used on this page are test fixtures in the BMAD+ repository (`tests/fixtures/swarm/`). A pack, or you, supply the definition. These files are not in the npm package, so this page prints each one in full: you can create them as they are.

<a id="how-it-flows"></a>
## The flow, end to end

**Files and commands**

```text
definition + profile|list|units + context
        |
      init ---- over budget? exit 2 "needs approval": ask a human, re-run with --approved
        |
   cards.json  (frozen)
        |
     briefs ---- one brief per unit, in waves
        |
  one sub-agent per brief  ->  answers/<unit>.json
        |
      check ---- missing / invalid, with reasons
        |  (second chance once; save files a printed reply)
      tally ---- tally.json + tally.md, disclosure first
```

1. **init** Validates the definition, the profile or units file, the context files and the brief template; deals or lists the cards; applies the budget. A panel needs `--n` (1 to 1000); the seed is drawn and recorded when you do not give one. A run larger than the budget stops here with exit code 2.

   ```sh
   npx bmad-plus swarm init --definition swarm/buyer-panel.json --profile swarm/profile.json --context pitch=swarm/pitch.md --n 30 --seed 7
   ```
2. **briefs** Renders one brief per unit that has no valid answer yet, from the frozen template, the card and the frozen contexts, and prints the waves (10 units per wave unless `--wave` says otherwise) with the brief path of each unit.

   ```sh
   npx bmad-plus swarm briefs RUN
   ```
3. **Launch a wave** Your assistant starts one sub-agent per brief with exactly one instruction: read the brief and follow it exactly; it is the whole instruction. It does not summarise the brief, add context or merge two units into one sub-agent. It waits for the whole wave before the next.
4. **check** Lists every unit whose answer is missing or invalid, with the reason. Exit code 0 when all units are valid, 1 otherwise.

   ```sh
   npx bmad-plus swarm check RUN
   ```
5. **Second chance, once** Run `briefs` again (it only renders units without a valid answer) and relaunch those units. A unit that printed its answer instead of writing it is filed with `save RUN UNIT --input FILE`, where the file holds that unit’s reply verbatim. A unit still missing after the second wave stays missing.

   ```sh
   npx bmad-plus swarm save RUN B005 --input swarm-inbox/B005.txt
   ```
6. **tally** Counts valid answers only and writes `tally.json` and `tally.md`. Units without a valid answer are listed as skipped and counted in the denominator line. The disclosure is the first line of `tally.md`.

   ```sh
   npx bmad-plus swarm tally RUN
   ```
7. **plan (fanout only)** Instead of running the sub-agents inside the session, exports the fanout as a Nexus plan. See the Nexus page.

   ```sh
   npx bmad-plus swarm plan RUN --out .bmad-plus/swarm/reports/RUN/plan.json
   ```

`swarm inspect RUN` prints the state of a run (units, briefed, valid, invalid, missing) at any time and writes nothing. Exit codes: 0 done, 2 `needs-approval` (a human must say yes first), 1 incomplete or error. With `--json`, every answer carries `schemaVersion: 1` and a `status`.

<a id="budget"></a>
## The budget, the waves and the approval

Three keys in the `swarm` block of `_bmad/config.yaml` set the limits. The installer writes the block with its defaults; a project updated from an earlier version has no block and gets the same defaults.

**The swarm block**

| Key | Default | Meaning |
| --- | --- | --- |
| `budget` | 26 | Sub-agents one run may use before the assistant must ask you. Integer from 1 to 1000. |
| `wave` | 10 | Sub-agents launched together. The assistant waits for a wave before the next. Integer from 1 to 1000. |
| `media_credits` | `ask` | Paid generation (images, video, audio) is never approved by a budget. The only accepted value is `ask`. |

When `n` is larger than the budget and `--approved` is absent, `init` creates the run with the status `needs-approval`, exits with 2 and writes no brief. The assistant asks you one sentence with the number ("This panel needs 30 sub-agents; the budget is 26. Run it?"). Only after your yes does it re-run `init` with `--approved` and the same seed. It never passes `--approved` on its own judgement. The refused run is terminal: later commands on it answer `needs-approval` and write nothing.

**Refused, then approved: a panel of 30 against a budget of 26**

```text
$ npx bmad-plus swarm init --definition swarm/buyer-panel.json --profile swarm/profile.json --context pitch=swarm/pitch.md --n 30 --seed 7
buyer-panel-20261019-g1i9: needs approval. The run needs 30 sub-agents and the budget is 26: ask the user, then re-run init with --approved --seed 7.
# Exit code 2. No brief was written.
$ npx bmad-plus swarm init --definition swarm/buyer-panel.json --profile swarm/profile.json --context pitch=swarm/pitch.md --n 30 --seed 7 --approved
buyer-panel-20261019-vqkf: panel of 30 units, budget 26, approval user.
Seed 7 (same seed, same cards).
Next: bmad-plus swarm briefs buyer-panel-20261019-vqkf
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-19

<a id="files"></a>
## The four files, field by field

The runtime refuses an unknown field, a wrong type and a bound breach; it never guesses. Every path is project-relative with `/` separators. The brief template path is relative to the definition file.

<a id="file-definition"></a>
### 1. The definition (JSON)

**Definition fields**

| Field | Rule |
| --- | --- |
| `schemaVersion` | Must be `1`. |
| `id` | 1 to 64 lowercase letters, digits or `-`. Opens the run id. |
| `kind` and `units.source` | The pair must match: `panel` with `profile`, `jury` with `list`, `fanout` with `input`. |
| `units.idPrefix` | 1 to 4 letters. Required for a panel; optional for a jury whose entries have no `id`; absent for a fanout. |
| `units.list` | Jury only: 1 to 1000 entries, each with a `lens` text and any other text, number or boolean fields; an optional `id`. |
| `pack` | Optional pack id, recorded in the run. |
| `title` | Heading of the tally. |
| `brief.template` | Plain relative path to a Markdown file, no `.` or `..` part. |
| `brief.context` | Names of the context files the moderator must pass at init (`--context NAME=FILE`). Each is required; at most 20. |
| `answer.required` | Fields that must be present. `id` is implicit and always required. |
| `answer.fields` | At most 50 fields, each typed (see the next table). A sub-agent may not write a field that is not declared. |
| `tally` | `rate` (a boolean field), `groupBy` (up to 5 card fields every card carries), `counts` (enum fields), `quotes` (string field to quotes kept per group, 1 to 10), `numbers` (integer or number fields). |
| `disclosure` | 1 to 500 characters, printed first in every tally and in the `briefs` output. |

**Answer field types**

| Type | Options | Accepted value |
| --- | --- | --- |
| `boolean` | none | `true` or `false`. |
| `integer` | `min`, `max` | A whole number within the bounds. |
| `number` | `min`, `max` | Any number within the bounds. |
| `string` | `min`, `max` (length) | Text; default 0 to 4000 characters, ceiling 20000; `min` is checked on the trimmed text. |
| `enum` | `values` (1 to 50 distinct) | One of the listed values. |
| `list` | `maxItems` (default 50, ceiling 200) | A list of non-empty texts, each up to 1000 characters. |

<a id="file-profile"></a>
### 2. The profile (panel only)

**Profile fields**

| Field | Rule |
| --- | --- |
| `subject`, `target` | What is being judged and who judges it. Documentation for readers of the file. |
| `dimensions` | 1 to 10 dimensions, each with a `name` and 1 to 50 `values`. The shares of one dimension add up to 1. |
| `values[].value`, `share` | The label of a value and its share of the panel. |
| `values[].ranges` | Optional numeric ranges sampled when that value is dealt, such as `"income": [38000, 85000]`. Every value of a dimension declares the same range names. |
| `values[].note` | Optional sentence. It becomes the card field `<dimension>_note` (empty for values without one). |
| `ranges` (top level) | Ranges sampled once per card. Integer bounds deal integers; other bounds deal numbers with two decimals. |
| `pools` | Named lists of sentences. One entry of each pool is dealt per card. |

Card fields are the dimension names, their range names, the top-level range names, the pool names and the notes. Names must be distinct, and `id`, `scope` and `lens` are reserved. Names of people are not dealt: a brief that wants one draws it from a pool the pack supplies.

<a id="file-units"></a>
### 3. The units file (fanout only)

**swarm/units.json**

```json
{
  "schemaVersion": 1,
  "units": [
    { "id": "about", "scope": "site/about", "title": "About us", "tone": "warm" },
    { "id": "pricing", "scope": ["site/pricing", "site/data/prices.json"], "title": "Pricing", "tone": "plain" },
    { "id": "contact", "scope": "site/contact", "title": "Contact", "tone": "short" }
  ]
}
```

- `id`: 1 to 64 lowercase letters, digits, `_` or `-`. It becomes a Nexus task id.
- `scope`: one path or a list of up to 20, portable and project-relative, never `.git` or `.bmad-plus`. Scopes are pairwise disjoint: no scope may contain another.
- Every other field must be a text, number or boolean; it becomes a card field available as `{{card.field}}` in the brief.

The definition that goes with this units file has no profile; `init` takes the units with `--units`, for instance `npx bmad-plus swarm init --definition swarm/page-fanout.json --units swarm/units.json`. This is the definition of the `site-pages` fanout used in the example at the end of this page, followed by its brief template.

**swarm/page-fanout.json (the definition)**

```json
{
  "schemaVersion": 1,
  "id": "site-pages",
  "kind": "fanout",
  "title": "One sub-agent per page of the site",
  "units": { "source": "input" },
  "brief": { "template": "briefs/page.md", "context": [] },
  "answer": {
    "required": ["id", "done", "words"],
    "fields": {
      "done": { "type": "boolean" },
      "words": { "type": "integer", "min": 0 },
      "note": { "type": "string", "max": 200 }
    }
  },
  "tally": { "rate": "done", "numbers": ["words"] },
  "disclosure": "Each page was written by a separate sub-agent from the same brief; read them together before publishing."
}
```

**swarm/briefs/page.md (the template)**

```md
# Page {{unit.id}}

Write the page "{{card.title}}" under `{{card.scope}}`, {{card.tone}} in tone. Touch nothing
outside that folder.

When the page is written, write a JSON object to `{{answer.path}}` with `id` ("{{unit.id}}"),
`done`, `words` (the word count) and an optional `note`.

{{disclosure}}
```

<a id="file-template"></a>
### 4. The brief template (Markdown) and the contexts

**Placeholders: nothing else is allowed between double braces**

| Placeholder | Renders |
| --- | --- |
| `{{card.<field>}}` | The card field; a list renders comma-separated. |
| `{{context.<name>}}` | The context file passed at init, verbatim and frozen in the run. |
| `{{answer.path}}` | The absolute path of the answer file the sub-agent must write. |
| `{{run.id}}`, `{{unit.id}}` | The run id and the unit id. |
| `{{disclosure}}` | The definition’s disclosure sentence. |

> **Warning · The runtime filters nothing** Card values, pool entries, listed unit fields and context files reach the sub-agent word for word. A pool entry that reads like an instruction lands in the brief as an instruction. The pack owns its pools, unit fields and templates; you own the contexts you pass. Read a context before passing it, and never pass a file you have not read: it is included in every brief, secrets and instructions alike. An unknown placeholder, a card field some card lacks, an undeclared context or a literal `{{` in the template makes `init` fail.

<a id="dealing"></a>
## Deterministic dealing and seeds

The same profile, the same `--n` and the same `--seed` always give the same cards. Each dimension is distributed by exact quota (largest remainder, ties by declaration order): with shares of 0.45, 0.40 and 0.15, twenty cards hold 9, 8 and 3 units of those values whatever the seed. The column of values is then shuffled by a generator based on SHA-256, and card k takes position k of every column; ranges and pool entries follow, in order. The seed (0 to 2147483647) is recorded in `run.json`.

This is what makes a before and after comparison honest. To test two versions of a pitch, run the same profile with the same `--n` and `--seed` and change only the context file: the buyers are the same, so the only thing that changed is the pitch. Unit ids are the prefix plus the position padded to at least three digits (`B001`, `B030`, `B1000`).

<a id="refusals"></a>
## Refused replies and what is never done

An answer counts only if `answers/<unit>.json` is a regular file, parses as a JSON object, carries `id` equal to the unit id and every required field, declares no unknown field and satisfies every type and bound. `check` reports a unit as `missing` (no file) or `invalid` (with the reasons). `save` applies the same rules to the first balanced JSON object found in a printed reply, and refuses when none is found, when it fails validation (a wrong `id` included), when the unit already has a valid answer and on a run that still needs approval. A refused reply changes nothing on disk.

**A reply is filed, another is refused**

```text
$ npx bmad-plus swarm save buyer-panel-20261019-2ftt B005 --input swarm-inbox/B005.txt
buyer-panel-20261019-2ftt: answer of B005 filed at /work/demo/app/.bmad-plus/swarm/runs/buyer-panel-20261019-2ftt/answers/B005.json
$ npx bmad-plus swarm save buyer-panel-20261019-2ftt B006 --input swarm-inbox/B006.txt
Swarm: Reply of unit B006 refused: field "buys" must be true or false.
# The first file holds a sentence followed by a valid JSON object; the second holds an object whose buys field is the text "maybe". Exit codes 0 and 1.
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-19

<a id="honesty"></a>
## The honesty rules

- Never answer for a unit: not by writing an answer file, not by "fixing" an invalid one, not by filing with `save` something a sub-agent did not print.
- Never edit a brief. A change belongs in the template or the definition, then in a new run.
- The disclosure comes once and first: the tally starts with it, and the assistant repeats it once in its report, before any figure.
- Rates are an upper bound to confirm with real people. Quotes are what a simulated unit wrote.
- The seed travels with the result: same seed, same cards, so a comparison is a comparison.
- A valid answer is a well-formed one. Nothing proves that a sub-agent read its brief, stayed in its card or wrote the truth; quality is your reading of the tally.
- Replies are filed from outside the state tree: `save --input` takes a project file the host wrote (for instance in `swarm-inbox/`), never a path under `.bmad-plus/`.

<a id="worked-panel"></a>
## A worked buyer panel

The scenario: a coffee bar called Orbit wants to know how an urban professional reacts to its pitch. The files below are the test fixtures of the BMAD+ repository, used here unchanged: the definition, the profile and the brief template. The pitch itself is two sentences.

**swarm/buyer-panel.json (the definition)**

```json
{
  "schemaVersion": 1,
  "id": "buyer-panel",
  "kind": "panel",
  "pack": "marketfit",
  "title": "Simulated buyers see the pitch",
  "units": { "source": "profile", "idPrefix": "B" },
  "brief": { "template": "briefs/buyer.md", "context": ["pitch"] },
  "answer": {
    "required": ["id", "buys", "reason_code", "reason"],
    "fields": {
      "buys": { "type": "boolean" },
      "reason_code": { "type": "enum", "values": ["price", "habit", "need", "trust", "other"] },
      "reason": { "type": "string", "max": 600 },
      "price_floor": { "type": "number", "min": 0 },
      "visits_per_week": { "type": "integer", "min": 0, "max": 14 },
      "would_tell": { "type": "list", "maxItems": 3 }
    }
  },
  "tally": {
    "rate": "buys",
    "groupBy": ["segment", "behaviour"],
    "counts": ["reason_code"],
    "quotes": { "reason": 2 },
    "numbers": ["price_floor"]
  },
  "disclosure": "Simulated units are not people. Read rates as an upper bound and confirm with real ones."
}
```

**swarm/profile.json (the profile)**

```json
{
  "schemaVersion": 1,
  "subject": "Orbit espresso bar",
  "target": "Urban professionals who buy coffee most days",
  "dimensions": [
    {
      "name": "segment",
      "values": [
        { "value": "early career", "share": 0.45, "ranges": { "income": [38000, 85000] } },
        { "value": "mid career", "share": 0.4, "ranges": { "income": [85000, 150000] } },
        { "value": "high earner", "share": 0.15, "ranges": { "income": [150000, 210000] } }
      ]
    },
    {
      "name": "behaviour",
      "values": [
        { "value": "daily regular", "share": 0.2, "note": "same drink, same place, most mornings" },
        { "value": "explorer", "share": 0.5 },
        { "value": "occasional", "share": 0.3, "note": "a treat, not a routine" }
      ]
    }
  ],
  "ranges": { "age": [24, 38], "walk_minutes": [1.5, 12] },
  "pools": {
    "objection": ["too steep for a drink", "my place is closer", "I do not know the brand"],
    "habit": ["you pass the street on the way to work", "you order the same thing every day"]
  }
}
```

**swarm/briefs/buyer.md (the template)**

```md
# You are unit {{unit.id}} of run {{run.id}}

You are one simulated buyer. Stay inside this card for the whole task.

- Segment: {{card.segment}}, income {{card.income}}
- Behaviour: {{card.behaviour}} ({{card.behaviour_note}})
- Age {{card.age}}, {{card.walk_minutes}} minutes on foot from the bar
- Your first objection: {{card.objection}}
- Your habit: {{card.habit}}

## The pitch

{{context.pitch}}

## Your answer

Write a JSON object to `{{answer.path}}` with the fields `id` (exactly "{{unit.id}}"), `buys`,
`reason_code` (price, habit, need, trust or other), `reason` (your own words, 600 characters at
most) and, when you have one, `price_floor`.

{{disclosure}}
```

**swarm/pitch.md (the context)**

```text
Orbit opens at 7, two streets from the station. A flat white is 4.20, the loyalty card gives
the tenth one free, and the bar keeps your usual order under your name.
```

Seed 7 and 30 units deal this first card (from `cards.json`). The brief the sub-agent receives is the template with this card, the pitch and the answer path filled in.

**The card of unit B001**

```json
{
  "id": "B001",
  "segment": "mid career",
  "income": 114720,
  "behaviour": "occasional",
  "behaviour_note": "a treat, not a routine",
  "age": 32,
  "walk_minutes": 8.99,
  "objection": "I do not know the brand",
  "habit": "you order the same thing every day"
}
```

In the transcript below, the assistant would launch 30 sub-agents in three waves. To show the files, the tool and the tally without any model, five of the answers were written by hand for this demonstration (four as files, one through `save`), and a sixth reply was refused. So the tally counts 5 of 30 units and names the 25 others. The quotes below are therefore not what a model wrote either: they are demonstration text.

**The demonstration answers (one file each in answers/; B005 and B006 are replies filed with save)**

```text
answers/B001.json    { "id": "B001", "buys": true, "reason_code": "habit", "reason": "The bar keeps my usual order under my name, so I would stop at a tenth coffee for free.", "price_floor": 3.8 }
answers/B002.json    { "id": "B002", "buys": false, "reason_code": "price", "reason": "A flat white at 4.20 is more than my usual place, and it is closer.", "price_floor": 3.2 }
answers/B003.json    { "id": "B003", "buys": true, "reason_code": "need", "reason": "Opening at seven fits my route to the station, and I order the same drink every morning.", "price_floor": 4.5 }
answers/B004.json    { "id": "B004", "buys": false, "reason_code": "trust", "reason": "I do not know the brand and nothing in the pitch tells me why I should switch.", "price_floor": 3.5 }
swarm-inbox/B005.txt Here is my answer as a buyer.
                     { "id": "B005", "buys": true, "reason_code": "habit", "reason": "Same drink, same place, most mornings: the loyalty card would pay for itself.", "price_floor": 4.2 }
swarm-inbox/B006.txt { "id": "B006", "buys": "maybe", "reason_code": "other", "reason": "I could go either way." }
```

**From briefs to tally, with five hand-written demonstration answers**

```text
$ npx bmad-plus swarm briefs buyer-panel-20261019-2ftt
buyer-panel-20261019-2ftt: 30 of 30 units need a sub-agent, in waves of 10.
Wave 1:
  B001  /work/demo/app/.bmad-plus/swarm/runs/buyer-panel-20261019-2ftt/briefs/B001.md
  B002  /work/demo/app/.bmad-plus/swarm/runs/buyer-panel-20261019-2ftt/briefs/B002.md
# ... units B003 to B010, then waves 2 and 3 (B011 to B030) ...
Each sub-agent: Read the brief and follow it exactly; it is your whole instruction.
Disclosure: Simulated units are not people. Read rates as an upper bound and confirm with real ones.
# Four answer files (B001 to B004) are now written by hand into answers/.
$ npx bmad-plus swarm check buyer-panel-20261019-2ftt
buyer-panel-20261019-2ftt: 4 of 30 valid answers.
  B005  missing: no answer file
  B006  missing: no answer file
# ... and so on up to B030. Exit code 1.
$ npx bmad-plus swarm tally buyer-panel-20261019-2ftt
/work/demo/app/.bmad-plus/swarm/runs/buyer-panel-20261019-2ftt/tally.md
5 of 30 units counted; 25 skipped.
Disclosure: Simulated units are not people. Read rates as an upper bound and confirm with real ones.
$ npx bmad-plus swarm inspect buyer-panel-20261019-2ftt
buyer-panel-20261019-2ftt: tallied (panel, approved by the user)
units 30, briefed 30, valid 5, invalid 0, missing 25
Disclosure: Simulated units are not people. Read rates as an upper bound and confirm with real ones.
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-19

**tally.md (opening and the rate section)**

```md
> Simulated units are not people. Read rates as an upper bound and confirm with real ones.

# Simulated buyers see the pitch (run buyer-panel-20261019-2ftt)

5 of 30 units gave a valid answer; 25 skipped: B006 (no answer file), B007 (no answer file), ...

## Rate: buys

| group | yes | of | rate |
|---|---|---|---|
| all | 3 | 5 | 60.0% |
| segment = mid career | 2 | 4 | 50.0% |
| segment = high earner | 1 | 1 | 100.0% |
| segment = early career | 0 | 0 | n/a |
| behaviour = occasional | 1 | 3 | 33.3% |
| behaviour = daily regular | 2 | 2 | 100.0% |
| behaviour = explorer | 0 | 0 | n/a |
```

How to read it: the rate comes with its denominator ("3 of 5", and 5 of 30 units counted), not as a percentage alone. A group with no valid answer shows `n/a`, not 0. The same run also writes counts per reason code, the first two quotes per group verbatim, and the median, mean, minimum and maximum of `price_floor`. Re-run `init` with the same profile, `--n 30` and `--seed 7` against a second pitch to compare: the file `cards.json` of the two runs is byte for byte identical (`cmp` reports no difference between them).

<a id="recipe-transcript"></a>
## The acceptance steps, replayed: 1 valid answer out of 30

The acceptance recipe of 0.23.0 includes a swarm step in an empty project, carried out by a person: no sub-agent runs, the tester acts as one unit, files a valid answer for B001 and tries a bad one. The tool counts exactly that, and says so. Below is the same sequence replayed with the 0.23.0 CLI (the 30 paths of `briefs` are shortened).

**briefs, check, save, tally with a single valid answer**

```text
$ npx bmad-plus swarm init --definition swarm/buyer-panel.json --profile swarm/profile.json --context pitch=swarm/pitch.md --n 30 --seed 7 --approved
buyer-panel-20261019-9da2: panel of 30 units, budget 26, approval user.
Seed 7 (same seed, same cards).
Next: bmad-plus swarm briefs buyer-panel-20261019-9da2
$ npx bmad-plus swarm briefs buyer-panel-20261019-9da2
buyer-panel-20261019-9da2: 30 of 30 units need a sub-agent, in waves of 10.
Wave 1:
# ... one line per unit with the brief path, three waves ...
Each sub-agent: Read the brief and follow it exactly; it is your whole instruction.
Disclosure: Simulated units are not people. Read rates as an upper bound and confirm with real ones.
# The tester writes one valid answer by hand: answers/B001.json.
$ npx bmad-plus swarm check buyer-panel-20261019-9da2
buyer-panel-20261019-9da2: 1 of 30 valid answers.
  B002  missing: no answer file
# ... B003 to B030 likewise.
$ npx bmad-plus swarm save buyer-panel-20261019-9da2 B002 --input swarm/reply-bad.txt
Swarm: Reply of unit B002 refused: field "buys" must be true or false.
# swarm/reply-bad.txt holds an object whose buys field is the text "maybe". Exit code 1.
$ npx bmad-plus swarm tally buyer-panel-20261019-9da2
/work/demo/app/.bmad-plus/swarm/runs/buyer-panel-20261019-9da2/tally.md
1 of 30 units counted; 29 skipped.
Disclosure: Simulated units are not people. Read rates as an upper bound and confirm with real ones.
$ head -n 3 .bmad-plus/swarm/runs/buyer-panel-20261019-9da2/tally.md
> Simulated units are not people. Read rates as an upper bound and confirm with real ones.

# Simulated buyers see the pitch (run buyer-panel-20261019-9da2)
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-19

<a id="jury"></a>
## A jury: three lenses on the same pitch

A jury needs no profile, no `--n` and no seed: its units are listed in the definition, one lens each. This one (`swarm/pitch-jury.json`, printed below with its template) reads the same pitch through price, trust and habit. The third juror carries its own `id` (`J-habit`); the first two get `J001` and `J002` from the prefix. Its answer has only three fields: a verdict, the weakest claim and up to two fixes, and its tally counts verdicts and keeps the first three quotes.

**swarm/pitch-jury.json (the definition)**

```json
{
  "schemaVersion": 1,
  "id": "pitch-jury",
  "kind": "jury",
  "title": "Three lenses on the pitch",
  "units": {
    "source": "list",
    "idPrefix": "J",
    "list": [
      { "lens": "price", "stance": "Every claim is a cost the buyer pays." },
      { "lens": "trust", "stance": "Who vouches for this, and why believe them?" },
      { "id": "J-habit", "lens": "habit", "stance": "What has to change in a normal morning?" }
    ]
  },
  "brief": { "template": "briefs/juror.md", "context": ["pitch"] },
  "answer": {
    "required": ["id", "verdict", "weakest_claim"],
    "fields": {
      "verdict": { "type": "enum", "values": ["convincing", "unclear", "weak"] },
      "weakest_claim": { "type": "string", "min": 5, "max": 300 },
      "fixes": { "type": "list", "maxItems": 2 }
    }
  },
  "tally": { "counts": ["verdict"], "quotes": { "weakest_claim": 3 } },
  "disclosure": "A simulated jury is a rehearsal, not a review by the people it imitates."
}
```

**swarm/briefs/juror.md (the template)**

```md
# Juror {{unit.id}}

Lens: {{card.lens}}. Stance: {{card.stance}}

Read the pitch below through that lens only and write a JSON object to `{{answer.path}}` with
`id` ("{{unit.id}}"), `verdict` (convincing, unclear or weak), `weakest_claim` and optional `fixes`.

{{context.pitch}}

{{disclosure}}
```

**The demonstration answers (answers/J001.json, answers/J002.json, answers/J-habit.json)**

```text
{ "id": "J001", "verdict": "unclear", "weakest_claim": "A flat white at 4.20 is stated, but nothing says what it is compared to.", "fixes": ["Compare the price with a nearby chain.", "Show what the loyalty card is worth in euros."] }
{ "id": "J002", "verdict": "weak", "weakest_claim": "No one vouches for the bar: there is no review, no name, no photo.", "fixes": ["Add two customer sentences with a first name."] }
{ "id": "J-habit", "verdict": "convincing", "weakest_claim": "Opening at seven is good; it does not say how long the queue is.", "fixes": ["Say how long a regular order takes."] }
```

**A jury of three, with three hand-written demonstration answers**

```text
$ npx bmad-plus swarm init --definition swarm/pitch-jury.json --context pitch=swarm/pitch.md
pitch-jury-20261019-3qxm: jury of 3 units, budget 26, approval not-needed.
Units listed, no seed.
Next: bmad-plus swarm briefs pitch-jury-20261019-3qxm
$ npx bmad-plus swarm briefs pitch-jury-20261019-3qxm
pitch-jury-20261019-3qxm: 3 of 3 units need a sub-agent, in waves of 10.
Each sub-agent: Read the brief and follow it exactly; it is your whole instruction.
Disclosure: A simulated jury is a rehearsal, not a review by the people it imitates.
# Three answer files are written by hand into answers/.
$ npx bmad-plus swarm check pitch-jury-20261019-3qxm
pitch-jury-20261019-3qxm: 3 of 3 valid answers.
$ npx bmad-plus swarm tally pitch-jury-20261019-3qxm
3 of 3 units counted; 0 skipped.
Disclosure: A simulated jury is a rehearsal, not a review by the people it imitates.
```

Output captured from a real run. Source: bmad-plus 0.23.0, 2026-10-19

**tally.md of the jury**

```md
> A simulated jury is a rehearsal, not a review by the people it imitates.

# Three lenses on the pitch (run pitch-jury-20261019-3qxm)

3 of 3 units gave a valid answer; 0 skipped.

## Counts: verdict

| group | convincing | unclear | weak |
|---|---|---|---|
| all | 1 | 1 | 1 |

## Quotes: weakest_claim (up to 3 per group, verbatim)

**all**
- J001: "A flat white at 4.20 is stated, but nothing says what it is compared to."
- J002: "No one vouches for the bar: there is no review, no name, no photo."
- J-habit: "Opening at seven is good; it does not say how long the queue is."
```

<a id="use-cases"></a>
## Panel and jury use cases

**Which kind answers which question**

| Question | Kind | How to run it |
| --- | --- | --- |
| Does the new pitch beat the old one with the same people? | `panel` | Same profile, `--n` and `--seed`; change only `--context pitch=...`; compare the two tallies. |
| Which segment objects to the price? | `panel` | Put the segment in `groupBy`, the objection in an enum field and read `counts` per group. |
| What is the weakest claim of this page? | `jury` | One lens per juror (price, trust, accessibility, legal); read the quotes per lens. |
| Does the plan survive a hostile reading? | `jury` | Give each juror a stance such as "assume this fails; where?" and tally the verdicts. |
| Can twelve pages be written without stepping on each other? | `fanout` | One unit per page with a disjoint `scope`; run as session sub-agents, or export with `swarm plan` for Nexus. |

<a id="example-swarm-panel-before-after"></a>
### Compare two pitches with the same simulated buyers

Use when you have a profile and a definition for a panel, and two versions of the text to test. Expect to be asked for approval if the panel is larger than 26.

**In your AI assistant**

```text
Run the buyer-panel swarm with 30 units and seed 7 on swarm/pitch-v1.md, then again with the same seed and profile on swarm/pitch-v2.md. If a run needs approval, ask me first with the numbers. Launch the sub-agents one wave at a time and never answer for a unit. Report from the two tallies only: the disclosure first, then the buy rate by segment for each pitch with its denominator, the reason codes, and what changed between the two.
```

**What you should get** The first `init` stops with exit code 2 and the line "needs approval ... 30 sub-agents and the budget is 26". After your yes, three waves of 10 run, `check` lists any missing unit, the tally files are written, and the report quotes the disclosure before any rate. Because the two runs share profile, `--n` and seed, their `cards.json` are identical and the difference comes from the pitch alone. Read the result as an upper bound to confirm with real people.

<a id="example-swarm-jury-read"></a>
### Have a jury attack a page before launch

Use with a jury definition (one lens per juror) and the text to read as a context file. No profile and no seed are needed.

**In your AI assistant**

```text
Run the pitch-jury swarm on swarm/pitch.md. One sub-agent per juror, each reading its own brief only. Read each context file before passing it. When the tally is written, report the disclosure first, then the verdict counts and the weakest claim quoted per lens, and list the fixes the jurors proposed. Do not write or edit any answer yourself.
```

**What you should get** Three briefs are rendered, three sub-agents answer, `check` reports 3 of 3 valid answers and `tally.md` opens with "A simulated jury is a rehearsal, not a review by the people it imitates." It then shows how many jurors found the pitch convincing, unclear or weak, and quotes each weakest claim with the juror’s id.

<a id="example-swarm-fanout-pages"></a>
### Write several pages in parallel and verify them

Use when units edit the repository, need a check each, or must survive the session: export the fanout as a Nexus plan.

**In your AI assistant**

```text
Initialise the site-pages fanout from swarm/page-fanout.json and swarm/units.json (about, pricing, contact), export it with swarm plan to .bmad-plus/swarm/reports/RUN/plan.json and create the Nexus run from it. Start one task at a time on this host, let me review the page, then verify and accept it. Do not accept a task whose check fails.
```

**What you should get** `swarm plan` reports "3 tasks" and writes a plan whose tasks carry the disjoint scopes; `nexus create` shows each task as "not started; verification=pending; acceptance=pending". For each task: `start`, the page is written, `record`, then `verify` runs the unit’s check and `accept` marks it accepted. A task whose answer file is invalid stays unaccepted.

<a id="run-tree-and-limits"></a>
## Run tree and limits

**.bmad-plus/swarm/runs/<definition-id>-<yyyymmdd>-<4 chars>/**

```text
run.json         kind, pack, hashes of every input, n, seed, wave, budget, approval, status
definition.json  the normalized definition, frozen
template.md      the brief template, frozen
cards.json       the dealt or listed units
context/<name>.md  each context file, frozen
briefs/<unit>.md   rendered by briefs or plan
answers/<unit>.json  written by the sub-agent, or filed by save
tally.json, tally.md
checks/answer.cjs, checks/swarm-validate.cjs, nexus-plan.json   (fanout, written by plan)
```

**Limits**

| Bound | Value |
| --- | --- |
| Units per run | 1 to 1000 |
| `budget`, `wave` | 1 to 1000 (defaults 26 and 10) |
| Seed | 0 to 2147483647 |
| Input files (definition, profile, units, template, contexts, answers) | At most 16 MiB each; a reply given to `save` at most 1 MiB |
| Profile | 10 dimensions of up to 50 values, 20 ranges, 20 pools of up to 200 entries |
| Answer fields | 50; enum values 50; list items up to 200; string length ceiling 20000 |
| `groupBy` fields; quotes per group | 5; 1 to 10 |
| Fanout scopes per unit | 20 |
| Export to Nexus | One task per unit, `maxParallel` at most 32 (Nexus accepts 200 tasks per run) |

Safety boundaries: the runtime has no child process, no network and no `eval`; every file is written atomically and only under `.bmad-plus/swarm/`; paths that traverse, are absolute, follow a link or reach `.git` are refused; inputs are frozen at `init`, so editing a context afterwards changes nothing for that run.

**See also**

- [Reference: bmad-plus swarm](https://bmad-plus.rochetta.fr/docs/reference/cli-swarm/)
- [Tutorial: from an idea to a swarm](https://bmad-plus.rochetta.fr/docs/guides/idea-to-swarm/)
- [Intake: ask first](https://bmad-plus.rochetta.fr/docs/concepts/intake/)
- [Nexus, autopilot and parallel work](https://bmad-plus.rochetta.fr/docs/concepts/nexus-orchestration/)
- [Reference: config.yaml](https://bmad-plus.rochetta.fr/docs/reference/config-yaml/)

[BMAD+ documentation](https://bmad-plus.rochetta.fr/docs/) · [Concepts](https://bmad-plus.rochetta.fr/docs/concepts/)

[Source on GitHub](https://github.com/lrochetta/BMAD-PLUS)
