# WorkflowFigure

A process in lanes and phases, with the main path drawn heavier than its branches.

- Group: Diagrams
- Import: `import { WorkflowFigure } from '@misoto22/folio/diagrams'`
- Page: https://ui.misoto22.com/components/workflow-figure/
- Related: lifecycle-figure, sequence-figure, steps

## When to reach for it

A runbook, an approval chain, a CI pipeline — anything with an owner per step. mainPath is what turns fourteen boxes into a diagram with a subject.

## Anatomy

- **Figure shell** (required) — The same frame every figure sits in. What a process hands it is a key of LINE variants rather than of node kinds — Call, Primary path, Crosses a trust boundary, Asynchronous — because a runbook is read for its arrows and not for its boxes.
- **Lane** (required) — A 92-unit band per lane with its name set in the 118-unit gutter to its left. The band spans only what the lane actually holds, and a lane marked exception is washed rather than ruled — the one wash in the package’s diagrams, because nothing routes across it.
- **Column** (required) — What col indexes: a 168-unit column with a 58-unit gutter, the same across every lane. A node sits centred in its lane’s depth, moved off that centre line only by yOffset.
- **Phase header** — A mono caption at the x of its fromCol, over a rule drawn across the columns from fromCol to toCol — so a phase’s extent is something a reader can see rather than a field only its author knows. variant dashes that rule for security and thickens it for emphasis. A phase is an axis label, so it is set like one rather than framed.
- **Group frame** — A labelled rule around a run of columns inside ONE lane — a planning loop, an evidence path — dashed when its variant is security.
- **Main path** — mainPath, drawn as weight rather than as a mark of its own: every edge between two consecutive ids on it goes heavier whatever its own variant says.

## Best practices

### Do

- Give every node a lane that spec.lanes declares. An unknown lane id still resolves to the first lane — there is nowhere else to put the box — so a step written for the exception band is drawn in the right column, at the right size, in the wrong row, and development prints DIAGRAM_LANE_UNKNOWN naming the node.
- Write mainPath as the path in order. The heavier weight is applied to CONSECUTIVE pairs, so [intake, review, ship] emphasises intake→review and review→ship and nothing else; an id whose neighbour on the list is not the far end of a real edge changes nothing at all.
- Say what an edge MEANS with role and let the drawing follow it. async and error take the quiet dashed line, return takes an open arrowhead, and main and branch add no stroke of their own because mainPath already draws that distinction as weight. An explicit variant overrides all of it, which is what it is for.

### Don’t

- Phases partition the columns; they do not stack. Each is drawn as a rule across its own span, so two phases claiming the same columns put two rules on one line and one caption over the other — and a reader cannot tell that from one phase drawn twice.
- An empty lane is not a spacer. The band is sized from what the lane holds, so a lane with no nodes collapses to about one column beside the gutter with its name still set — and an exception lane in that state washes a strip of ground next to the figure rather than under the failure path.

## Accessibility

- Same contract as every figure: a named picture, with its nodes and relationships published as text beside it.
- The exception lane is the one washed band in the system’s diagrams, and it is still labelled — the wash is not carrying the meaning on its own.

## WorkflowFigure

A process: who does what, in what order, and where it can go wrong. Three structures stacked on one grid, and they are three because they answer three different questions a reader brings to a runbook. **Lanes** are rows and answer WHO. A lane marked `exception` is the one band that gets a wash rather than a rule, and it earns the exception to the frames-are-rules law because nothing routes across it — it IS the ground for the failure path, not a frame drawn over the happy one. **Phases** are columns across every lane and answer WHEN. Printed as captions on one header rule, because a phase is an axis label. **Groups** are frames inside a single lane and answer WHAT BELONGS TOGETHER — a planning loop, an evidence path. THE MAIN PATH IS HEAVIER, and that is the whole hierarchy. `mainPath` lists the node ids a reader should be able to follow without thinking. Every edge between two consecutive ids on it is drawn at the emphasis weight whatever its own variant says, which is the one thing that turns fourteen boxes and nineteen arrows into a diagram with a subject. An `error` role goes the other way — dashed and soft — so the exception path recedes without disappearing, and so does `async`. `branch` adds no stroke of its own: a branch is an edge the main path does not list, and that is already the difference on the page.

