doc-hub · example mapping
The .examplemap format
An example map is a text file. It belongs in the repository of the product it describes, next to the feature file it produces. The technique itself is written up in the development hub.
An example
// Example map exported by doc-em.
// Comments and blank lines in an imported file are not preserved: the board
// is the source, this file is a render of it.
examplemap "Redeem a voucher" {
product "client-onboarding"
space "CLONB"
delivery "Sprint 24" sprint #CLONB-S24 points 13
delivery "Sprint 25" sprint #CLONB-S25 points 8
delivery "2026.9" release #CLONB-R9
story "Redeem a voucher" #CLONB-42 ~analysing @"2026.9" {
as "Returning customer"
want "to apply a voucher code at checkout"
so "I pay the price I was promised"
question "Which currencies can a voucher be issued in?"
}
rule "A voucher must not be expired" {
example "A voucher that expired yesterday is refused" @"Sprint 24" {
given "a voucher SUMMER10 that expired on 2026-08-21"
given "a basket of 40 CHF"
when "the voucher is applied"
then "the voucher is refused"
then "the basket total is still 40 CHF"
}
example "A voucher expiring today is accepted" @"Sprint 25"
question "Is expiry checked when it is applied, or when the basket is paid?"
}
rule "A voucher applies once per basket" {
example "Applying the same voucher twice leaves one discount" @"2026.9"
}
rule "A voucher cannot take a basket below zero" {
note "The finance team asked for this in writing. Do not\
change it without them."
example "A 50 CHF voucher on a 30 CHF basket leaves a total of 0.00 CHF" @"Sprint 24" {
given "a basket of 30 CHF"
when "a 50 CHF voucher is applied"
then "the basket total is 0.00 CHF"
}
example "The remaining 20 CHF is not carried to the next order"
}
rule "One voucher per basket" {
question "Does that include the automatic loyalty voucher?"
}
}
It is deliberately a map you would not estimate yet: two rules carry questions, and"One voucher per basket" has no examples at all.
What the format decides for you
- One story, always
- The practice is defined as taking one story, so a map has exactly one. A second
storyline is an error rather than a list: two stories on one map is two sessions, and merging them would hide that. A map with no story line at all is fine — it is a session that has not named its story yet, and the board shows "To be defined". - The story states its need
as/want/so— the formal story language, modelled in three fields rather than written as prose in a note. The title says what to build and the need says why anyone should; thesoclause is the half that gets dropped first and missed most, and a session spends its whole length interrogating that sentence. All three are optional and independently so — a room that has agreed the persona but not the outcome is an ordinary state.- The persona is free text here, and a reference in doc-sm
- There, a story may only name a persona its own activity lists, so the board offers exactly those and a name from outside the cast is refused. There is no cast on this board: example mapping takes one story that some other conversation already chose, and a map that had to declare its personas before naming one would be inventing structure the technique does not have. So the card edits all three clauses as text, with no dropdown to offer choices that do not exist.
- The story carries the ticket, and it is the only thing that does
- A
#idand a~statusmay follow the story’s title, in either order:story "Redeem a voucher" #CLONB-42 ~ready. Rules, examples and questions take neither. Breaking a story down does not produce more tickets — that is the difference between this board and the story map next door, where every row is a level in the tracker. - The id is set here, and only here
- The board shows the ticket id and will not let you retype it. An example mapping session refines a story; it does not re-address one, and a mistyped id silently points a whole map of rules at somebody else’s ticket. Changing it is an edit to this file, where it is deliberate and shows up in a diff. The
~statusbeside it is the opposite: that is what a session changes, so it is one click on the card menu as well as a word here. - A status is a cache, never the truth
- The ticketing system owns it.
~openis what an unlinked story reads as — a placeholder meaning nothing has been said yet, not a claim — and it is omitted on export for exactly that reason. A linked story always writes its status, because there the value is an answer rather than the absence of one. productis a shortname;spacedefaults to it- The product is the
slugdoc-registry assigns, not the display name — a map that recorded "Client Onboarding" would stop matching its product the day somebody fixed the capitalisation.spaceis the ticketing container, a Jira project key or whatever your tracker calls one; leave it out and the product shortname stands in. They come apart often enough to be separate: a key ofCLONBagainst a product ofclient-onboardingis ordinary. - Declaration order is timeline order
- A
deliveryline adds a band:delivery "Sprint 24" sprint,delivery "2026.9" release. The order they are written is the order they happen, and there is no date and no index — a date is the one thing here that would go stale on its own, and an index is a second copy of what the list already says. Re-order the plan by moving a band, not by editing a number. - A band has a ticket too
- A sprint is a real object in the tracker and so is a release — one has a number, the other a version, and both are things you can open. So a delivery carries
#idafter its kind:delivery "Sprint 24" sprint #CLONB-S24. Like the story’s, it is set here and shown read-only on the board, because a mistyped id silently re-points a whole sprint’s worth of examples with no symptom on screen. A band takes no~status: where a sprint is in its own lifecycle is the tracker’s business, and caching it would be claiming to know something doc-em never asks. - Only a sprint is sized
points 13after a sprint’s kind is its estimate:delivery "Sprint 24" sprint #CLONB-S24 points 13. A release takes none, and writing one is an error rather than a value quietly dropped — a release is delivered by the sprints before it, so sizing it would either double-count them or state a competing number for the same work. Switching a sized sprint to a release on the board clears the estimate for the same reason.- Points are the sprint’s, not the cards’
- A whole number, and the board edits it — unlike the
#idbeside it, an estimate is decided in the room rather than issued by the tracker. Empty is not zero:0says the sprint carries no estimable work, and leaving it out says nobody has sized it. Examples are never given points; the practice does not ask anyone to, and a number on every green card would invite a different meeting than the one this board is for. - A sprint is a kind of delivery, not a different thing
- Both kinds are the same structure and behave identically; the word is for reading. Four sprints and a release says something five equal bands do not — that four of them are steps towards the fifth.
- The story ships in a release; examples ship in sprints
@places a card on the timeline:story "…" @"2026.9"andexample "…" @"Sprint 24". An example is the smallest thing on the board with business value attached, which is why the time axis crosses the rules rather than ordering them — a rule is a constraint, and a constraint is not delivered in a sprint. The concrete cases that satisfy it are, one at a time.- Unscheduled is the absence of a
@ - No sentinel to spell wrong. An example with no
@sits below the line: agreed, and not committed to. That is where most examples are born, and where they return if you delete the band they were in — deleting a sprint never deletes the work planned into it. - A duplicate band title is an error; a late example is only a warning
- Two deliveries with the same title is refused, because
@names a band by its title and a duplicate makes the reference meaningless. But an example scheduled *after* the story ships still parses — the board says so in its readings instead. You move the release first and the examples after, and a parser that refused that intermediate state would make replanning impossible in the tool that exists to plan. - An example belongs to a rule
- Examples are written under the rule they illustrate, and cannot float. A rule with no examples is legal and is the practice’s own warning sign: nobody has agreed what that rule means yet.
- Steps repeat;
Anddoes not exist - An example may carry
given,whenandthenlines, and any of them may be written more than once — that is how a scenario accumulates context. There is noandkeyword, becauseAndis how a repeat is *printed*: the secondgivenin a row renders asAndon the card and in the feature file. Storing it would make a line mean something different depending on the line above it. - Steps come back in Gherkin’s order
- Given establishes context, When is the one action, Then is what must hold afterwards. Write them in any order you like; export puts them back in that one, because any other order is not a scenario. It costs nothing to normalise, since the three are separate fields with no order between them to lose.
- An example may be a title alone
- Steps are optional. A session that produced ten example titles and no steps did example mapping correctly — the steps get written by whoever makes a card precise, which is often later and often not in the room.
- A question hangs on the story or on a rule
- A doubt raised before any rule exists belongs to the story; one raised while discussing a rule sits with that rule. Both are meaningful when you read the finished map — "this rule has three unanswered questions" says something different from "the board has three".
- Braces, not indentation
- Whitespace is a formatting choice and never syntax, so a file that has been through a chat window or a different editor still parses.
- Notes wrap at 50 characters
- A note is prose. A trailing backslash carries its string onto the next line, and that split is the break — one pair of quotes for the whole note.
- Comments do not survive the board
- A // comment is read and discarded. Import a commented file, export it again, and the comments are gone: the board is the source, the file is a render of it.
The grammar
File = ExampleMap , EOF ;
ExampleMap = 'examplemap' , String ,
[ '{' , { Product | Space | Delivery | Story | Rule | Note } , '}' ] ;
Product = 'product' , String ; (* at most one *)
Space = 'space' , String ; (* at most one *)
Delivery = 'delivery' , String , ( 'sprint' | 'release' ) ,
{ Ticket | Points } ,
[ '{' , { Note } , '}' ] ; (* order is timeline order *)
Points = 'points' , Integer ; (* sprints only; at most one *)
Story = 'story' , String , { Ticket | Status | Ships } ,
[ '{' , { As | Want | So | Question | Note } , '}' ] ; (* exactly one *)
As = 'as' , String ; (* who the story is for; at most one *)
Want = 'want' , String ; (* what they want; at most one *)
So = 'so' , String ; (* the outcome; at most one *)
Ticket = '#' , ( Ident | String ) ; (* at most one *)
Status = '~' , ( 'open' | 'analysing' | 'ready'
| 'in-progress' | 'done' | 'closed' ) ; (* at most one *)
Ships = '@' , ( Ident | String ) ; (* names a Delivery *)
Rule = 'rule' , String , [ '{' , { Example | Question | Note } , '}' ] ;
Example = 'example' , String , [ Ships ] , [ '{' , { Step | Note } , '}' ] ;
Step = ( 'given' | 'when' | 'then' ) , String ; (* each repeatable *)
Question = 'question' , String , [ '{' , { Note } , '}' ] ;
Note = 'note' , String ;
String = '"' , { Char | Escape | Splice } , '"' ;
Escape = '\\' , ( '"' | '\\' | 'n' | 't' ) ;
Splice = '\\' , newline , { space } ; (* carries the string on; is a break *)
Comment = '//' , { Char } ; (* discarded *)And the Gherkin it writes
Three of the four colours have a Gherkin keyword of their own — Feature:,Rule:, Scenario: — and the fourth has none. An open question is not a specification, so the feature file cannot carry the red cards and the board says so before it writes one. That is why the .examplemap file is the one that round-trips.
An example's steps are written out verbatim, with the second of a clause rendered asAnd. Nothing is guessed: an example that is still a title alone produces a scenario with no steps, and a comment saying so — because a scenario with no steps parses, runs, and passes.
Rule: A voucher must not be expired
Scenario: A voucher that expired yesterday is refused
Given a voucher SUMMER10 that expired on 2026-08-21
And a basket of 40 CHF
When the voucher is applied
Then the voucher is refused
And the basket total is still 40 CHF
Scenario: A voucher expiring today is accepted
# No steps yet — this scenario would pass without asserting anything.