Skip to content

Flow diagrams

A flow diagram shows structure: components and how they connect, dependencies, task order, states, and business rules with their branches. This guide is about drawing one well. The flow spec reference lists every field and its allowed values.

Two habits help throughout. Give every node and edge a short id of your own (client-api, not a number), because ids keep an element the same from one version to the next. And run diago check -strict on the spec before you render it: it reports content findings without drawing anything.

Most nodes are plain rectangles, the default. Give each other shape one role and keep it, so the reader learns the vocabulary once:

Shape Role In text art
rect services, processes, steps (the default) a box
rounded outside actors (users, clients, external systems); a flowchart’s start and end rounded corners
cylinder anything that holds state: a database, cache, queue, bucket a double top border
diamond decisions only ◇ marks
hexagon gateways, proxies a box, like rect
parallelogram input and output a box, like rect
circle events, start and end markers, short labels like rounded
shapes.json
{
"type": "flow",
"direction": "RIGHT",
"ignore": ["shape-soup"],
"nodes": [
{"id": "user", "label": "User", "shape": "rounded"},
{"id": "upload", "label": "Upload", "shape": "parallelogram"},
{"id": "gateway", "label": "Gateway", "shape": "hexagon"},
{"id": "service", "label": "Service", "shape": "rect"},
{"id": "valid", "label": "Valid?", "shape": "diamond"},
{"id": "db", "label": "Database", "shape": "cylinder"},
{"id": "done", "label": "Done", "shape": "circle"}
],
"edges": [
{"id": "user-upload", "from": "user", "to": "upload"},
{"id": "upload-gateway", "from": "upload", "to": "gateway"},
{"id": "gateway-service", "from": "gateway", "to": "service"},
{"id": "service-valid", "from": "service", "to": "valid"},
{"id": "valid-db", "from": "valid", "to": "db", "label": "yes"},
{"id": "db-done", "from": "db", "to": "done"}
]
}
A User uploads through a hexagon Gateway to a Service, which feeds a Valid? decision; a yes leads to a Database and then a Done circle.A User uploads through a hexagon Gateway to a Service, which feeds a Valid? decision; a yes leads to a Database and then a Done circle.
  ╭──────╮    ┌────────┐    ┌──────────┐    ┌──────────┐    ┌────────────┐    ╒══════════╕    ╭──────╮
  │ User │───►│ Upload │───►│ Gateway  │───►│ Service  │───►│ ◇ Valid? ◇ │───►│ Database │───►│ Done │
  ╰──────╯    └────────┘    └──────────┘    └──────────┘    └────────────┘    └──────────┘    ╰──────╯
                                                                          yes

This spec draws all seven shapes to show them, which is more than a real diagram should. Keep to about 4 distinct shapes: check reports shape-soup above 5, and the spec above silences it with "ignore": ["shape-soup"] because the finding is the point. Text art keeps only rect, rounded, cylinder and diamond apart, as the art above shows: when a diagram may be read as text, a role shown by hexagon, parallelogram or circle should also be in its label.

Keep labels to 1 to 3 words and prefer role names to identifiers: “Auth Service”, not auth-svc-v2. A word over 24 characters raises unbreakable-token, and a label over 60 characters raises long-label.

An edge is drawn solid unless you say otherwise. The line style carries meaning, and it survives in text art:

Style Meaning In text art
solid the main or synchronous path (the default) ─ │
dashed async calls, responses, events ╌ ╎
dotted optional links, monitoring, logging ┈ ┊
thick the one path the reader should follow, at most one per diagram ━ ┃

direction on an edge says where the arrowheads go: forward (the default) at the target, backward at the source, both at both ends, none at neither. Use both for a request and response pair, as one edge rather than two, and none for a peer link with no flow.

