From excalidraw
Create, edit, validate, render, and iterate on Excalidraw diagrams (.excalidraw JSON). Use when users ask to draw or modify diagrams, inspect existing drawings, connect nodes, move/relabel/recolor/delete elements, or review what is in a diagram file.
How this skill is triggered — by the user, by Claude, or both
Slash command
/excalidraw:excalidrawThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
------------------------------------------------------------------------
Use this skill as a deterministic workflow around the excalidraw-tools
CLI.
Never write raw .excalidraw JSON by hand or via custom generator
scripts. The excalidraw-tools package handles per-type defaults
(roundness, seed generation, index ordering) that are easy to get wrong
manually.
Follow this loop on every diagram task:
excalidraw-tools build,
excalidraw-tools edit, or scripts that import from
excalidraw_tools (see Create Diagrams below)excalidraw-tools validate/render/png).excalidraw
file and the .png fileAlways render after changes. The preview is the shared whiteboard view.
Always save the rendered PNG with the same base name as the
.excalidraw file. For example, if the diagram is
/tmp/foo.excalidraw, render to /tmp/foo.png (not a generic name like
excalidraw_preview.png).
Prefer this path whenever it is available:
curl -fsS http://localhost:3004/healthz >/dev/null
curl -fsS -X POST "http://localhost:3004/render/png" \
-H "Content-Type: text/plain" \
--data-binary "@/tmp/FILE.excalidraw" \
-o /tmp/FILE.png
If a custom render font was chosen for the session, append fontMap to
the URL:
curl -fsS -X POST "http://localhost:3004/render/png?fontMap=Helvetica:CMU+Serif" \
-H "Content-Type: text/plain" \
--data-binary "@/tmp/FILE.excalidraw" \
-o /tmp/FILE.png
If available in the repo, scripts/render_diagrams.sh is the preferred
batch/single-file wrapper:
scripts/render_diagrams.sh --input FILE.excalidraw
This path is the default. Use fallback only when this path is unavailable.
Use Matplotlib preview rendering only when the Chromium path is not available (use matching base name):
excalidraw-tools preview /tmp/FILE.excalidraw --output /tmp/FILE.png
The preview renderer is approximate and should not be preferred if
/render/png works.
Use renderer-safe math text in all diagram labels and annotations:
xₙ,
xₙ₊₁, c₁, x², φ(α), ≤, ≥, −, √π).$...$, \(...\), or \[...\]._ / ^ forms in final diagram text.<=, >=, -, ').When updating an existing diagram, normalize math labels to Unicode format before final render.
At the start of a diagram session, ask two questions:
"Do you want a sidecar .spec.json for this diagram?"
Offer three choices: 1. No spec file 2. Create/update spec on request 3. Keep spec synced after every round
If the user chooses option 3, keep using --sync-spec on each
build/edit command for the rest of that session unless they change
preference.
"What style? Hand-drawn (default) or Clean?"
| Preset | Font | Roughness | Description |
|---|---|---|---|
| Hand-drawn | Virgil (1) | 1 | Sketchy, informal (Excalidraw default) |
| Clean | Helvetica (2) | 0 | Crisp, presentation-ready |
If the user picks Clean, use fontFamily: 2 and roughness: 0 as
session defaults --- in specs (via the style block), in
excalidraw-tools edit flags (--font-family 2 --crisp), and in
library scripts.
If the user specifies individual values (e.g., "Cascadia font, sketchy"), use those instead. Available built-in fonts: 1=Virgil, 2=Helvetica, 3=Cascadia.
"Do you want a custom render font? The renderer can substitute any built-in Excalidraw font with a custom font at render time."
The fontMap parameter supports arbitrary FROM:TO mappings for any
built-in font name (Virgil, Helvetica, Cascadia). Common choices (system
fonts available in the renderer container):
| Render font | Maps from | fontMap value |
|---|---|---|
| CMU Serif | Virgil (1) | Virgil:CMU+Serif |
| CMU Serif | Helvetica (2) | Helvetica:CMU+Serif |
| CMU Sans Serif | Virgil (1) | Virgil:CMU+Sans+Serif |
| CMU Sans Serif | Helvetica (2) | Helvetica:CMU+Sans+Serif |
| CMU Typewriter Text | Cascadia (3) | Cascadia:CMU+Typewriter+Text |
Match the FROM font to the fontFamily used in the diagram. For
hand-drawn style (fontFamily 1), map from Virgil. For clean style
(fontFamily 2), map from Helvetica.
If the user picks a custom render font, store the fontMap query string
for the session and append it to every render curl command. The
.excalidraw file still uses the built-in fontFamily integer (e.g.,
fontFamily: 2); the substitution happens at render time only.
If the user does not want a custom font, omit fontMap from render
commands (the built-in font renders as-is).
The excalidraw-tools CLI provides these subcommands:
excalidraw-tools build: Build a new diagram from a compact JSON
specexcalidraw-tools edit: Deterministic edits (move, relabel,
recolor, delete, add-box, connect)excalidraw-tools validate: Schema and linkage validationexcalidraw-tools preview: Approximate PNG preview renderer
(matplotlib)excalidraw-tools sync-spec: Derive/update a .spec.json from an
.excalidraw fileexcalidraw-tools golden-check: Regression check against golden
fixtureThe Python library is importable as excalidraw_tools:
from excalidraw_tools import IdFactory, make_shape, new_document, save_diagram
The package is installed via uv tool install in an isolated virtual
environment, so the system Python cannot import it directly. Do not
use pip install or uv run --with --- the package is served from
GitHub, not a public PyPI.
| Task | Command |
|---|---|
| CLI subcommands | uvx excalidraw-tools <subcommand> ... |
| Python library scripts | "$(uv tool dir)/excalidraw-tools/bin/python" script.py |
All element-creating functions share this calling convention:
(elements, ids) where
elements is the list being built and ids is an IdFactory
instance.elements and
returns it.id, index, seed, versionNonce,
updated, or boundElements --- the library handles these.ids = IdFactory(seed=42)
ids.random_id() # → "el-hbrpoig8f1cb" (unique element ID)
ids.random_id("arr") # → "arr-fno6b9m80o2r" (custom prefix)
ids.nonce() # → 1738238662 (random int for seed/versionNonce)
ids.next_index() # → "a0", "a1", … (fractional z-index)
ids.reserve_id("my-id") # prevent future duplicates
make_shape(elements, ids, etype, x, y, width, height, *,
stroke="#1e1e1e", background="transparent",
stroke_width=2, stroke_style="solid",
roughness=1, element_id=None)
Supported etype values: "rectangle", "ellipse", "diamond". Do
not use "line" or "arrow" --- use make_arrow instead.
make_text(elements, ids, content, x, y, width, height, *,
container_id=None, font_size=20, font_family=1,
stroke="#1e1e1e")
width/height are required --- estimate from text length (e.g.,
width = len(text) * font_size * 0.6, height = font_size * 1.5).container_id (defaults to None, sets
verticalAlign="top").container_id=shape["id"] (sets
verticalAlign="middle"). Prefer add_label for this.make_arrow(elements, ids, x, y, points, *,
start_id=None, end_id=None,
stroke="#1e1e1e", stroke_width=2,
elbowed=False, source_edge=None, target_edge=None)
Use for all line and arrow elements --- including curves, tick
marks, axes, and polylines. points is a list of [x, y] offsets
relative to (x, y) (e.g., [[0, 0], [100, 0]] for a horizontal
segment).
endArrowhead="arrow" by default. For a
plain line (no arrowhead), override on the returned dict:
el["endArrowhead"] = None and el["type"] = "line".start_id/end_id bind the arrow to shapes (updates
boundElements on both).add_label(elements, ids, shape, label, *,
font_size=20, font_family=1, text_height=25)
Creates text centered inside shape. Automatically sets containerId
and updates shape["boundElements"].
connect(elements, ids, source, target, *,
source_edge="bottom", target_edge="top",
stroke="#1e1e1e", elbowed=False)
High-level arrow between two shapes. Calculates edge points and routing
automatically. Preferred over make_arrow when connecting labeled
shapes.
doc = new_document(elements) # wrap element list in document structure
save_diagram("/path/to/file.excalidraw", doc) # path first, data second
doc = load_diagram("/path/to/file.excalidraw") # read existing diagram
Always use the tools --- never write raw .excalidraw JSON.
nodes and
edges, then generate with excalidraw-tools build. Iterate with
excalidraw-tools edit subcommands.from excalidraw_tools import (
IdFactory, make_shape, make_text, make_arrow,
add_label, new_document, save_diagram,
)
ids = IdFactory(seed=42)
elements = []
# Rectangle with a bound label
box = make_shape(elements, ids, "rectangle", 100, 100, 200, 80,
stroke="#1971c2", background="#a5d8ff")
add_label(elements, ids, box, "API Server")
# Standalone text
make_text(elements, ids, "Clients", 160, 30, 80, 25, font_size=16)
# Arrow (axis, line, or connector)
make_arrow(elements, ids, 200, 180, [[0, 0], [0, 60]],
stroke="#e03131")
# Plain line (no arrowhead) — override the returned element
line = make_arrow(elements, ids, 50, 200, [[0, 0], [300, 0]])
line["type"] = "line"
line["endArrowhead"] = None
save_diagram("/tmp/example.excalidraw", new_document(elements))
Run the script using the excalidraw-tools venv Python (see Tooling
Layout above):
"$(uv tool dir)/excalidraw-tools/bin/python" /tmp/example.py
Warning --- make_shape and element types: - Do not use
make_shape with etype="line" or etype="arrow". It produces
elements missing the required points array, which crashes the Kroki
renderer (error:
Cannot read properties of undefined (reading 'length')). - Use
make_arrow for all lines, arrows, axes, curves, and polylines. For a
plain line without an arrowhead, override the returned dict
(el["type"] = "line" and el["endArrowhead"] = None). - For simple
grid lines or separators, thin rectangles also work (e.g., width=1 for
vertical, height=1 for horizontal).
nodes and edges.excalidraw-tools build --spec diagram.spec.json --output system-architecture.excalidraw
If continuous spec sync is enabled:
excalidraw-tools build --spec diagram.spec.json --output system-architecture.excalidraw --sync-spec
?fontMap=... to the curl
URL:excalidraw-tools validate /tmp/system-architecture.excalidraw
if curl -fsS http://localhost:3004/healthz >/dev/null; then
curl -fsS -X POST "http://localhost:3004/render/png" \
-H "Content-Type: text/plain" \
--data-binary "@/tmp/system-architecture.excalidraw" \
-o /tmp/system-architecture.png
else
excalidraw-tools preview /tmp/system-architecture.excalidraw --output /tmp/system-architecture.png
fi
{
"seed": 42,
"updated": 1700000000000,
"style": {
"fontFamily": 2,
"roughness": 0
},
"nodes": [
{
"id": "api",
"type": "rectangle",
"label": "API",
"x": 120,
"y": 220,
"width": 200,
"height": 80,
"stroke": "#7048e8",
"background": "#d0bfff"
}
],
"edges": []
}
The style block is optional. When omitted, defaults are
fontFamily: 1 (Virgil) and roughness: 1 (sketchy). Per-node
fontFamily or roughness overrides the style block.
Use excalidraw-tools edit subcommands.
excalidraw-tools edit move --input diagram.excalidraw --label "API" --dx 220 --dy 0
excalidraw-tools edit relabel --input diagram.excalidraw --label "API" --text "Gateway API"
excalidraw-tools edit recolor --input diagram.excalidraw --label "Gateway API" --stroke "#1971c2" --background "#a5d8ff"
excalidraw-tools edit delete --input diagram.excalidraw --label "Legacy Service"
excalidraw-tools edit add-box --input diagram.excalidraw --label "Cache" --x 520 --y 220 --width 180 --height 80 --stroke "#fd7e14" --background "#ffe8cc"
For clean style, add --font-family 2 --crisp.
excalidraw-tools edit connect --input diagram.excalidraw --from-label "Gateway API" --to-label "Cache" --from-edge right --to-edge left --elbowed --label "Redis"
If continuous spec sync is enabled, append --sync-spec to each edit
command.
After any edit, run validate + render.
If a diagram already exists and no spec is present, derive one:
excalidraw-tools sync-spec --diagram system-architecture.excalidraw --spec
Omit --spec value to use the default sidecar path
(system-architecture.spec.json).
When asked what is in a file:
freedraw content as approximateUse (always match the PNG base name to the .excalidraw file):
if curl -fsS http://localhost:3004/healthz >/dev/null; then
curl -fsS -X POST "http://localhost:3004/render/png" \
-H "Content-Type: text/plain" \
--data-binary "@FILE.excalidraw" \
-o FILE.png
else
excalidraw-tools preview FILE.excalidraw --output FILE.png
fi
Before finalizing major changes to this skill:
excalidraw-tools validate assets/golden/simple-flow.excalidraw
excalidraw-tools golden-check
golden-check verifies: - schema validity - expected element counts -
deterministic golden hash - spec round-trip consistency - render smoke
test
Default rendering should use the Chromium-backed /render/png path
(browser-accurate text shaping and spacing).
The excalidraw-tools preview command is fallback-only and
intentionally approximate: - uses Matplotlib, not Excalidraw's browser
renderer - applies invert_yaxis() to match screen coordinates - good
for review loops when the Chromium path is unavailable - does not
support fontMap --- custom font substitution only works with the
Chromium renderer
If exact visual parity is required, open the .excalidraw file in
Excalidraw.
The Chromium renderer accepts a scale query parameter that sets the
device pixel ratio for the screenshot. The viewport stays at the logical
size, but Chromium renders at N× pixel density --- producing a PNG that
is scale × width pixels wide with perfect text and color fidelity.
# 3× resolution (default 1600px width → 4800px output)
curl -fsS -X POST "http://localhost:3004/render/png?scale=3" \
-H "Content-Type: text/plain" \
--data-binary "@FILE.excalidraw" \
-o FILE.png
scale composes with width: ?scale=2&width=3200 produces a 6400px
wide PNG. The render_diagrams.sh wrapper also accepts --scale N.
The Chromium renderer supports a fontMap query parameter that replaces
Excalidraw's built-in font names in the SVG before rasterization.
Format: fontMap=FROM:TO (comma-separated for multiple). The
.excalidraw file is unchanged; substitution is render-only.
For fonts registered in renderer/fonts/fonts.json (custom
.otf/.ttf/.woff2 files), the renderer also injects @font-face
CSS rules so Chromium can resolve them. System fonts installed in the
container (e.g., CMU via fonts-cmu) need no fonts.json entry.
Load detailed references only when needed:
references/json-format.md: Required fields and element rulesreferences/arrows.md: Arrow routing patterns and edge mathnpx claudepluginhub ddarmon/excalidraw-tools --plugin excalidrawGenerates architecture diagrams on a live Excalidraw canvas from text, components, or samples for data flows, call chains, and exports to PNG/SVG/Excalidraw.
Generates Excalidraw JSON diagram files (.excalidraw) for visualizing workflows, architectures, and concepts as visual arguments with evidence artifacts for technical diagrams.
Generates hand-drawn Excalidraw diagrams for concepts, architectures, mind maps, flows, and comparisons. Enforces accessibility and cognitive limits; auto-renders JSON to SVG.