Swimlane diagrams
A swimlane diagram shows a process and who does each part of it: a lane for each actor or system, steps in the lane that performs them, decisions, and the hand-offs between lanes. Use it for a business process, an integration between systems, an approval, or an incident runbook in a design document. It is for anyone, human or AI agent, who needs the answer to "who does what, and in what order".
TL;DR
| Question | Answer |
|---|---|
| What is it for? | A process with actors: who performs each step, where the process branches, and where it passes from one actor to another. Flow can approximate it with groups, but a swimlane keeps every step in its lane. |
| How does it start? | swimlane "Title", then lane id "Name" { with the steps that lane performs inside it, then a -> b : "label" for each transition. |
| What goes in a lane? | step id "Label", decision id "Question?", start id "Label", and end id "Label". A transition may cross lanes freely, and that crossing is the hand-off. |
| Which way does it read? | direction=right draws the lanes as bands with time running left to right, direction=down draws them as columns with time running down, and auto, the default, keeps the bands unless they would be wider than a Markdown column. |
| How do I make it readable? | Name each lane for an actor, put an icon on each step, color the outcomes (green for success, red for failure, amber for a warning), and label every branch of a decision with its condition. See Make it readable. |
| What else can it hold? | Dashed lines for a reply, dotted lines for an event, steps with a detail line, a transition that returns to an earlier step, and numbered badges with numbered=on. |
| Can I convert Mermaid? | Not yet. Mermaid has no swimlane diagram, so there is nothing to read. See From Mermaid for what to do instead. |
| How big may it be? | 12 lanes, 80 steps, and 160 transitions. Past a limit the diagram is refused with error E140, before anything is drawn. |
| How is it drawn? | Each lane is a rounded band with its icon and name in a rail, each step is a card, each decision is a tinted pill, and each start or end is a small pill. Layout is automatic, and the theme chooses every color. |
Ask your agent
Say who the actors are and what each one does, in order, then ask for an icon on every step and a color for each outcome. The agent writes the rest.
Use Plotwork to draw how an online order is fulfilled as a swimlane diagram with four lanes: Customer, Web app, Payments, and Warehouse. The customer places an order, the web app checks stock, and if the item is sold out the order is cancelled. Otherwise Payments charges the card. A declined card shows an error and the customer updates the card, and then Payments tries again. Once paid, the warehouse packs and ships the order, and the customer sees it delivered. Give every step an icon, and color the cancelled order red and the delivered order green.Use Plotwork to draw an OAuth sign-in as a swimlane diagram with four lanes: Browser, App, Identity provider, and API. The browser opens the app, which redirects it to the identity provider. The user approves access, the provider sends back an authorization code, and the app exchanges it for a token. If the token is valid, the API returns the profile and the browser shows that the user is signed in. Otherwise access is denied. Use a dashed line for the reply from the API.Use Plotwork to draw our incident response as a swimlane diagram with three lanes: On-call, Support, and Engineering. An alert wakes the on-call engineer, who triages it. If customers are affected, Support notifies them and posts updates, and pages an engineer. Engineering ships the fix and writes the review, and the incident closes when both the updates and the review are done.Use Plotwork to draw a nightly data pipeline as a swimlane diagram with the lanes Scheduler, Queue, and Worker. The scheduler exports the orders and puts a batch on the queue, and the worker transforms the rows. A valid batch is loaded into the warehouse, which acknowledges the scheduler, and an invalid one goes to a dead letter queue. Show the hand-offs through the queue as events, and the acknowledgement as a reply.Example
This is the order fulfilment from the first prompt. It shows four lanes, a decision whose branches carry a condition, a transition that returns to an earlier step (the card is tried again), a step with a detail line, and green and red ends. The diagram is narrow enough to read in a Markdown column, so the lanes are drawn as columns.
swimlane "Order fulfilment" desc="A customer's order is checked against stock, paid for, packed and shipped, or cancelled."
lane customer "Customer" icon=lucide:user { start placed "Order placed" icon=lucide:receipt end cancelled "Order cancelled" tone=red icon=status:error step card "Update card" sub="Enters a new card" icon=lucide:credit-card end delivered "Delivered" tone=green icon=status:success}lane web "Web app" icon=lucide:globe { step check "Check stock" icon=lucide:boxes decision stock "In stock?" step error "Show error" sub="Payment declined" tone=amber icon=lucide:triangle-alert}lane pay "Payments" icon=lucide:wallet { step charge "Charge card" icon=lucide:credit-card}lane warehouse "Warehouse" icon=lucide:warehouse { step pack "Pack order" icon=lucide:package-check step ship "Ship order" icon=lucide:send}
placed -> checkcheck -> stockstock -> charge : "[in stock]"stock -> cancelled : "[sold out]"charge -> pack : "[paid]"charge -> error : "[declined]"error -> cardcard -> charge : "retry"pack -> shipship -> delivered : "arrived"Lanes:
- Customer
- Order placed, start
- Order cancelled, end
- Update card (Enters a new card), step
- Delivered, end
- Web app
- Check stock, step
- In stock?, decision
- Show error (Payment declined), step
- Payments
- Charge card, step
- Warehouse
- Pack order, step
- Ship order, step
Transitions:
- Order placed to Check stock
- Check stock to In stock?
- In stock? to Charge card, if in stock
- In stock? to Order cancelled, if sold out
- Charge card to Pack order, if paid
- Charge card to Show error, if declined
- Show error to Update card
- Update card to Charge card on retry
- Pack order to Ship order
- Ship order to Delivered on arrived
Make it readable
A swimlane reads fastest when the lanes say who acts, the cards say what they do, and the colors say how it went. Four habits do that, and an agent can write all of them in one line per step, as the order fulfilment above does.
| Habit | How | Example |
|---|---|---|
| A lane for each actor | Name a lane for a person, a team, or a system, never for a stage. A process has stages, and the columns of the picture already show them. | lane warehouse "Warehouse" icon=lucide:warehouse { |
| One icon on every step | icon= says what the step is, so a card is clear at a glance. A status: icon has a tone of its own, such as status:success in green and status:error in red, and a simple: icon is the mark of a tool. | step charge "Charge card" icon=lucide:credit-card |
| Color shows how it went | tone=green for a good outcome, tone=amber for a warning, and tone=red for a failure. A line that arrives at a step or an end of one of those tones takes a softer line of the same color, so the way to a good outcome and the way to a bad one part by color. | end cancelled "Order cancelled" tone=red |
| A condition on every branch | Write the condition in square brackets, so the picture draws it as a tag. A decision with a branch that says nothing is warning W201. | stock -> charge : "[in stock]" |
Keep a step name to a few words and move detail into sub=. Text wider than a card or a label is cut with warning W203. Do not rely on color alone: the name and the icon say the same thing, so the diagram still reads for someone who cannot tell two tones apart.
Sign in through an identity provider
A sign-in crosses four lanes, and every transition between two of them is a hand-off. The reply from the API is dashed, and the invalid token ends in a red step.
swimlane "OAuth login" desc="A browser signs in through an identity provider, and the app calls the API with the token it gets back."
lane browser "Browser" icon=lucide:monitor { start open "Open the app" icon=lucide:circle-play step approve "Approve access" sub="Signs in at the provider" icon=lucide:user-check end signed "Signed in" tone=green icon=status:success}lane app "App" icon=lucide:server { step redirect "Redirect to provider" icon=lucide:send step exchange "Exchange the code" sub="Sends the code and secret" icon=lucide:key-round decision valid "Token valid?" end denied "Access denied" tone=red icon=status:error}lane idp "Identity provider" icon=lucide:shield-check { step login "Show the login page" icon=lucide:lock-keyhole step issue "Issue the tokens" icon=lucide:file-key}lane api "API" icon=lucide:cloud { step profile "Return the profile" icon=lucide:circle-user}
open -> redirectredirect -> login : "authorize request"login -> approve : "login page"approve -> issue : "approves"issue -> exchange : "authorization code"exchange -> validvalid -> profile : "[valid]"valid -> denied : "[invalid]"profile --> signed : "profile"Lanes:
- Browser
- Open the app, start
- Approve access (Signs in at the provider), step
- Signed in, end
- App
- Redirect to provider, step
- Exchange the code (Sends the code and secret), step
- Token valid?, decision
- Access denied, end
- Identity provider
- Show the login page, step
- Issue the tokens, step
- API
- Return the profile, step
Transitions:
- Open the app to Redirect to provider
- Redirect to provider to Show the login page on authorize request
- Show the login page to Approve access on login page
- Approve access to Issue the tokens on approves
- Issue the tokens to Exchange the code on authorization code
- Exchange the code to Token valid?
- Token valid? to Return the profile, if valid
- Token valid? to Access denied, if invalid
- Return the profile to Signed in (dashed) on profile
An incident response
An incident response branches on one question and then runs two lanes side by side. The decision's two ways out leave from different sides of the pill, and the incident closes only when both the updates and the review have arrived.
swimlane "Incident response" desc="An alert wakes the on-call engineer, who triages it, brings in support and engineering, and closes the incident after a review."
lane oncall "On-call" icon=lucide:siren { start alert "Alert fires" icon=lucide:bell-ring step triage "Triage" sub="Severity and scope" icon=lucide:search-check decision impact "Customers affected?" end closed "Incident closed" tone=green icon=status:success}lane support "Support" icon=lucide:users { step notify "Notify customers" icon=lucide:megaphone step updates "Post status updates" icon=lucide:message-circle}lane eng "Engineering" icon=lucide:code { step fix "Ship the fix" icon=lucide:bug-off step review "Write the review" tone=violet icon=lucide:book-open-check}
alert -> triagetriage -> impactimpact -> notify : "[yes]"impact -> fix : "[no]"notify -> updatesnotify -> fix : "page engineer"updates -> closedfix -> reviewreview -> closedLanes:
- On-call
- Alert fires, start
- Triage (Severity and scope), step
- Customers affected?, decision
- Incident closed, end
- Support
- Notify customers, step
- Post status updates, step
- Engineering
- Ship the fix, step
- Write the review, step
Transitions:
- Alert fires to Triage
- Triage to Customers affected?
- Customers affected? to Notify customers, if yes
- Customers affected? to Ship the fix, if no
- Notify customers to Post status updates
- Notify customers to Ship the fix on page engineer
- Post status updates to Incident closed
- Ship the fix to Write the review
- Write the review to Incident closed
A data pipeline with asynchronous hand-offs
A pipeline passes work through a queue, so the hand-offs are events. This one reads as bands, because direction=right is written in the header and the process is short enough to fit. A dotted line is an event, a dashed line is a reply, and the dead letter is red, so the line that reaches it is red too.
swimlane "Nightly order export" direction=right desc="A scheduler starts the export, a queue carries each batch, and a worker loads it into the warehouse, with a dead letter for a bad batch."
lane scheduler "Scheduler" icon=lucide:timer { step export "Export orders" sub="Nightly at 02:00" icon=lucide:cloud-upload}lane queue "Queue" tone=orange icon=lucide:layers { step enqueue "Enqueue batch" icon=lucide:send step dead "Dead letter" tone=red icon=lucide:triangle-alert}lane worker "Worker" icon=lucide:server-cog { step transform "Transform rows" icon=lucide:workflow step load "Load the batch" tone=green icon=lucide:database}
export ..> enqueue : "batch ready"enqueue ..> transform : "batch event"transform -> load : "[valid]"transform ..> dead : "[invalid]"load --> export : "ack"Lanes:
- Scheduler
- Export orders (Nightly at 02:00), step
- Queue
- Enqueue batch, step
- Dead letter, step
- Worker
- Transform rows, step
- Load the batch, step
Transitions:
- Export orders to Enqueue batch (dotted) on batch ready
- Enqueue batch to Transform rows (dotted) on batch event
- Transform rows to Load the batch, if valid
- Transform rows to Dead letter (dotted), if invalid
- Load the batch to Export orders (dashed) on ack
The language
A line is one statement. A # starts a comment. Ids are words such as check, in-stock or _x, labels are in double quotes, and options are written key=value. The first line is the header.
The header
swimlane "Order fulfilment" direction=right numbered=on desc="How an order moves from the shop to the customer."| Part | Meaning |
|---|---|
swimlane | The kind. |
"Title" | The title. It is the accessible name of the picture and heads the text description. Optional. |
direction= | auto (the default), right, or down. See Which way it reads. |
numbered=on | Draws a numbered badge on each transition, in the order written. See Numbered transitions. |
start= | The number of the first badge. It needs numbered=on. |
desc= | A plain-language description for the text description. Optional. A desc "text" statement does the same. |
Statements
| Statement | Meaning | Example |
|---|---|---|
lane <id> "Name" tone=<tone> icon=<set:name> {, the steps inside, then } | A lane: the actor or system that performs the steps written inside it. Lanes stack in the order written. The } is on a line of its own. | lane customer "Customer" icon=lucide:user { |
step <id> "Label" sub="Detail" tone=<tone> icon=<set:name> | A step, drawn as a card. sub= is the line under the name, and a \n in it starts a new line. A tone other than neutral tints the card. | step charge "Charge card" sub="Payments service" icon=lucide:credit-card |
decision <id> "Question?" tone=<tone> icon=<set:name> | A decision, drawn as a tinted pill with a branch icon. It has one way in and a transition out for each answer. The default tone is amber, icon= swaps the icon, and icon=none drops the chip. | decision stock "In stock?" |
start <id> "Label" tone=<tone> icon=<set:name> | Where the process begins, drawn as a small pill. | start placed "Order placed" icon=lucide:receipt |
end <id> "Label" tone=<tone> icon=<set:name> | Where the process finishes, drawn as a small pill. A process may have several ends, such as delivered and cancelled. | end delivered "Delivered" tone=green icon=status:success |
<from> -> <to> : "event [guard] / action" step=<n> | A transition between two steps, which may be in different lanes. The label and its step= are optional. | stock -> charge : "[in stock]" |
<from> --> <to> and <from> ..> <to> | A reply or an asynchronous hand-off (dashed), and an event (dotted). They take a label like any transition. | ship ..> track : "shipped event" |
desc "text" | The description of the diagram. | desc "How an order moves." |
tone= takes neutral, blue, violet, aqua, orange, green, amber, or red, and icon= takes a name from the closed list of icons, such as lucide:user, status:success or simple:github. A diagram cannot set a color: the theme chooses every color, in light and dark.
Lanes
A step belongs to the lane it is written in, and a step outside every lane is an error. The lanes stack in the order written, and the first is the top lane, or the left one. A lane draws its icon and its name in a rail at its start, and a tone= on the lane gives its rail and its band a color. The bands alternate in shade, so neighbours part without a heavy line.
lane warehouse "Warehouse" tone=orange icon=lucide:warehouse { step pack "Pack order" icon=lucide:package-check step ship "Ship order" icon=lucide:send}A lane is for an actor, so a lane that holds no step is warning W201. Ids are shared by lanes and steps, so a lane and a step cannot have the same id.
Transitions and their labels
A transition is one line, from -> to, and a label after a colon. The three arrows say what the line means.
| Written | Drawn | Use it for |
|---|---|---|
a -> b | A solid line | The process moves on from a to b. |
a --> b | A dashed line | A reply, or a hand-off the process does not wait for. |
a ..> b | A dotted line | An event, such as a message on a queue or a webhook. |
The label of a transition reads event [guard] / action, as in a state diagram. The picture draws the event as plain text, the guard as a tinted tag with a branch icon, and the action after a bolt icon, and the text description says "on the event, if the guard, then the action".
| Written | Event | Guard | Action |
|---|---|---|---|
payment received | payment received | none | none |
[in stock] | none | in stock | none |
pay [card valid] / charge card | pay | card valid | charge card |
/ notify support | none | none | notify support |
Use a guard on every branch out of a decision, because the condition then reads as one. A transition may return to an earlier step, as card -> charge : "retry" does above, and it may join two steps in one lane. It may not join a step to itself, because a loop is drawn through a second step. Text wider than 200 px is cut with an ellipsis and warning W203; the full text stays in the text description.
Which way it reads
direction= | The lanes are | Time runs | Use it when |
|---|---|---|---|
right | Bands, stacked top to bottom | Left to right | The process is short, or the page is wide. |
down | Columns, side by side | Top to bottom | The process has many steps in a row. |
auto | Bands, or columns when the bands would be wider than 1100 px | As the lanes | You do not mind. This is the default. |
A process of more than five steps in a row is wider than a Markdown column as bands and narrower as columns, so auto turns it into columns. That is warning W202 when you ask for right yourself.
Numbered transitions
Add numbered=on to the header and every transition gets a numbered badge, in the order written, so people can say "step 4" aloud. A transition with no label shows the badge alone on its line, and one with a label carries it in the left end of its pill. Write step=off on a transition to leave its badge out, or step=12 to give it the number 12, and the next continues from 13.
swimlane "Checkout" numbered=on
lane shopper "Shopper" icon=lucide:user { step cart "Fill the cart" icon=lucide:check}lane shop "Shop" icon=lucide:globe { step total "Compute the total" icon=lucide:receipt}
cart -> total : "go to checkout"total --> cart : "show the total"Limits and warnings
Warnings never stop a diagram from being drawn. Errors do.
| Limit | Value | What happens past it |
|---|---|---|
| Lanes | 12 | E140 |
| Steps (every start, decision, and end counts as one) | 80 | E140 |
| Transitions | 160 | E140 |
| Lanes before the density warning | 8 | W200 |
| Steps before the density warning | 40 | W200 |
| Width of a label | 200 px | Cut with an ellipsis, and W203 |
The limits are checked from the text alone, so a diagram that is too big is refused at once with a hint on how to split it. Layout is plain arithmetic over a grid, so even a diagram at the limits is laid out in a few milliseconds.
| Code | Severity | When | Fix |
|---|---|---|---|
E100 | error | A step outside a lane, a lane inside a lane, a lane with no block, a missing id or label, an arrow a transition cannot have (<-> and --), a transition from a step to itself, or a diagram with no lane. | Each says what to write. |
E140 | error | The diagram is over a limit in the table above. | Split the diagram. |
E100, E101, E110, E121 to E123, E130, E131, E133 to E136 | error | The shared codes: a syntax error, an unterminated string, an unknown statement, an unknown tone, icon, or direction (left and up are not read), a duplicate id, an unknown step, an unclosed or unmatched block, an unknown option, or a bad option value. | Each says what to write, with the nearest name for a mistyped one. |
W200 | warning | There are more than 8 lanes or 40 steps. | Split the process into one diagram for each phase, or merge the lanes of actors that work as one. |
W201 | warning | A lane holds no step, a step no transition touches, a step no path from a start reaches, a decision has fewer than two ways out, or a branch of a decision has no label. | Connect it, label it, or remove it. |
W202 | warning | The diagram is wider than a column would keep readable. | Use direction=down, shorten the labels, or split it. |
W203 | warning | Text was cut with an ellipsis. | Shorten the text, or move the detail into sub=. |
W201 covers every loose end: the message says which one it is. A step is unreachable only when the diagram has a start, because a diagram with none has no place to count from.
From Mermaid
Mermaid's swimlane syntax is not read, because Mermaid has none. People draw lanes in Mermaid with a flowchart whose subgraphs are the actors, and convert_mermaid reads that as a flow diagram with groups, which keeps the nodes and the lines but not the idea that a lane is a row of a process.
To get a swimlane from a Mermaid flowchart, convert it first, then ask an agent to rewrite the converted flow diagram: each group becomes a lane, each node a step or a decision, each terminal a start or an end, and each edge a transition. Keep the labels, and move the condition of a decision's branch into a guard in square brackets. A Mermaid sequenceDiagram is a different picture, a conversation over time, and converts to a sequence diagram.
| Mermaid | Plotwork |
|---|---|
subgraph Customer and its nodes | lane customer "Customer" { and its steps |
A[Place order] | step a "Place order" |
B{In stock?} | decision b "In stock?" |
C([Done]) | end c "Done" |
A --> B | a -> b |
B -->|yes| C | b -> c : "[yes]" |
A -.-> B | a --> b for a reply, or a ..> b for an event |
See also
| Doc | Why |
|---|---|
| Writing diagrams | The flow, sequence, and schema kinds, and the options every kind shares |
| Examples to copy | Finished diagrams with the prompt that gets an agent to write each one |
| Migrating from Mermaid | The converter, its diagnostic codes, and what carries over |
| State diagrams | The states and transitions whose labels, cards, and line colors a swimlane shares |
| User journeys | The steps of a person, scored by how they feel |
| Icon reference | Every icon name you can put in icon= |
| Language reference | Every word of the language, one page for each topic |
| Diagram kinds | The kinds, and what each one draws |