edges.json
{
"type": "flow",
"direction": "RIGHT",
"nodes": [
{"id": "hub", "label": "Hub"},
{"id": "s1", "label": "Main path"},
{"id": "s2", "label": "Async"},
{"id": "s3", "label": "Optional"},
{"id": "s4", "label": "Followed"},
{"id": "c1", "label": "Client"},
{"id": "c2", "label": "Server"},
{"id": "p1", "label": "Producer"},
{"id": "p2", "label": "Consumer"},
{"id": "n1", "label": "Peer A"},
{"id": "n2", "label": "Peer B"}
],
"edges": [
{"id": "hub-s1", "from": "hub", "to": "s1", "label": "solid", "style": "solid"},
{"id": "hub-s2", "from": "hub", "to": "s2", "label": "dashed", "style": "dashed"},
{"id": "hub-s3", "from": "hub", "to": "s3", "label": "dotted", "style": "dotted"},
{"id": "hub-s4", "from": "hub", "to": "s4", "label": "thick", "style": "thick"},
{"id": "c1-c2", "from": "c1", "to": "c2", "label": "both", "direction": "both"},
{"id": "p1-p2", "from": "p2", "to": "p1", "label": "backward", "direction": "backward"},
{"id": "n1-n2", "from": "n1", "to": "n2", "label": "none", "direction": "none"}
]
}
A hub fans out to four nodes by a solid, a dashed, a dotted and a thick edge, each labelled with its style; below it, a Client and Server joined by an edge with arrowheads at both ends, a Consumer pointed at by a backward edge from a Producer, and two peers joined by a line with no arrowhead.A hub fans out to four nodes by a solid, a dashed, a dotted and a thick edge, each labelled with its style; below it, a Client and Server joined by an edge with arrowheads at both ends, a Consumer pointed at by a backward edge from a Producer, and two peers joined by a line with no arrowhead.
                solid ┌────────────┐
                 ┌───►│ Main path  │
                 │    └────────────┘
                 │
                 │
                 │    ┌────────────┐
  ┌──────────┐   │ ┌╌►│   Async    │
  │          │───┘ ╎  └────────────┘
  │   Hub    │╌╌╌╌╌┘ dashed
  │          │┈┈┈┈┈┐ dotted
  │          │━━━┐ ┊  ┌────────────┐
  └──────────┘   ┃ └┈►│  Optional  │
                 ┃    └────────────┘
                 ┃
                 ┃
                 ┃    ┌────────────┐
                 └━━━►│  Followed  │
                thick └────────────┘


  ┌──────────┐        ┌────────────┐
  │  Client  │◄──────►│   Server   │
  └──────────┘  both  └────────────┘


  ┌──────────┐        ┌────────────┐
  │ Consumer │◄───────│  Producer  │
  └──────────┘        └────────────┘
              backward

  ┌──────────┐        ┌────────────┐
  │  Peer A  │────────│   Peer B   │
  └──────────┘  none  └────────────┘

Edge ids matter more than they look. diago diff and render -previous use them to decide which edge is which across two versions, so an edge you relabel is reported as changed, not as one removed and one added. Without an id diago derives from->to#n, which breaks when you rename a node.

Label an edge with a verb phrase (publishes order, not data) and only where its two ends do not already make the meaning obvious: each label is one more thing to place, so fewer labels read cleaner. A label such as data or calls is reported as vague-edge-label.

direction is the way the diagram flows: DOWN, UP, RIGHT, LEFT or AUTO. Pick it by the medium’s tight axis, not by habit. A terminal is short of width, so a long chain belongs DOWN; a slide or a document is short of height, so the same chain belongs RIGHT, as a banner. Here is one six-step chain both ways, as text art:

chain-down.json
{
"type": "flow",
"direction": "DOWN",
"nodes": [
{"id": "commit", "label": "Commit", "shape": "rounded"},
{"id": "build", "label": "Build"},
{"id": "test", "label": "Test"},
{"id": "package", "label": "Package"},
{"id": "deploy", "label": "Deploy"},
{"id": "verify", "label": "Verify"}
],
"edges": [
{"id": "commit-build", "from": "commit", "to": "build"},
{"id": "build-test", "from": "build", "to": "test"},
{"id": "test-package", "from": "test", "to": "package"},
{"id": "package-deploy", "from": "package", "to": "deploy"},
{"id": "deploy-verify", "from": "deploy", "to": "verify"}
]
}
A six-step pipeline running top to bottom: Commit, Build, Test, Package, Deploy, Verify.A six-step pipeline running top to bottom: Commit, Build, Test, Package, Deploy, Verify.
   ╭────────╮
   │ Commit │
   ╰────────╯
        │
        │
        ▼
   ┌────────┐
   │ Build  │
   └────────┘
        │
        │
        ▼
    ┌──────┐
    │ Test │
    └──────┘
        │
        │
        ▼
  ┌──────────┐
  │ Package  │
  └──────────┘
        │
        │
        ▼
   ┌────────┐
   │ Deploy │
   └────────┘
        │
        │
        ▼
   ┌────────┐
   │ Verify │
   └────────┘
chain-right.json
{
"type": "flow",
"direction": "RIGHT",
"nodes": [
{"id": "commit", "label": "Commit", "shape": "rounded"},
{"id": "build", "label": "Build"},
{"id": "test", "label": "Test"},
{"id": "package", "label": "Package"},
{"id": "deploy", "label": "Deploy"},
{"id": "verify", "label": "Verify"}
],
"edges": [
{"id": "commit-build", "from": "commit", "to": "build"},
{"id": "build-test", "from": "build", "to": "test"},
{"id": "test-package", "from": "test", "to": "package"},
{"id": "package-deploy", "from": "package", "to": "deploy"},
{"id": "deploy-verify", "from": "deploy", "to": "verify"}
]
}
The same six-step pipeline running left to right as a single row.The same six-step pipeline running left to right as a single row.
  ╭────────╮    ┌────────┐    ┌──────┐    ┌──────────┐    ┌────────┐    ┌────────┐
  │ Commit │───►│ Build  │───►│ Test │───►│ Package  │───►│ Deploy │───►│ Verify │
  ╰────────╯    └────────┘    └──────┘    └──────────┘    └────────┘    └────────┘