### Props

- `spec` (required) — `WorkflowSpec`.

Also accepts: `FigureChrome`.

## Example — default

```tsx
import { WorkflowFigure } from '@misoto22/folio/diagrams'

<WorkflowFigure
  spec={{
    schema_version: 2,
    meta: { title: 'Release', subtitle: 'Green tests ship; a red deploy rolls back.' },
    lanes: [
      { id: 'ci', label: 'CI' },
      { id: 'recover', label: 'Recovery', variant: 'exception' },
    ],
    phases: [
      { id: 'verify', label: 'Verify', fromCol: 0, toCol: 1 },
      { id: 'ship', label: 'Ship', fromCol: 2, toCol: 2 },
    ],
    nodes: [
      { id: 'lint', lane: 'ci', col: 0, type: 'backend', label: 'Lint', sublabel: 'eslint' },
      { id: 'test', lane: 'ci', col: 1, type: 'backend', label: 'Tests', sublabel: 'vitest' },
      { id: 'deploy', lane: 'ci', col: 2, type: 'cloud', label: 'Deploy', sublabel: 'Cloudflare' },
      { id: 'roll', lane: 'recover', col: 2, type: 'security', label: 'Roll back', sublabel: 'previous build' },
    ],
    mainPath: ['lint', 'test', 'deploy'],
    edges: [
      { id: 'a', from: 'lint', to: 'test' },
      { id: 'b', from: 'test', to: 'deploy', label: 'green' },
      { id: 'c', from: 'deploy', to: 'roll', label: 'health check failed', role: 'error', variant: 'dashed' },
    ],
  }}
/>
```

## Example — a branch and both outcomes

```tsx
import { WorkflowFigure } from '@misoto22/folio/diagrams'

<WorkflowFigure
  spec={{
    schema_version: 2,
    meta: {
      title: 'A pull request',
      subtitle: 'One gate, two ways out, and the way back in.',
      legend: { entries: { dashed: { label: 'Failed check' } } },
    },
    lanes: [
      { id: 'bot', label: 'Automation' },
      { id: 'author', label: 'Author' },
    ],
    groups: [{ id: 'gate', label: 'Merge gate', lane: 'bot', fromCol: 1, toCol: 2 }],
    nodes: [
      { id: 'open', lane: 'bot', col: 0, type: 'frontend', label: 'PR opened', sublabel: 'branch pushed' },
      { id: 'check', lane: 'bot', col: 1, type: 'backend', label: 'Checks', sublabel: 'lint and tests' },
      { id: 'merge', lane: 'bot', col: 2, type: 'cloud', label: 'Merge', sublabel: 'squash' },
      { id: 'changes', lane: 'author', col: 1, type: 'external', label: 'Changes requested', sublabel: 'review comment' },
    ],
    mainPath: ['open', 'check', 'merge'],
    edges: [
      { id: 'a', from: 'open', to: 'check' },
      { id: 'b', from: 'check', to: 'merge', label: 'all green' },
      { id: 'c', from: 'check', to: 'changes', label: 'a check failed', role: 'branch', variant: 'dashed' },
      { id: 'd', from: 'changes', to: 'check', label: 'pushed a fix', role: 'return' },
    ],
  }}
/>
```

## Example — three structures on one grid

