Skip to content

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"
  level process-modelling

  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
    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
    event "Payment accepted" @5
    hotspot "Nobody agrees whether a refused payment cancels the order" @5
  }

  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 eventstorm block. 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 one lane, 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.
The board is a grid: lanes down, time across
A lane is a horizontal swimlane — a department, an actor, a subsystem — and @column on 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 @3 where 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 @column and keep the order they appear in — the only record of a stacking order there is.
@column may 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
level big-picture, level process-modelling or level software-design. Big picture has six colours, context among them — finding the seams is the last phase of a big picture, not a software-design activity. Process modelling adds command, policy and readmodel; software design adds aggregate and ui — the component, and the screen a person decides on, which completes the human path read 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 declared, not inferred
It is a statement of intent: a session that has decided it is modelling a process decided that before placing its first command. Inferring it from the cards would mean the level changed under the room as somebody added one, and a facilitator could never set it up in advance. Omitting it means big-picture, which is where the practice starts — and is why files written before this setting existed still open.
A card the level does not admit is an error
Not a silent promotion. Quietly deepening the level because somebody wrote one command would change what the file claims about itself without anybody deciding to. The message names the level that *would* admit the card, so the fix is one word and the error says which. The board enforces the same rule from the other side: it will not let the level be lowered past the notes already on the wall.
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 | Level | Lane | Card | Note } , '}' ] ;
Product    = 'product'  , String ;   (* at most one *)
Level      = 'level'    , ( 'big-picture' | 'process-modelling'
                          | 'software-design' ) ;   (* at most one; default big-picture *)
Lane       = 'lane'     , String , [ '{' , { Card | Note } , '}' ] ;
Card       = Kind , String , [ Column ] , [ '{' , { Note } , '}' ] ;
Column     = '@' , Integer ;   (* one-based; defaults to the next square *)
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 *)