In text art the DOWN chain is 14 columns wide and 33 lines tall; the RIGHT chain is 82 columns wide and 3 lines tall. The reverse holds for a wide fan-out, one node with many children: RIGHT keeps it narrow in a terminal, and DOWN suits an image. A hierarchy, a decision tree or layers read best DOWN.

Set direction yourself rather than rely on AUTO, which chooses from the shape of the graph and does not know where the diagram will be read. UP fits only when the reader expects foundations at the bottom, and LEFT almost never. When in doubt, render both and keep the one that fits and reads better.

A group draws a frame around the nodes it contains. Use one for a boundary that matters to the reader: a deployment boundary, a package, a team, a trust zone. A group of one member only adds a frame, so give it at least 2. A group id in another group’s contains nests it, up to 3 levels; a fourth is a validation error.

groups.json
{
"type": "flow",
"title": "Production",
"direction": "DOWN",
"nodes": [
{"id": "client", "label": "Client", "shape": "rounded"},
{"id": "api", "label": "API"},
{"id": "auth", "label": "Auth"},
{"id": "db", "label": "Database", "shape": "cylinder"}
],
"edges": [
{"id": "client-api", "from": "client", "to": "api", "label": "sends request"},
{"id": "api-auth", "from": "api", "to": "auth", "label": "verifies"},
{"id": "api-db", "from": "api", "to": "db", "label": "reads orders"}
],
"groups": [
{"id": "cloud", "label": "Cloud", "color": "blue", "contains": ["services", "db"]},
{"id": "services", "label": "Services", "contains": ["api", "auth"]}
]
}
A Client calls an API inside a Services group, which sits inside a blue Cloud group together with a Database; the API verifies with Auth and reads orders from the Database. A Production title is above.A Client calls an API inside a Services group, which sits inside a blue Cloud group together with a Database; the API verifies with Auth and reads orders from the Database. A Production title is above.
            Production

               ╭────────╮
               │ Client │
               ╰────────╯
                    │
                    │ sends request
                    │
  ┌─ Cloud ─────────┼────────┐
  │                 │        │
  │  ┌─ Services ───┼─────┐  │
  │  │              ▼     │  │
  │  │          ┌──────┐  │  │
  │  │          │ API  │  │  │
  │  │          └──────┘  │  │
  │  │   verifies │  │    │  │
  │  │            │  │    │  │
  │  │      ┌─────┘  │    │  │
  │  │      ▼        │    │  │
  │  │  ┌──────┐     │    │  │
  │  │  │ Auth │     │    │  │
  │  │  └──────┘     │    │  │
  │  └───────────────┼────┘  │
  │                  │       │
  │     reads orders │       │
  │                  ▼       │
  │            ╒══════════╕  │
  │            │ Database │  │
  │            └──────────┘  │
  └──────────────────────────┘

Groups are where color pays off best: to tell domains apart, color the groups (here Cloud), not their nodes. Deeper groups get progressively different fill shading, so nesting reads at a glance. In text art, keep a group label to one short word: a long one widens the frame, and the art with it. The title above the diagram is covered below.

A diamond is for a decision, and every exit needs a label. A diamond with two or more exits and one unlabeled raises unlabeled-branch. Branches that rejoin simply point to the same node.

decision.json
{
"type": "flow",
"direction": "DOWN",
"nodes": [
{"id": "start", "label": "Order received", "shape": "rounded"},
{"id": "stock", "label": "In stock?", "shape": "diamond"},
{"id": "ship", "label": "Ship order"},
{"id": "backorder", "label": "Backorder"},
{"id": "notify", "label": "Email customer"}
],
"edges": [
{"id": "start-stock", "from": "start", "to": "stock"},
{"id": "stock-ship", "from": "stock", "to": "ship", "label": "yes"},
{"id": "stock-backorder", "from": "stock", "to": "backorder", "label": "no"},
{"id": "ship-notify", "from": "ship", "to": "notify"},
{"id": "backorder-notify", "from": "backorder", "to": "notify"}
]
}
An Order received step leads to an In stock? decision; yes goes to Ship order and no to Backorder, and both lead to Email customer.An Order received step leads to an In stock? decision; yes goes to Ship order and no to Backorder, and both lead to Email customer.
          ╭────────────────╮
          │ Order received │
          ╰────────────────╯
                   │
                   │
                   ▼
          ┌────────────────┐
          │ ◇ In stock? ◇  │
          └────────────────┘
               │ yes  │ no
               │      │
         ┌─────┘      └─────┐
         ▼                  ▼
  ┌────────────┐     ┌────────────┐
  │ Ship order │     │ Backorder  │
  └────────────┘     └────────────┘
         │                  │
         │                  │
         └─────┐      ┌─────┘
               ▼      ▼
          ┌────────────────┐
          │ Email customer │
          └────────────────┘

