FSM Diagrams in Pelican and Claude Code

Finite state machines turn up everywhere: protocol implementations, hardware controllers, UI flows, parsers. Drawing them well is useful but tedious. This post covers two tools I've built: a Pelican plugin that renders FSM diagrams at build time so they appear as images in published posts, and a Claude Code skill that generates diagrams from plain-English descriptions.

The pattern is identical to the WaveDrom plugin and skill: describe what you want, get a fenced code block, paste it into a post, and the build renders it to SVG automatically.

The Pelican plugin

The plugin is at github.com/morganp/pelican-fsm, cloned to ~/Code/pelican-fsm (a sibling directory to the blog repo). It intercepts fenced ```mermaid ``` and ```dot ``` code blocks before Pelican's standard Markdown processing, renders each one to an SVG using the appropriate CLI tool, caches the result by content hash in content/images/fsm/, and replaces the block with a standard image reference. Pelican then copies the SVG to output/images/fsm/ as a static asset.

SVGs are cached across builds, so only diagrams whose source has changed are re-rendered.

Requirements

npm install -g @mermaid-js/mermaid-cli
brew install graphviz      # macOS
sudo apt install graphviz  # Debian/Ubuntu

Both tools are optional. If one is missing, blocks for that type fall back gracefully to a fenced text block without failing the build.

Installation

Clone the plugin alongside your Pelican project and install it into the Pelican virtualenv in editable mode:

git clone https://github.com/morganp/pelican-fsm ../pelican-fsm
source venv/bin/activate
pip install -e ../pelican-fsm --config-settings editable_mode=compat

The editable_mode=compat flag is required. Without it, modern setuptools editable installs use a path-hook mechanism that prevents Pelican's namespace plugin auto-discovery from finding the plugin.

Pelican 4.5+ auto-discovers namespace plugins, so no changes to pelicanconf.py are needed.

Optional config

If mmdc or dot are not on your PATH during the build, set their full paths in pelicanconf.py:

FSM_MERMAID_CLI = '/opt/homebrew/bin/mmdc'
FSM_DOT_CLI = '/opt/homebrew/bin/dot'

Using it in a post

Write a fenced code block with the language set to mermaid or dot:

```mermaid
stateDiagram-v2
    [*] --> Idle
    Idle --> Running : start
    Running --> Idle : stop
    Running --> Error : fault
    Error --> Idle : reset
```

At build time this becomes an SVG embedded in the page:

FSM diagram

Graphviz DOT works the same way, and is a better fit for hardware and RTL documentation where the compact box-and-arrow style is conventional:

FSM diagram

If the CLI is not found or rendering fails, the block falls back to fenced text — the post still builds, you just see the raw source instead of a diagram.

The Claude Code skill

The skill lives at github.com/morganp/dotfiles/tree/main/config/claude/skills/fsm. Claude Code auto-discovers skills from ~/.claude/skills/ and loads them on demand.

The skill triggers automatically when you describe anything involving states, transitions, or control flow: protocol implementations, UI flows, hardware FSMs, parsers, game logic. You describe the system in plain English and Claude generates the diagram.

The skill knows:

  • Full Mermaid stateDiagram-v2 syntax: plain states, display labels, transitions with events/guards/actions, composite (nested) states, choice pseudostates, fork/join for parallel regions, concurrent regions (--), notes, direction, and classDef styling
  • Graphviz DOT FSM conventions: Moore vs Mealy annotation, initial pseudostate arrow, accepting states as double circles
  • When to prefer each format: Mermaid for software/web docs, DOT for hardware/RTL

Example workflow

Describe the system:

"Draw the states for a TCP connection"

Claude produces:

FSM diagram

Because the skill outputs a mermaid fenced block, you can paste it directly into a blog post and the Pelican plugin renders it automatically.

Installing the skill

mkdir -p ~/.claude/skills/fsm
curl -o ~/.claude/skills/fsm/SKILL.md \
  https://raw.githubusercontent.com/morganp/dotfiles/main/config/claude/skills/fsm/SKILL.md

Or clone the dotfiles repo and symlink:

git clone https://github.com/morganp/dotfiles ~/dotfiles
ln -s ~/dotfiles/config/claude/skills/fsm ~/.claude/skills/fsm

Syntax quick reference

Both formats can express the same FSM concepts, but use different syntax. The table below maps them side by side.

Concept Mermaid stateDiagram-v2 Graphviz DOT
Graph declaration stateDiagram-v2 digraph FSM { ... }
Default node style automatic node [shape=circle]
Left-to-right layout direction LR rankdir=LR
Comment %% comment // comment
Plain state StateName StateName
State with display label state "Label" as Name Name [label="Label"]
Initial pseudostate [*] --> State __start [shape=point width=0.2] + __start -> State
Final/accepting state State --> [*] State [shape=doublecircle]
Transition A --> B A -> B
Transition with event A --> B : event A -> B [label="event"]
Transition with action A --> B : event / action() A -> B [label="event / action()"]
Guarded transition A --> B : [guard] A -> B [label="[guard]"]
Composite (nested) state state S { ... } subgraph cluster_S { ... }
Choice pseudostate state C <<choice>> C [shape=diamond]
Fork pseudostate state F <<fork>> invisible node + multiple outgoing edges
Join pseudostate state J <<join>> invisible node + multiple incoming edges
Concurrent regions -- inside composite parallel subgraph blocks

Project structure

~/Code/
├── morganp.github.io/              # blog source (main branch)
   └── content/images/fsm/         # SVG cache (persists across make clean)
└── pelican-fsm/                    # github.com/morganp/pelican-fsm
    └── pelican/plugins/fsm_renderer/
        ├── __init__.py             # plugin entry point, signal registration
        └── preprocessor.py        # Markdown extension + preprocessor

~/.claude/skills/
└── fsm/
    └── SKILL.md                    # Claude Code skill definition

The combination is useful for any documentation that involves control flow: describe the behaviour, get the diagram from Claude, drop it into a post, and the plugin renders it at build time. No manual editing of diagram syntax, no copy-pasting between browser tabs.

social