| name | excalidraw-render |
| description | Generate clean, production-quality Excalidraw architecture diagrams from any input — ASCII sketches, vague descriptions, component lists, screenshots, or rough mockups. Use this skill whenever the user asks for an architecture diagram, system diagram, data flow, sequence diagram, call chain, or any visual diagram, even when they just say "draw me X" or paste rough ASCII art. The skill writes a .excalidraw JSON file, renders it locally to PNG via @swiftlysingh/excalidraw-cli, and iterates by viewing the rendered image until layout, alignment, spacing, and labels are correct. Heavy use of Haiku sub-agents for all JSON authoring and edits keeps token costs low — the main agent only critiques rendered images, never reads or edits the raw JSON. |
Excalidraw Render Skill
Make a polished Excalidraw diagram from a rough input, without baby-sitting. The main agent thinks in pixels (by viewing rendered PNGs); a Haiku sub-agent does the JSON work.
Core principle — cost discipline
A .excalidraw file is huge: a 10-box diagram is 3–5K tokens. Reading or editing it with the main model is wasteful and slow. Vision on a rendered PNG is far cheaper and far more accurate for layout judgment.
The main agent MUST:
- ✅ View the rendered PNG to judge correctness (this is what vision is for)
- ✅ Plan layouts up-front (positions, sizes, colors) — architectural judgment
- ✅ Critique specific visual issues from the PNG (overlaps, clipping, misalignment)
- ✅ Dispatch surgical fix instructions to a Haiku sub-agent
The main agent MUST NOT:
- ❌ Read the
.excalidraw JSON file — delegate to Haiku
- ❌ Write the
.excalidraw JSON inline — delegate to Haiku
- ❌ Edit JSON with
str_replace manually — delegate to Haiku
- ❌ Loop forever — hard cap at 3 render–critique cycles
Workflow
User input (ASCII / description / sketch)
│
[MAIN] Plan layout ← architectural judgment
│
[HAIKU] Write diagram.excalidraw ← author the JSON
│
[BASH] npx excalidraw-cli convert ← produce PNG
│
[MAIN] View the PNG ← visual judgment
│
Issues? ── yes ──► [HAIKU] Apply specific fixes ──┐
│ │
no │
│ │
Present to user ◄────────────────────────────┘
(max 3 cycles)
Step 1 — Plan the layout (main agent)
Before any tool calls, decide:
- Components — what shapes (rectangles, diamonds, ellipses) and what each says
- Layout family — vertical flow, horizontal pipeline, hub-and-spoke, or 3-column data flow
- Coordinates — give every shape an exact
(x, y, width, height) using the Sizing Rules
- Colors — pick from the palette by role
- Connections — list arrows as
(from_id → to_id, label, style)
- Zones / titles / annotations — optional backgrounds, headers, side labels
Write this as a plain-text layout brief. NOT JSON. Haiku will produce the JSON.
Preserve all detail from the user's input. If the ASCII says "Fail = Stop & Notify", that label appears verbatim. Don't summarize. Resize boxes to fit the full text.
Step 2 — Hand off to Haiku to author the JSON
Spawn a claude-haiku-4-5 sub-agent. Pass it the layout brief, the output path, and inline the Schema Cheat Sheet below so it doesn't have to guess.
Sub-agent prompt template (author):
You are authoring a valid Excalidraw scene file. Output the complete JSON to:
<ABS_PATH>/diagram.excalidraw
# Layout brief
<paste the brief from Step 1 — every shape with exact x/y/w/h, every arrow, every label>
# Schema cheat sheet
<paste the "Schema cheat sheet" section from this skill verbatim>
# Rules
- Use only the colors from the palette in the brief.
- Every shape has an `id` so arrows can reference it.
- Labels inside shapes are SEPARATE `text` elements with `containerId` set to the shape's id, AND the shape's `boundElements` includes `{ "type": "text", "id": "<label-id>" }`.
- Arrow labels are SEPARATE `text` elements with `containerId` set to the arrow's id, plus matching `boundElements` on the arrow.
- Set `roughness: 0` everywhere for clean lines.
- Output ONLY the file. No commentary, no fences. Make sure it parses with `JSON.parse`.
The main agent does NOT review the resulting JSON. Trust Haiku and move on to rendering.
Step 3 — Render
npx @swiftlysingh/excalidraw-cli convert <path>/diagram.excalidraw --format png -o <path>/diagram.png --scale 2
No setup needed — npx handles installation on first run. Uses @excalidraw/utils under the hood (via jsdom + resvg-js), no headless browser required. If rendering fails for a large file, spawn a Haiku to summarize what's in the file (Haiku reads it, main agent never does) before fixing.
Step 4 — View the PNG and critique
view the PNG. Walk this checklist and write down concrete fixes in pixel / id terms:
| Check | What to look for | Fix instruction to Haiku |
|---|
| Overlap | Two shapes touch or cross | "Move shape <id> to (x, y) = (...)." |
| Clipped arrow label | Label cut into a shape or smudged | "Increase gap between <a> and <b> to 180px (set <b>.x to ...)." |
| Text overflow | Label spills outside the box | "Set <id>.width = 240 (or shorten label to '...')." |
| Cramped zone | Zone background hugs children | "Resize zone <id> to (x,y,w,h) = (...)." |
| Off-balance | Content piled to one side | "Shift columns: rebalance to center on x=400." |
| Missing title / layer labels | No header; layers unlabeled | "Add text at (x,y), content '...', fontSize 24." |
| Wrong arrow target | Arrow points to wrong box | "Change arrow <id>'s endBinding.elementId from X to Y." |
| Wrong color for role | DB shown blue, API green, etc. | "Recolor <id> to bg=#b2f2bb stroke=#2f9e44 (Database role)." |
Main agent describes WHAT needs to change in pixel/id terms; Haiku reads the file and applies it. Don't try to compute every coordinate yourself for more than 2-3 elements — let Haiku do bulk edits.
Step 5 — Hand off fixes to Haiku
Sub-agent prompt template (fix):
Edit the existing Excalidraw scene at <ABS_PATH>/diagram.excalidraw.
Apply ONLY these changes:
1. <fix #1>
2. <fix #2>
3. <fix #3>
# Schema cheat sheet
<paste the "Schema cheat sheet" section so Haiku knows the structure>
# Rules
- Read the file. Modify only the elements named. Preserve every other field exactly.
- When you move a shape, ALSO move its label text element (same containerId) by the same delta so they stay aligned.
- When you resize a shape, recompute the label element's x/y/width to stay centered.
- Write back to the same path. The result MUST parse with `JSON.parse`.
- Output ONLY the path of the file you wrote. No commentary.
Re-render and re-view. Hard cap: 3 cycles total. After cycle 3, present what you have and list any remaining issues explicitly. Don't loop more — the user can take it from there.
Step 6 — Present
Show the final rendered PNG with present_files. Briefly summarize: components shown, connections, and any unresolved issues. Both diagram.excalidraw (editable) and diagram.png (image) should be available.
Schema cheat sheet (copy-paste this into Haiku prompts)
Scene wrapper:
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [ ],
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
"files": {}
}
Rectangle / ellipse / diamond:
{
"id": "react-app",
"type": "rectangle",
"x": 40, "y": 50, "width": 220, "height": 90,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [], "frameId": null, "roundness": { "type": 3 },
"seed": 1, "version": 1, "versionNonce": 1, "isDeleted": false,
"boundElements": [{ "type": "text", "id": "react-app-label" }],
"updated": 1, "link": null, "locked": false
}
Text label inside a shape (separate element, bound by containerId):
{
"id": "react-app-label",
"type": "text",
"x": 50, "y": 75,
"width": 200, "height": 40,
"angle": 0,
"strokeColor": "#1e1e1e", "backgroundColor": "transparent",
"fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid",
"roughness": 0, "opacity": 100,
"groupIds": [], "frameId": null, "roundness": null,
"seed": 2, "version": 1, "versionNonce": 2, "isDeleted": false,
"boundElements": null, "updated": 1, "link": null, "locked": false,
"fontSize": 18, "fontFamily": 1,
"text": "React App\nFrontend",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "react-app",
"originalText": "React App\nFrontend",
"lineHeight": 1.25
}
Arrow with bindings and a label:
{
"id": "arrow1",
"type": "arrow",
"x": 260, "y": 95,
"width": 180, "height": 0,
"angle": 0,
"strokeColor": "#1971c2", "backgroundColor": "transparent",
"fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
"roughness": 0, "opacity": 100,
"groupIds": [], "frameId": null, "roundness": { "type": 2 },
"seed": 3, "version": 1, "versionNonce": 3, "isDeleted": false,
"boundElements": [{ "type": "text", "id": "arrow1-label" }],
"updated": 1, "link": null, "locked": false,
"points": [[0, 0], [180, 0]],
"lastCommittedPoint": null,
"startBinding": { "elementId": "react-app", "focus": 0, "gap": 4 },
"endBinding": { "elementId": "api-server", "focus": 0, "gap": 4 },
"startArrowhead": null, "endArrowhead": "arrow",
"elbowed": false
}
strokeStyle: "solid" (normal) · "dashed" (async / optional) · "dotted" (weak dependency).
Color palette
| Role | Background | Stroke |
|---|
| Frontend / UI | #a5d8ff | #1971c2 |
| Backend / API | #d0bfff | #7048e8 |
| Database | #b2f2bb | #2f9e44 |
| Storage | #ffec99 | #f08c00 |
| AI / ML | #e599f7 | #9c36b5 |
| External API | #ffc9c9 | #e03131 |
| Queue / Event | #fff3bf | #fab005 |
| Cache | #ffe8cc | #fd7e14 |
| Decision / Gate | #ffd8a8 | #e8590c |
| Zone / Group | #e9ecef | #868e96 |
Same role → same color. Limit a diagram to 3–4 fill colors.
Sizing rules
The #1 cause of ugly diagrams is cramping. When unsure, double the gap.
| Property | Value |
|---|
| Box width | 200–240px |
| Box height | 80–120px (3–4 lines comfortable) |
| Horizontal gap, labeled arrow | 150–200px |
| Horizontal gap, unlabeled arrow | 100–120px |
| Column pitch, labeled | 400px |
| Column pitch, unlabeled | 340px |
| Row pitch | 250–300px |
| Font, body | 16–18px |
| Font, title | 22–28px |
| Font, annotations | 13–14px |
| Zone padding around children | 50–60px on every side |
| Zone opacity | 25–40 |
Labeled-arrow visibility test: if the label is more than half the gap between its two boxes, increase the gap. Common offenders: "auto deploy", "rollback on failure", "All pass" — these are 100–150px wide and clip below ~150px gap.
Zone sizing rule: x = min(child_x) − 50, y = min(child_y) − 55, w = max(child_right) − x + 60, h = max(child_bottom) − y + 60.
Layout patterns
- Vertical flow (default): top→bottom, multi-column. Use for layered architectures and request flows.
- Horizontal pipeline: left→right, single row. Use for ETL, transforms.
- Hub-and-spoke: central shape at center, others radial. Use for event buses, brokers.
- 3-column data flow: left = layer names (gray, x<0), center = flow boxes (x: 60–360), right = data-form annotations (orange, x: 570+). Use for parameter threading and call-chain traces.
Common mistakes
| Mistake | Fix |
|---|
Main agent read or edited the .excalidraw JSON | Stop. Delegate to Haiku. The skill exists to avoid this. |
| Arrow labels clipped | Increase gap between the two boxes to ≥150px |
| Shapes overlap | Move one box by ≥200px; re-check zone bounds |
| Zone hugs its children | Recompute zone box with the Zone sizing rule above |
| Label drifted away from its shape after moving | When Haiku moves a shape, it must move the bound text element by the same delta |
| Dropped detail from user's sample | The sample is the source of truth. Every label verbatim. Resize boxes. |
| Same issue persists 3 cycles in a row | Stop. Present current PNG. List as open issue. Don't loop. |
| Same fix applied twice with no effect | Try a different approach — wider gap, restructure — not the same fix harder. |
| Mixed colors for the same role | One role → one color. Enforce on every iteration. |