An ordinary edge says “this comes before that”, so diago ranks its ends. A link between peers has no order: replication between two sites that each serve their own traffic, or a sync between two services at one level. Mark it "flat": true and diago leaves the ends where the rest of the diagram puts them, side by side when there is room, then routes the edge on the finished layout. Without it the two sites below would stack.

flat.json
{
"type": "flow",
"direction": "DOWN",
"nodes": [
{"id": "east_web", "label": "Web"},
{"id": "east_api", "label": "API"},
{"id": "east_db", "label": "Database", "shape": "cylinder"},
{"id": "west_web", "label": "Web"},
{"id": "west_api", "label": "API"},
{"id": "west_db", "label": "Database", "shape": "cylinder"}
],
"edges": [
{"id": "east-web-api", "from": "east_web", "to": "east_api", "label": "routes"},
{"id": "east-api-db", "from": "east_api", "to": "east_db", "label": "queries"},
{"id": "west-web-api", "from": "west_web", "to": "west_api", "label": "routes"},
{"id": "west-api-db", "from": "west_api", "to": "west_db", "label": "queries"},
{
"id": "east-west",
"from": "east_db",
"to": "west_db",
"label": "replicates",
"style": "dashed",
"flat": true
}
],
"groups": [
{"id": "east", "label": "East", "contains": ["east_web", "east_api", "east_db"]},
{"id": "west", "label": "West", "contains": ["west_web", "west_api", "west_db"]}
]
}
Two groups, East and West, each hold a Web, an API and a Database chained top to bottom; a dashed replicates edge joins the two databases sideways.Two groups, East and West, each hold a Web, an API and a Database chained top to bottom; a dashed replicates edge joins the two databases sideways.
  ┌─ East ─────────┐     ┌─ West ─────────┐
  │                │     │                │
  │    ┌──────┐    │     │    ┌──────┐    │
  │    │ Web  │    │     │    │ Web  │    │
  │    └──────┘    │     │    └──────┘    │
  │        │       │     │        │       │
  │ routes │       │     │ routes │       │
  │        ▼       │     │        ▼       │
  │    ┌──────┐    │     │    ┌──────┐    │
  │    │ API  │    │     │    │ API  │    │
  │    └──────┘    │     │    └──────┘    │
  │        │       │     │        │       │
  │        │ queries     │        │ queries
  │        ▼     replicates       ▼       │
  │  ╒══════════╕  │     │  ╒══════════╕  │
  │  │ Database │╌╌┼╌╌╌╌╌┼╌►│ Database │  │
  │  └──────────┘  │     │  └──────────┘  │
  └────────────────┘     └────────────────┘

Two rules keep a flat edge honest. A node whose only edges are flat is placed as an unconnected node, wherever there is room, not beside its partner, so keep one ordinary link on it: a standby database with nothing else attached keeps its replication edge from the primary ordinary, which puts it under the primary. And do not mark a link flat when it is part of the flow, such as a request, a dependency or a pipeline step; a flat edge cannot be a self-loop. When no clean route exists, the edge is laid out as an ordinary one and the render warns with flat-edge-ranked.

Color is the weakest channel, since text art drops it: never let it carry meaning alone.

  • Use 0 to 2 accents per diagram: one for the focus (the entry point, the component under discussion), orange for a problem. Leave the rest uncolored.
  • Say what a color means in the sentence above the diagram. A flow diagram has no color legend.
  • Color groups rather than the nodes inside them to tell domains apart.
  • Avoid green, blue and red in a spec you will diff: the diff draws added elements green, changed ones blue and removed ones red.

Values are red, green, blue, yellow, orange, purple, gray or #rrggbb, on nodes, edges and groups.

Add a top-level "title" when the diagram stands alone in a file. It is a band above the diagram in SVG and PNG and a centered first line in text art, as in the Production diagram above. It never affects the layout.

Aim for 3 to 12 nodes in a chat reply and 5 to 15 in a document. check reports too-large above 20 nodes (groups do not count). Past that, split the subject into an overview and one zoom-in per area, and say how they fit together.

For every field and value, see the flow spec reference.