```tsx
import { WorkflowFigure } from '@misoto22/folio/diagrams'

<WorkflowFigure
  spec={{
    schema_version: 2,
    meta: { title: 'Order fulfilment', subtitle: 'Three teams, three stages, one hop nobody waits for.' },
    lanes: [
      { id: 'store', label: 'Storefront' },
      { id: 'ops', label: 'Fulfilment' },
      { id: 'finance', label: 'Finance' },
    ],
    phases: [
      { id: 'take', label: 'Take', fromCol: 0, toCol: 0 },
      { id: 'settle', label: 'Settle', fromCol: 1, toCol: 2 },
      { id: 'ship', label: 'Ship', fromCol: 3, toCol: 3 },
    ],
    groups: [{ id: 'pci', label: 'PCI scope', lane: 'finance', fromCol: 1, toCol: 2, variant: 'security' }],
    nodes: [
      { id: 'cart', lane: 'store', col: 0, type: 'frontend', label: 'Checkout', sublabel: 'web' },
      { id: 'receipt', lane: 'store', col: 2, type: 'messagebus', label: 'Receipt email', sublabel: 'queued' },
      { id: 'pick', lane: 'ops', col: 3, type: 'backend', label: 'Pick and pack', sublabel: 'warehouse' },
      { id: 'auth', lane: 'finance', col: 1, type: 'security', label: 'Authorise', sublabel: 'card network', tag: 'PCI' },
      { id: 'capture', lane: 'finance', col: 2, type: 'backend', label: 'Capture', sublabel: 'settled' },
    ],
    mainPath: ['cart', 'auth', 'capture', 'pick'],
    edges: [
      { id: 'a', from: 'cart', to: 'auth', label: 'card token' },
      { id: 'b', from: 'auth', to: 'capture', label: 'approved' },
      { id: 'c', from: 'capture', to: 'pick', label: 'paid', fromSide: 'right', toSide: 'bottom' },
      { id: 'd', from: 'capture', to: 'receipt', label: 'confirmation', role: 'async', variant: 'dashed' },
    ],
    cards: [
      { dot: 'amber', title: 'PCI scope', items: ['Only the two boxes inside the dashed frame see a card number.'] },
      { dot: 'slate', title: 'Off the path', items: ['The receipt is queued, so a slow mailer cannot hold up a shipment.'] },
    ],
  }}
/>
```

## Example — what a role draws

```tsx
import { WorkflowFigure } from '@misoto22/folio/diagrams'

<WorkflowFigure
  spec={{
    schema_version: 2,
    meta: { title: 'Canary deploy', subtitle: 'One path forward, three ways off it.' },
    lanes: [
      { id: 'pipeline', label: 'Pipeline' },
      { id: 'side', label: 'Out of band' },
      { id: 'oncall', label: 'On call', variant: 'exception' },
    ],
    nodes: [
      { id: 'build', lane: 'pipeline', col: 0, type: 'backend', label: 'Build', sublabel: 'container image' },
      { id: 'canary', lane: 'pipeline', col: 1, type: 'cloud', label: 'Canary', sublabel: '5% of traffic' },
      { id: 'full', lane: 'pipeline', col: 2, type: 'cloud', label: 'Full rollout', sublabel: 'all regions' },
      { id: 'notify', lane: 'side', col: 1, type: 'messagebus', label: 'Announce', sublabel: '#deploys' },
      { id: 'page', lane: 'oncall', col: 2, type: 'security', label: 'Page the on-call', sublabel: 'PagerDuty' },
    ],
    mainPath: ['build', 'canary', 'full'],
    edges: [
      { id: 'a', from: 'build', to: 'canary', role: 'main' },
      { id: 'b', from: 'canary', to: 'full', label: 'healthy for 10m', role: 'main' },
      { id: 'c', from: 'canary', to: 'notify', label: 'posts the diff', role: 'async', variant: 'dashed' },
      { id: 'd', from: 'canary', to: 'page', label: 'errors above 2%', role: 'error', variant: 'dashed' },
      { id: 'e', from: 'page', to: 'build', label: 'previous image restored', role: 'return', variant: 'dashed' },
    ],
  }}
/>
```
