doc-hub · event storming
The .eventstorm format
Plain text, and the file the board reads and writes. The technique itself is written up in the development hub.
An example
// Event storm exported by doc-es.
// Comments and blank lines in an imported file are not preserved: the board
// is the source, this file is a render of it.
eventstorm "Ordering a pizza" {
product "client-onboarding"
lane "Customer" {
actor "Hungry customer" @1
event "Menu opened" @1
event "Pizza added to the basket" @2
event "Basket emptied and started again" @2 {
note "Happens more than anybody expected. Worth\
understanding before it is designed away."
}
event "Order placed" @3 +revenue
event "Pizza delivered" @8
}
lane "Payments" {
command "Take the payment" @3
event "Payment requested" @4
system "Payment provider" @4
policy "Whenever a payment is refused, hold the order" @5
event "Payment refused" @5 +revenue
event "Payment accepted" @5
hotspot "Nobody agrees whether a refused payment cancels the order" @5 +"ask payments" +revenue
}
lane "Kitchen" {
readmodel "Orders waiting" @6
event "Order sent to the kitchen" @6
actor "Kitchen staff" @6
event "Pizza put in the oven" @6
opportunity "Tell the customer when it goes in the oven" @6
event "Pizza handed to the driver" @7
}
}
A process model: three lanes over one timeline, notes stacked at column 5, a gap in the customer’s lane while the others are busy, and a hotspot nobody has resolved. The payment lane carries the level’s whole point — a command, the event it causes, and the policy that reacts to it.
What the format decides for you
- One storm, and one wall
- A file holds one
eventstormblock. A second is an error rather than a merge: two walls in one file is almost always a bad paste. A storm always has at least onelane, even when nobody has named one — the practice starts with paper on a wall, and the board needs a row to put the first note on. - The keyword is the colour
- A card is written with the keyword of its kind —
event,actor,system,hotspot,opportunity— and there is no separate colour or type annotation. A card whose keyword said one thing and whose annotation said another would be a state the file could express and the board could not. - Any note takes any number of
+tags - A tag is a free label, written after the title:
hotspot "Nobody agrees whether a refused payment cancels the order" @5 +"ask payments" +revenue. Use quotes when the label has spaces in it. Every kind takes them, and takes as many as you like — the keyword says what a note *is*, which is one answer per card, where a tag says something that is also true of it. - There is no list of permitted tags
- Nothing here validates a tag beyond the shape of the word, and
+legelis therefore a tag rather than an error. A fixed vocabulary would have to be chosen once, here, for every room that will ever run a storm — and the useful tags are exactly the ones nobody could have guessed: the team who has to be asked, the system nobody owns any more, the workshop day it came from. The wall pays the bargain back by drawing the tags in use, so a misspelling shows up as a chip that appears once beside one that appears eleven times. - Tags are not coloured, and never will be
- On this board colour is *everything*. The ten kinds are Brandolini’s ten colours, they were never ours to choose, and somebody who has stood at one of these walls has to recognise this one without being told. A second colour system laid over that would not merely compete with the notation — it would destroy it. So a tag is a small word in a plain outline that borrows the note’s own ink, and staying quiet is the point of it.
- The board is a grid: lanes down, time across
- A
laneis a horizontal swimlane — a department, an actor, a subsystem — and@columnon a card says where along the timeline it sits. Column 4 is the same moment in every lane, which is what lets two notes side by side mean *simultaneous*, and what lets a lane have a visible gap where its neighbour is busy. - A column is a coordinate, not a list position
- Which is why cards carry
@3where the other two boards refuse index fields. There, a card’s position *was* its place in a list, so an index would have been a second copy of the same fact. Here the board is two-dimensional and the column is the only record of when a note happens. Lane order is still a list, and still has no index. - Several notes may share one square
- A moment often turns out to involve an actor, a system and an event at once, so notes stack at one point of one lane. They are written with the same
@columnand keep the order they appear in — the only record of a stacking order there is. @columnmay be left out, and is always written back- A card with no
@takes the square after the last one written in its lane, so a run of events typed straight down needs no numbering. Export always writes the number out: the coordinate is the fact, and leaving it implicit would make a file’s meaning depend on the order of the lines around it. - The board is meant to feel endless
- There is always one empty column past the rightmost note and a control to add a lane under the last. Reaching the end creates the next square. It is not literally unbounded — the grid draws what is used plus one, with a floor so a fresh board looks like a board — because genuine infinity means a virtualised grid, and that would cost the thing that makes a wall readable: seeing all of it at once.
- Cards may be written before any lane
- Chaotic exploration produces a heap of events long before anybody agrees which lane they belong to. So a card is legal at the top level, and every such card is gathered into one unnamed lane on import. Export then writes that lane out explicitly — the one place this format normalises rather than renders.
- Three levels, and the notation is cumulative
- Big picture, process modelling, software design. Big picture has six colours,
contextamong them — finding the seams is the last phase of a big picture, not a software-design activity. Process modelling addscommand,policyandreadmodel; software design addsaggregateandui— the component, and the screen a person decides on, which completes the human pathread model → screen → command. Each level keeps everything the shallower ones have — a process model still has domain events and hotspots on it, it has *more*, never different. - The level is discovered, not declared
- There is no
levelline. A wall with acommandon it is a process model and a wall with anaggregateon it is a software design — the cards already say which workshop this is, so a line saying it too could only ever agree with them, and a duplicate is the half that drifts. The board reads the level off the notes. Alevelline written by an older version is accepted and discarded, and disappears the next time the board writes the file. - The level is a lens the reader holds
- Because the notation is cumulative, a wall carrying commands and policies still contains a perfectly good big picture — so the board offers all three levels whatever is on it, and choosing a shallower one dims the notes it does not cover with the same dimming the tag filter uses. It also decides what you can place: the
+palette and the re-colour menu offer exactly the notation the legend is showing. None of it touches the text. A storm modelled to software design can be shown to a room as a big picture and handed back with the file byte-identical. - 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 = EventStorm , EOF ;
EventStorm = 'eventstorm' , String ,
[ '{' , { Product | Lane | Card | Note } , '}' ] ;
Product = 'product' , String ; (* at most one *)
Lane = 'lane' , String , [ '{' , { Card | Note } , '}' ] ;
Card = Kind , String , { Column | Tag } , [ '{' , { Note } , '}' ] ;
Column = '@' , Integer ; (* one-based; defaults to the next square *)
Tag = '+' , ( Ident | String ) ; (* any number; any kind of note *)
Kind = 'event' | 'actor' | 'system' | 'hotspot' | 'opportunity'
| 'context' (* big picture *)
| 'command' | 'policy' | 'readmodel' (* process modelling *)
| 'aggregate' | 'ui' ; (* software design *)
Note = 'note' , String ;
String = '"' , { Char | Escape | Splice } , '"' ;
Escape = '\\' , ( '"' | '\\' | 'n' | 't' ) ;
Splice = '\\' , newline , { space } ; (* carries the string on; is a break *)
Comment = '//' , { Char } ; (* discarded *)