A BMAD workflow, end to end: shipping a feature the spec-driven way
If the only way you've ever worked with an AI is to vibe code with it, which is to say you open the editor, type a prompt, and keep whatever looks roughly right, then this is the post I wish someone had handed me back when I was doing exactly that. It isn't another argument about why spec-driven development is better, because I already made that case here; it's simply one complete pass through a real workflow, with the actual commands, so that you can see for yourself what the whole thing looks like from the inside. Together we'll build one small feature with BMAD, taking it all the way from a vague idea to reviewed code, and although the example is deliberately thin it is also complete, running the full sequence of brainstorm → spec → architecture → story → implementation → review without waving our hands over any of the parts that usually get skipped, so that by the end you can see exactly where the friction lives and what it buys you in return. If you've never met the BMAD agents before, the previous post introduces the crew, and while you don't strictly need it in order to follow along here, it does help to know that "John" is a product manager and "Amelia" is a developer rather than people I actually work with. The example is a URL shortener where links can expire. All the artifacts you'll see below are illustrative snippets from that walkthrough. The feature (and the trap) Here's the feature: a URL shortener where links can expire. Now watch how a vibe-coding session usually starts, because you'd open a chat and type something like this: Build me a URL shortener in [your stack]. Links should be able to expire. That prompt feels complete, but it really isn't, because it's a specification riddled with holes, and the AI is going to quietly fill every one of those holes with a guess that you never actually got to see it make, so it's worth counting them: When does a link expire? After a fixed TTL? At an absolute timestamp you pass in? After N clicks? "Expire" is three different features. What happens when someone hits an expired link? A 404? A 410 Gone? A redirect to a "this link expired" page? These are observably different behaviors, and a caller integrating with you will build against whichever one you pick. Do we count clicks? If expiry can be click-based, you need a counter. If it can't, you probably still want analytics. That's a storage decision hiding inside a one-line prompt. What's the ID scheme? Random short codes? Collisions? Custom aliases? Vibe coding doesn't make any of these decisions go away, it simply arranges for someone else to make them silently and invisibly at generation time, and then it hands you back a pile of code that you now have to reverse-engineer in order to discover what was actually decided on your behalf. So when the "expired link returns 404" bug report lands on your desk three weeks later, you'll find yourself debugging a decision that nobody ever consciously made in the first place. Spec-driven development, at its core, is really just this: you make the decisions before you write the code, on purpose, and in a place where you can actually see them, and BMAD is simply one way of running that process with the AI working inside the loop with you rather than against you. Let's run it. The mental model, in 30 seconds Two things to hold in your head before we start: BMAD is a team of agents rather than a single assistant. Each one is a persona with a specific job to do, so there's an analyst who interrogates the idea, a product manager who writes the requirements, an architect who makes the technical calls and a developer who implements them, and you move between these personas deliberately as the work progresses. In the commands that follow, every phase can be run either by summoning the persona directly with /bmad-agent-* or by invoking its underlying workflow with /bmad-*, and you should feel free to use whichever of the two you prefer. The real output of the early phases is not code at all, it's artifacts. These are things like a brief, a PRD, an architecture document and a story file, all of them plain markdown that lives right there in your repository, and the code itself only arrives last, sitting downstream of a set of documents that you can read, review and version like anything else. Broadly speaking there are two stages to all of this, a planning stage in which you work out what to build and how to build it, followed by an implementation stage in which you actually build it, and the whole point of the arrangement is that planning leaves behind durable, reviewable artifacts which then carry all of their context forward into implementation, so that the AI writing your code is working from a genuine spec rather than from a vibe. Everything drawn with a dotted line there is a file that ends up in your repository, and that is precisely the part of the process that vibe coding never produces. Phase 1, Brainstorm: drag the hidden decisions into the light We start with Mary, the analyst, and a brainstorming session: /bmad-agent-analyst # then pick the brainstorming capability # or go straight to it: /bmad-brainstorming The move here is genuinely counterintuitive if you're accustomed to prompting an AI for output, because you're not asking it for an answer at all, you're asking it to interrogate you, and Mary's entire job in this session is to ask you the questions that you quietly skipped over. This is exactly the moment where those four hidden decisions from the trap section earlier stop being invisible and start being things you have to answer for. A brainstorm on "URL shortener with expiring links" should surface, at minimum: which kinds of expiry we actually want (TTL vs absolute date vs click-count), the expired-link behavior as a deliberate choice, whether click tracking is a feature or just a mechanism, and the ID/alias scheme. The artifact that comes out of this phase is a brief, and this really is the part worth staring at for a moment, because it doesn't merely list the things we intend to build; it also records the things we have deliberately decided not to build, so that those decisions leave a paper trail behind them instead of quietly evaporating. Here's the heart of an example brief.md: ## Decisions (made on purpose, for v1) - **Expiry model: absolute timestamp.** A link carries an optional `expires_at` (UTC instant). No `expires_at` means the link never expires. - **Expiry is inclusive.** At exactly `expires_at`, the link is already expired. - **Behavior on an expired link: `410 Gone`,** with body `{ "reason": "expired" }`. Not a `404`, not a `302` to a landing page. - **ID scheme: 7-character base62 random code,** with collision-retry. ## Explicitly out of scope for v1 (deferred, not forgotten) - Relative / TTL expiry ("expire 24h after creation"). - Click-count expiry, click tracking / analytics. - Custom aliases, custom "this link expired" landing pages. Take a good look at that "out of scope" list, because every single line in it is a decision that a vibe-coding prompt would have made for you, silently and without asking, whereas here every one of them is on the record, deferred deliberately rather than forgotten by accident. If you want a more formal artifact than a brainstorm dump: /bmad-product-brief Notice what has already happened here, because we haven't yet written a single line of code and we have already caught three or four decisions that vibe coding would otherwise have made for us essentially at random. Phase 2, PRD: turn decisions into requirements you can check Now we switch to John, the product manager, to produce a Product Requirements Document. /bmad-agent-pm # or the workflow directly: /bmad-prd John's job is to take something like "we decided that links expire by absolute timestamp and that expired links return 410 Gone" and convert it into requirements that are precise enough that you could hand them to a complete stranger and expect the right thing to come back, and this is the point at which the fuzzy brainstorm finally hardens into something resembling a contract. Here's an example FR-3 from prd.md: ### FR-3: Accessing an expired link - **Given** a short link whose `expires_at` is at or before now - **When** a client requests `GET /{code}` - **Then** the service responds `410 Gone` - **And** the response body includes a machine-readable `reason: "expired"` - **And** no redirect (`3xx`) is issued Non-goals for v1: custom "expired" landing pages, per-link grace periods. Read that through and notice what has changed, because the 410 decision is now written down, reviewable and testable, which means that a month from now the question "why does it return 410?" has an actual answer with a paper trail attached to it, and that really is the whole difference between a decision and an accident. If you want the requirements distilled into a tighter, machine-oriented contract for the downstream agents, BMAD has a dedicated step: /bmad-spec Phase 3, Architecture: the technical calls, made once Winston, the architect, takes the what that we've established so far and decides the how, and for a feature this small the right instinct is to keep the whole thing thin, because you really don't need a forty-page architecture document for a URL shortener, you only need the handful of decisions that would be genuinely expensive to get wrong: /bmad-agent-architect # or: /bmad-architecture The decisions worth pinning down here: Storage: how do we persist code → target_url + expires_at? Key-value store, relational table, in-memory for a toy? Expiry enforcement: lazy (check expires_at on read) or active (a sweeper that deletes expired rows)? ID generation: how we mint short codes and handle collisions. The lazy-versus-active call is the one with real teeth in it, and here is how architecture.md records that call, capturing both the decision and its consequence: ### Expiry enforcement: lazy (checked on read) - **Chosen: lazy.** Simplest correct behavior; no scheduler, no clock daemon. - **Consequence, on purpose:** an expired link's record still _exists_ in the store until something evicts it. `resolve` must therefore never assume "present in store" means "live". It must evaluate expiry every time. - Rejected for v1: active sweeping. Adds a scheduler we don't need yet. Writing down the consequence, namely that "present in store" does not mean "live", is the part that pays off later on, because it's the exact invariant that the implementation is going to have to honor, and it has been stated in plain language before a single line of resolve even exists. The output of all this is an architecture.md sitting in your repository, reviewable and versioned, the kind of thing a teammate can simply read instead of having to reverse-engineer it back out of the code. Phase 4, The story: packaging context for the implementer This is the phase that vibe coders have quite simply never seen, and it also happens to be the one that makes the whole method actually work. /bmad-create-epics-and-stories # break the PRD into epics + stories /bmad-create-story # generate the next story with full context A story is a single, self-contained unit of work, but the genuinely important part is what's actually in it, because a BMAD story file is nothing like a Jira ticket that carries a title and a single sentence of description. It is instead a complete context package, gathering together the relevant requirements, the architectural decisions that happen to apply, the acceptance criteria and pointers to the exact files that are in play, and it is written in such a way that an agent, or for that matter a human, can pick it up completely cold and still have everything they need to get started. Here's an example story-1.3: # Story 1.3 — Return 410 for expired links ## Context Implements FR-3 (see prd.md). Expiry is enforced lazily on read, and the boundary instant is inclusive (now >= expires_at ⇒ expired) — both are architecture decisions. `now` is injected into resolve() so expiry stays deterministic in tests. ## Acceptance criteria - [ ] resolve of a link whose expires_at
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to