Flowcharts and swimlanes
There is no flowchart block. A flowchart is a diagram holding four shapes and a list of connections, and this chapter is about the two things that make it worth drawing in WCL rather than in a drawing program: the :layered layout, which places the shapes for you from the connection graph, and the swim-lane, which is a technique rather than a block kind.
Every diagram below is rendered by this page from the source printed above it. Type them into a document of your own and compare.
§ 1The four shapes
Four stdlib block kinds draw the figures a flowchart is made of. They take the same fields; only the figure differs.
| Kind | Figure | Default size | What it means |
|---|---|---|---|
| process | Rectangle | 80 × 40 | A step — something happens |
| decision | Diamond | 80 × 40 | A branch — a question with named answers |
| terminator | Oval | 80 × 40 | A start or an end of the flow |
| node | Circle | 56 × 56 | A vertex in a graph, not a step in a flow |
Each takes its text as an inline label, so the shape reads as the thing it draws:
diagram {
width = 560 height = 110
desc = "The four flowchart shapes: terminator, process, decision, node."
terminator "Start" { x = 20.0 y = 33.0 width = 110.0 height = 44.0 }
process "Reserve" { x = 160.0 y = 33.0 width = 110.0 height = 44.0 }
decision "In stock?" { x = 300.0 y = 23.0 width = 110.0 height = 64.0 }
node "queue" { x = 442.0 y = 27.0 width = 56.0 height = 56.0 }
}
node is the odd one out, and it is in the same file for a reason. A circle is not a flowchart figure — it is a graph vertex, and it is what you reach for when the picture has no direction: a dependency web, a network, anything you would lay out with :force or :radial rather than in ranks. It shares the flowchart fields so it can sit in the same diagram, and its box is square by default because a solver allocating a cell for a circle should allocate a square one.
§ 1.1The fields they share
All four are independent @block types extending SvgBlock. They are not variants of one base type, and none of them extends node — the shared field list is written out four times in the stdlib rather than inherited. What matters to you is that the list is the same one:
| Field | Type | What it does |
|---|---|---|
| the inline label | utf8 | The centred text. Wraps and shrinks to fit the figure. |
| id | identifier? | The name an edge connects to. A shape with no id can never be an endpoint. |
| x / y | f64 (default 0) | Top-left corner. Omit under an auto-layout — see the warning below. |
| width / height | f64 | The figure's box. A layout solver reserves exactly this. |
| fill / stroke | utf8? | Explicit colours, which opt the shape out of theming. |
| class | list<utf8>? | Style classes — see Themes and styling. |
| connect_points | list<AnchorSide>? | Which sides (:north / :east / :south / :west) edges may attach to. Default: all four. |
| icon | utf8? | An icon badge inside the figure — see Icons. icon_size, icon_pos and icon_class tune it. |
| link | utf8? | Makes the figure a link to an in-site page. An unknown page fails the build. |
Leave fill unset
An unstyled shape — no fill, no stroke, no class — gets a theme class named after its kind (wdoc-process, wdoc-decision, wdoc-terminator, wdoc-node), which is why the four figures above have different coloured outlines with no colour anywhere in the source. Those classes follow the site's light and dark mode. Set fill and you have opted out of that for good: the colour you wrote is the colour every reader gets, in both modes and in the PDF. Reach for class instead when you want a shape to stand out.
§ 2Wiring the flow
Shapes are connected with the document-level connection statement, source -> destination :kind, written inside the diagram. Both ends name a shape's id. The kind comes from the EdgeKind symbol set. Every kind draws the same arrow; two of the five add a word to it:
| Kind | Draws |
|---|---|
| :default | An arrow. |
| :flow | An arrow. |
| :data | An arrow. |
| :yes | An arrow labelled yes at its midpoint. |
| :no | An arrow labelled no at its midpoint. |
The three that draw the same thing are not interchangeable to a stylesheet: whichever kind you write is stamped on the rendered path as data-kind, so :data can be dashed and :flow solid without either one changing what the model says. What those three lack is the word, not an identity of their own.
That is the whole labelling story for a -> statement, and it is a short one: the grammar carries a kind and nothing else, so a decision names its answers through the kind. :yes and :no exist because those two answers are the ones a flowchart asks for over and over. An edge that needs any other word has to come from a computed edges list — Connections and routing covers that path.
An endpoint that names nothing is dropped
Edge is declared @dynamic, because a diagram's endpoints may name ids that a repeater generated rather than ids you typed. The price of that is that a misspelled endpoint is not an error: the edge is dropped with a build warning and the diagram renders without it. If an arrow you wrote is missing, check the spelling of both ids before you suspect the router.
§ 3Layered auto-layout
Set layout = :layered and stop writing coordinates. The solver ranks the shapes topologically against the diagram's own connection graph, stacks the ranks along one axis, and spreads each rank along the other. Here is a complete document — save it, build it, and you have a flowchart:
import <wdoc.wcl>
site orders {
title = "Orders"
toc { chapter "Fulfilment" { page = fulfilment } }
}
page fulfilment {
title = "Fulfilment"
h1 "Order fulfilment"
diagram {
width = 360 height = 400
layout = :layered
desc = "A five-step order fulfilment flow, ranked top to bottom."
terminator "Order placed" { id = placed width = 140.0 height = 44.0 }
process "Reserve stock" { id = reserve width = 140.0 height = 44.0 }
process "Pick and pack" { id = pick width = 140.0 height = 44.0 }
process "Hand to courier" { id = ship width = 140.0 height = 44.0 }
terminator "Delivered" { id = done width = 140.0 height = 44.0 }
placed -> reserve :flow
reserve -> pick :flow
pick -> ship :flow
ship -> done :flow
}
}
$ wcl wdoc build fulfilment.wcl --out _site
wrote 1 page
Not one coordinate. The five shapes are in source order here only because that reads well — reorder them and the picture is identical, because the ranks come from the edges.
§ 3.1How a rank is assigned
The solver is a Kahn-style topological sort with longest-path ranking. Four rules cover everything it does:
- A shape's rank is one past its deepest predecessor. Where two paths reach the same shape, the longer one wins, so a shape never sits above something that flows into it.
- A shape nothing flows into starts at rank 0. That includes a shape with no id at all: it cannot be an edge endpoint, so it joins the first rank in source order.
- A cycle is not an error. Shapes the sort could not reach — everything in a cycle, and everything downstream of one — go together on a single rank below the deepest ranked shape, in source order. The picture stays readable; it just stops claiming an order it cannot know.
- A label, a line and a boundary are not flow nodes. They are annotations: the solver skips them, gives them the whole content box to place themselves against, and lets them contribute nothing to it. Every other child, including a plain rect, is ranked.
Ranks stack along the primary axis, which direction chooses. Within a rank, shapes sit in source order, and the rank as a whole is centred against the widest rank in the diagram — which is what keeps a fan-out symmetric.
| Field | Default | What it controls |
|---|---|---|
| direction | :top_to_bottom | Which axis the ranks stack along. :left_to_right is the other one. |
| layer_gap | 40.0 | Space between one rank and the next. |
| node_gap | 40.0 | Space between two shapes within a rank. |
| width / height (on the shape) | 80 × 40 | The cell the solver reserves. Multi-line text grows it. |
x and y are not ignored under a layout
The solver positions each shape by wrapping it in a transform, and the shape then draws itself at its own x / y inside that wrapper. The two add up. A left-over x = 200.0 on a :layered shape neither overrides the layout nor raises an error — it silently shifts that one figure 200 units off its rank, arrows and all. When you switch a hand-placed diagram to :layered, delete the coordinates.
§ 3.2Branching
A decision earns its diamond when its outgoing edges are :yes and :no. This one runs left to right, which gives the two answers a rank of their own to sit in:
diagram {
width = 640 height = 200
layout = :layered
direction = :left_to_right
desc = "A fulfilment flow branching at an in-stock decision."
terminator "Order placed" { id = received width = 120.0 height = 44.0 }
process "Reserve stock" { id = checked width = 120.0 height = 44.0 }
decision "In stock?" { id = instock width = 120.0 height = 70.0 }
process "Pick and pack" { id = packed width = 120.0 height = 44.0 }
process "Back-order" { id = backordered width = 120.0 height = 44.0 }
terminator "Delivered" { id = delivered width = 120.0 height = 44.0 }
received -> checked :flow
checked -> instock :flow
instock -> packed :yes
instock -> backordered :no
packed -> delivered :flow
backordered -> delivered :flow
}
packed and backordered share one rank, and delivered sits past both of them rather than beside backordered, because a rank is one past the deepest predecessor. Read the two branch labels: each sits at the midpoint of its own arrow, which is why the direction matters here more than it looks.
Two branch labels on a top-to-bottom decision collide
A label is drawn at the arc-length midpoint of its path. In a top-to-bottom layout both branches leave the diamond's south side, run down the same trunk, and only part near the bottom — so both midpoints land on that shared trunk and yes is printed over no. Widening node_gap does not fix it; it only makes the trunk longer. Two things do. Run the flow :left_to_right, as above, so the branches split vertically. Or keep it top-to-bottom and give the diamond connect_points = [:east, :west, :north], which forces the answers out of its sides and takes their labels with them.
§ 3.3Elbow routing
Placing the shapes is only half of it. Edges are routed afterwards, and by default (routing = :elbow) each one is an orthogonal polyline found by an A* search over a coarse grid, with the other shapes' boxes as obstacles and a turn penalty that biases the result toward long straight runs. The first and last leg leave perpendicular to the side they attach to, so an arrow meets a figure square-on rather than at a corner.
Three consequences are worth knowing:
- Edges route around shapes, not through them. That is why a flowchart with a long back-edge still reads: the arrow goes the long way rather than crossing three boxes.
- Parallel edges are nudged apart, so a shared corridor does not become one thick line. edge_separation (default 4) is the step. The legs touching a shape are never moved, so arrows leaving one anchor stay aligned.
- An unroutable edge fails the build. When the shapes are packed too tightly for any orthogonal path to exist, the build stops and names both ends of the edge rather than drawing a line through the boxes in between. Widen node_gap or layer_gap, grow the canvas, or set routing = :straight.
routing = :straight draws a single line between the two anchors and ignores everything in the way. It is the right answer for a hub-and-spoke picture and the wrong one for a dense flow. Connections and routing covers both, and connect_points, in full.
§ 4Swim-lanes are composed, not declared
A swim-lane flowchart says who does each step. wdoc has no swimlane block and no lane block, and it does not need one: a lane is a band drawn behind the shapes, and a diagram can already draw bands. You compose one out of parts this chapter and The diagram canvas have already covered.
The recipe is five steps:
- Leave the layout alone. A swim-lane gives a shape's position a meaning, so the position has to be yours. Keep the diagram's default :free layout and place every shape by x / y.
- Draw each lane as a rect spanning the full width of the canvas, with a translucent fill so the shapes on top of it stay legible.
- Name each lane with a label in a gutter down the left-hand side.
- Place each shape inside its lane's band — the same y for everything in one lane, x advancing with the flow.
- Wire it with edges as usual. The arrows cross between lanes, and those crossings are the point of the whole picture: each one is a hand-off from one party to another.
diagram {
width = 620 height = 268
desc = "A three-lane swim-lane flowchart: customer, sales, warehouse."
# The lane bands. Translucent grey reads on a light and a dark theme.
rect { x = 0.0 y = 0.0 width = 620.0 height = 84.0 fill = "#7f7f7f14" }
rect { x = 0.0 y = 92.0 width = 620.0 height = 84.0 fill = "#7f7f7f2e" }
rect { x = 0.0 y = 184.0 width = 620.0 height = 84.0 fill = "#7f7f7f14" }
# The lane titles, centred in a 96-unit gutter.
label "Customer" { x = 48.0 y = 42.0 font_size = 12.0 fill = "#888" }
label "Sales" { x = 48.0 y = 134.0 font_size = 12.0 fill = "#888" }
label "Warehouse" { x = 48.0 y = 226.0 font_size = 12.0 fill = "#888" }
# One shape per step, each inside its lane's band.
terminator "Place order" {
id = ordered x = 110.0 y = 22.0 width = 140.0 height = 40.0
}
decision "Approved?" {
id = approved x = 290.0 y = 104.0 width = 130.0 height = 60.0
}
process "Pick and pack" {
id = packing x = 290.0 y = 206.0 width = 140.0 height = 40.0
}
terminator "Shipped" {
id = shipped x = 470.0 y = 206.0 width = 130.0 height = 40.0
}
terminator "Refused" {
id = refused x = 470.0 y = 22.0 width = 130.0 height = 40.0
}
ordered -> approved :flow
approved -> packing :yes
approved -> refused :no
packing -> shipped :flow
}
Read it by its crossings. Three of the four arrows cross a band, and each crossing is a hand-off: the customer hands the order to sales, sales hands the approved order down to the warehouse, and sales hands a refusal back up to the customer. The fourth arrow, packing -> shipped, stays inside one band — that step is nobody else's business. None of that is in the shapes, and none of it is in the edges. All of it is in the y values.
Three numbers do the work, and writing them down once keeps the picture straight. A band pitch — 92 here — is the distance from one lane's top edge to the next one's. A band height — 84 — leaves an 8-unit gutter between bands. A shape inset — 22 — is the distance from a band's top edge to the top of the shapes in it. Every lane after the first is the one before it plus the pitch, so a shape 40 units high sits at y = 22.0, 114.0 or 206.0; the diamond is 60 high, so it sits at 104.0 to stay centred in its band.
Why the layout has to stay free
Only a label, a line and a boundary are exempt from a layout solver. A rect is an ordinary flow node, so under :layered your three lane bands are ranked like any other shape: they land on rank 0 side by side, 620 units wide each, and shove the flowchart off to the right of them. There is no error and no warning — only a picture that makes no sense. Swim-lanes and auto-layout are alternatives, not a combination.
The fill values are two translucent greys, #7f7f7f14 and #7f7f7f2e: one hue at two alpha levels, so alternating bands read as stripes. Mid-grey at a low alpha is the one colour that works over a white page and a dark one, which is why the lanes are the exception to the advice about fill above — a band is chrome, not a figure. If your site has a palette of its own, put the two greys in a class instead and let the theme choose them.
§ 4.1Vertical lanes
Turn the recipe ninety degrees and the lanes become columns with the flow running downward. The bands become full-height rectangles, the titles sit in a strip along the top, and the pitch now applies to x:
diagram {
width = 540 height = 330
desc = "The same three-lane flow drawn as vertical lanes."
rect { x = 0.0 y = 0.0 width = 172.0 height = 330.0 fill = "#7f7f7f14" }
rect { x = 184.0 y = 0.0 width = 172.0 height = 330.0 fill = "#7f7f7f2e" }
rect { x = 368.0 y = 0.0 width = 172.0 height = 330.0 fill = "#7f7f7f14" }
label "Customer" { x = 86.0 y = 22.0 font_size = 12.0 fill = "#888" }
label "Sales" { x = 270.0 y = 22.0 font_size = 12.0 fill = "#888" }
label "Warehouse" { x = 454.0 y = 22.0 font_size = 12.0 fill = "#888" }
terminator "Place order" {
id = v_ordered x = 16.0 y = 54.0 width = 140.0 height = 40.0
}
decision "Approved?" {
id = v_approved x = 205.0 y = 124.0 width = 130.0 height = 60.0
}
process "Pick and pack" {
id = v_packing x = 384.0 y = 204.0 width = 140.0 height = 40.0
}
terminator "Goods received" {
id = v_received x = 16.0 y = 274.0 width = 140.0 height = 40.0
}
v_ordered -> v_approved :flow
v_approved -> v_packing :yes
v_packing -> v_received :flow
}
Which way round to draw it is a question about the reader, not about wdoc. Horizontal lanes suit a process with many steps and few parties, because the picture grows in the direction a page is already wide. Vertical lanes suit the opposite — a handful of steps handed between many parties — and they read like a sequence diagram, which is what to reach for instead once the hand-offs matter more than the steps. Sequence and state diagrams covers that.
§ 5Choosing between the two
The two halves of this chapter are the two ways to place a shape, and they answer different questions.
| The question the picture answers | Reach for |
|---|---|
| What happens next? | :layered — let the edges rank the shapes |
| Who does each step? | Swim-lanes — position carries the answer |
| What is connected to what? | node with :force or :radial — see The diagram canvas |
| In what order do these parties talk? | A sequence diagram |
The temptation is to want both halves at once — auto-layout inside lanes. Nothing in the model offers it, because a lane is a claim about where a shape belongs and a solver's whole job is to decide where a shape belongs. Pick the one whose claim you care about.
§ 6Where to go next
- The diagram canvas — the diagram block itself, the other layout modes, container, boundary, and the primitive shapes a lane band is made of.
- Connections and routing — edges in depth: connect_points, routing, computed edge lists, and edge labels beyond yes / no.
- Sequence and state diagrams — the two other diagram families, both of which draw their own layout.
- Themes and styling — the class system behind wdoc-process and its siblings, and how to give a lane band a themed colour.
- Writing your own blocks — how process lowers to a Rect plus a Label, and how to declare a figure of your own the same way.