Browse the documentation

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.

On this page
  1. When to use a swarm
  2. The flow, end to end
  3. The budget, the waves and the approval
  4. The four files, field by field
  5. Deterministic dealing and seeds
  6. Refused replies and what is never done
  7. The honesty rules
  8. A worked buyer panel
  9. The acceptance steps, replayed: 1 valid answer out of 30
  10. A jury: three lenses on the same pitch
  11. Panel and jury use cases
  12. Run tree and limits

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.

Ask first, then swarm 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.
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

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.

When to use a swarm

The three kinds of swarm
KindUnits come fromTypical useWhat the tally reports
panelA 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.
juryA 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.
fanoutA 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.

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.

The flow, end to end

Files and commands
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.

    In your project terminal
    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.

    In your project terminal
    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.

    In your project terminal
    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.

    In your project terminal
    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.

    In your project terminal
    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.

    In your project terminal
    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.

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
KeyDefaultMeaning
budget26Sub-agents one run may use before the assistant must ask you. Integer from 1 to 1000.
wave10Sub-agents launched together. The assistant waits for a wave before the next. Integer from 1 to 1000.
media_creditsaskPaid 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
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

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.

1. The definition (JSON)

Definition fields
FieldRule
schemaVersionMust be 1.
id1 to 64 lowercase letters, digits or -. Opens the run id.
kind and units.sourceThe pair must match: panel with profile, jury with list, fanout with input.
units.idPrefix1 to 4 letters. Required for a panel; optional for a jury whose entries have no id; absent for a fanout.
units.listJury only: 1 to 1000 entries, each with a lens text and any other text, number or boolean fields; an optional id.
packOptional pack id, recorded in the run.
titleHeading of the tally.
brief.templatePlain relative path to a Markdown file, no . or .. part.
brief.contextNames of the context files the moderator must pass at init (--context NAME=FILE). Each is required; at most 20.
answer.requiredFields that must be present. id is implicit and always required.
answer.fieldsAt most 50 fields, each typed (see the next table). A sub-agent may not write a field that is not declared.
tallyrate (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).
disclosure1 to 500 characters, printed first in every tally and in the briefs output.
Answer field types
TypeOptionsAccepted value
booleannonetrue or false.
integermin, maxA whole number within the bounds.
numbermin, maxAny number within the bounds.
stringmin, max (length)Text; default 0 to 4000 characters, ceiling 20000; min is checked on the trimmed text.
enumvalues (1 to 50 distinct)One of the listed values.
listmaxItems (default 50, ceiling 200)A list of non-empty texts, each up to 1000 characters.

2. The profile (panel only)

Profile fields
FieldRule
subject, targetWhat is being judged and who judges it. Documentation for readers of the file.
dimensions1 to 10 dimensions, each with a name and 1 to 50 values. The shares of one dimension add up to 1.
values[].value, shareThe label of a value and its share of the panel.
values[].rangesOptional numeric ranges sampled when that value is dealt, such as "income": [38000, 85000]. Every value of a dimension declares the same range names.
values[].noteOptional 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.
poolsNamed 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.

3. The units file (fanout only)

swarm/units.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)
{
  "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)
# 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}}

4. The brief template (Markdown) and the contexts

Placeholders: nothing else is allowed between double braces
PlaceholderRenders
{{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.

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).

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
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

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 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)
{
  "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)
{
  "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)
# 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)
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
{
  "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)
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
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)
> 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).

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
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 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)
{
  "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)
# 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)
{ "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
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
> 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."

Panel and jury use cases

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

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
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.

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
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.

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
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.

Run tree and limits

.bmad-plus/swarm/runs/<definition-id>-<yyyymmdd>-<4 chars>/
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
BoundValue
Units per run1 to 1000
budget, wave1 to 1000 (defaults 26 and 10)
Seed0 to 2147483647
Input files (definition, profile, units, template, contexts, answers)At most 16 MiB each; a reply given to save at most 1 MiB
Profile10 dimensions of up to 50 values, 20 ranges, 20 pools of up to 200 entries
Answer fields50; enum values 50; list items up to 200; string length ceiling 20000
groupBy fields; quotes per group5; 1 to 10
Fanout scopes per unit20
Export to NexusOne 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