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.

KindFigureDefault sizeWhat it means
processRectangle80 × 40A step — something happens
decisionDiamond80 × 40A branch — a question with named answers
terminatorOval80 × 40A start or an end of the flow
nodeCircle56 × 56A 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:

wcl
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 }
}
The four flowchart shapes: terminator, process, decision, node.StartReserveInstock?queue

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:

FieldTypeWhat it does
the inline labelutf8The centred text. Wraps and shrinks to fit the figure.
ididentifier?The name an edge connects to. A shape with no id can never be an endpoint.
x / yf64 (default 0)Top-left corner. Omit under an auto-layout — see the warning below.
width / heightf64The figure's box. A layout solver reserves exactly this.
fill / strokeutf8?Explicit colours, which opt the shape out of theming.
classlist<utf8>?Style classes — see Themes and styling.
connect_pointslist<AnchorSide>?Which sides (:north / :east / :south / :west) edges may attach to. Default: all four.
iconutf8?An icon badge inside the figure — see Icons. icon_size, icon_pos and icon_class tune it.
linkutf8?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:

KindDraws
:defaultAn arrow.
:flowAn arrow.
:dataAn arrow.
:yesAn arrow labelled yes at its midpoint.
:noAn 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:

fulfilment.wclwcl
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
  }
}
text
$ wcl wdoc build fulfilment.wcl --out _site
wrote 1 page
A five-step order fulfilment flow, ranked top to bottom.Order placedReserve stockPick and packHand to courierDelivered

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:

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.

FieldDefaultWhat it controls
direction:top_to_bottomWhich axis the ranks stack along. :left_to_right is the other one.
layer_gap40.0Space between one rank and the next.
node_gap40.0Space between two shapes within a rank.
width / height (on the shape)80 × 40The 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:

wcl
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
}
A fulfilment flow branching at an in-stock decision.Order placedReserve stockInstock?Pick and packBack-orderDeliveredyesno

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:

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:

wcl
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
}
A three-lane swim-lane flowchart: customer, sales, warehouse.CustomerSalesWarehousePlace orderApproved?Pick and packShippedRefusedyesno

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:

wcl
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
}
The same three-lane flow drawn as vertical lanes.CustomerSalesWarehousePlace orderApproved?Pick and packGoods receivedyes

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 answersReach 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