--- name: excalidraw description: Use when user requests diagrams, flowcharts, architecture charts, or visualizations. Also use proactively when explaining systems with 3+ components, complex data flows, or relationships that benefit from visual representation. Generates .excalidraw files and exports to PNG/SVG via Kroki API or locally using excalidraw-brute-export-cli. homepage: https://github.com/Agents365-ai/excalidraw-skill metadata: {"version":"1.3.0","openclaw":{"requires":{"bins":["curl"]},"emoji":"🎨"}} --- # Excalidraw Diagrams ## Overview Generate `.excalidraw` JSON files and export to PNG/SVG. **Two export options:** - **Kroki API** (`curl`) β€” zero install, SVG output only - **excalidraw-brute-export-cli** β€” local Firefox-based, PNG + SVG **Supported formats:** PNG (local CLI only), SVG (both options). PDF is NOT supported. ## When to Use **Explicit triggers:** user says "η”»ε›Ύ", "diagram", "visualize", "flowchart", "draw", "ζžΆζž„ε›Ύ", "桁程图" **Proactive triggers:** - Explaining a system with 3+ interacting components - Describing a multi-step process or decision tree - Comparing architectures or approaches side by side **Skip when:** a simple list or table suffices, or user is in a quick Q&A flow **When NOT to use it β€” route elsewhere:** - Polished, precise diagrams, strict UML, or branded vendor icons β†’ **drawio**. - Diagrams-as-code in git, auto-laid-out from text β†’ **mermaid** (general) or **plantuml** (UML). - An infinite-canvas whiteboard or programmatic freehand strokes β†’ **tldraw**. ## Prerequisites ### Option A: Kroki API (recommended β€” zero install, SVG only) ```bash # Just needs curl (pre-installed on macOS/Linux/Windows Git Bash) curl --version ``` No additional setup. SVG rendered via `https://kroki.io`. ### Option B: Local CLI (required for PNG) The CLI uses **Firefox** (not Chromium). Check and install: ```bash npm install -g excalidraw-brute-export-cli npx playwright install firefox ``` **macOS patch (one-time, required):** ```bash CLI_MAIN=$(npm root -g)/excalidraw-brute-export-cli/src/main.js sed -i '' 's/keyboard.press("Control+O")/keyboard.press("Meta+O")/' "$CLI_MAIN" sed -i '' 's/keyboard.press("Control+Shift+E")/keyboard.press("Meta+Shift+E")/' "$CLI_MAIN" ``` **Windows/Linux:** No patch needed. ## Workflow 1. **Check deps** β€” use Kroki (curl) for SVG; use local CLI for PNG 2. **Plan** β€” pick the visual metaphor (see **Relationship-to-layout map**), then the diagram type and color palette 3. **Generate** β€” write `.excalidraw` JSON file (section-by-section for large diagrams) 4. **Export** β€” run Kroki or CLI command 5. **Verify the render** β€” view the exported PNG, fix any defects, re-export (see **Verify the Render**) 6. **Review loop** β€” show the image to the user, apply the minimal `.excalidraw` edit per request, re-export until approved (see **Review Loop**) 7. **Report** β€” tell user the output file path ## Design Principles ### Default style - `roughness: 0` β€” clean, modern look for all technical diagrams (use `1` only when user requests hand-drawn/casual style) - `fontFamily: 2` (Helvetica) β€” professional look; use `1` (Virgil) only for casual/sketch style, `3` (Cascadia) for code snippets - `fillStyle: "solid"` β€” default fill ### Containers: prefer typography over boxes A box around every label makes a diagram look like a wireframe. The cleanest Excalidraw diagrams use **free-floating text and lines** for structure and reserve filled boxes for things that are genuinely *components*. - **Default to no container** β€” use a standalone `text` element unless the box earns its place. - **Add a box only when** the element is a real system component, an arrow binds to it, the shape itself carries meaning (decision diamond, start/end ellipse), or it groups a zone. - Aim for **under ~30% of text elements inside boxes.** - For timelines, trees, and hierarchies, use a **line/connector + free-floating labels**, not a stack of rectangles. Size, weight, and color create hierarchy without boxes. ### Font size hierarchy | Level | Size | Use for | |-------|------|---------| | Title | 28px | Diagram title | | Header | 24px | Section/group headers | | Label | 20px | Primary element labels | | Description | 16px | Secondary text, descriptions | | Note | 14px | Annotations, fine print | ### Color palette Follow the **60-30-10 rule**: 60% whitespace/neutral, 30% primary accent, 10% highlight. **Semantic fill colors** (use with `strokeColor` one shade darker): | Category | Fill | Stroke | Use for | |----------|------|--------|---------| | Primary / Input | `#dbeafe` | `#1e40af` | Entry points, APIs, user-facing | | Success / Data | `#dcfce7` | `#166534` | Data stores, success states | | Warning / Decision | `#fef9c3` | `#854d0e` | Decision points, conditions | | Error / Critical | `#fee2e2` | `#991b1b` | Errors, alerts, critical paths | | External / Storage | `#f3e8ff` | `#6b21a8` | External services, databases, AI/ML | | Process / Default | `#e0f2fe` | `#0369a1` | Standard process steps | | Trigger / Start | `#fed7aa` | `#c2410c` | Start nodes, triggers, events | | Neutral / Container | `#f1f5f9` | `#475569` | Groups, swimlanes, backgrounds | **Text colors:** | Level | Color | |-------|-------| | Title | `#1e293b` | | Label | `#334155` | | Description | `#64748b` | **Rule: Do not invent new colors.** Pick from this palette. ### Arrow semantics | Style | Meaning | |-------|---------| | Solid (`strokeStyle: "solid"`) | Primary flow, main path | | Dashed (`"dashed"`) | Response, async, callback | | Dotted (`"dotted"`) | Optional, reference, weak dependency | ## Excalidraw JSON Structure ### File skeleton ```json { "type": "excalidraw", "version": 2, "source": "claude-code", "elements": [], "appState": { "viewBackgroundColor": "#ffffff" } } ``` ### Element types | type | use for | |-----------|----------------------------------| | rectangle | boxes, components, modules | | ellipse | start/end nodes, databases | | diamond | decision points | | arrow | directed connections | | line | undirected connections | | text | standalone labels | `image`, `frame`, and `embeddable` are **not covered** by this skill: `image` needs a separate `files` map plus a `fileId`, and frames/embeds render inconsistently through the export path. Stick to the six types above β€” and for ready-made icons built *from* these primitives, see **Community shape & icon libraries** below. ### Community shape & icon libraries Need a real AWS / Azure / GCP / network / UML / BPMN icon instead of a plain box? The [Excalidraw community libraries](https://libraries.excalidraw.com) (200+ `.excalidrawlib` files) are built almost entirely from the **same vector primitives** above β€” so their items **render through Kroki and the local CLI** with no `image` element and no `files` map. Use the helper in `scripts/excalidraw_lib.py`: ```bash # 1. Find a library (matches name / description / item names) python scripts/excalidraw_lib.py search aws # 2. List its items (index, name, element count; flags any image-based item) python scripts/excalidraw_lib.py items slobodan/aws-serverless.excalidrawlib # 3. Build your base scene first, then drop an item in at (x, y). IDs are # namespaced and coordinates translated, so it merges without collisions: python scripts/excalidraw_lib.py merge scene.excalidraw \ slobodan/aws-serverless.excalidrawlib 0 455 257 --scale 0.9 --prefix lambda ``` **Rules:** - **Vector only.** `merge` refuses any item containing an `image` element (won't render via the export path); `items` flags them up front. - **Use sparingly.** An icon is just a labeled node β€” keep the design system's spacing, labels, and arrow semantics. Icons accent a diagram; they don't replace it. - **Arrows don't bind to library groups** β€” draw connectors with explicit edge-to-edge `points` (bindings don't affect the static export anyway). - Libraries are MIT-licensed; a courtesy credit is welcome, not required. - Still run **Verify the Render** afterward β€” icon bounding boxes vary, so check alignment and spacing. ### Element sizing Calculate element width from label text to prevent truncation: ``` Latin text: width = max(160, charCount * 9) CJK text: width = max(160, charCount * 18) Mixed text: estimate each character individually, sum up ``` Height: use `60` for single-line labels, add `24` per additional line. **Standalone `text` does NOT auto-wrap.** For multi-line standalone labels, insert manual `\n` line breaks yourself β€” aim for ≀ ~30 Latin (≀ ~15 CJK) characters per line at 16px β€” and add `24` height per line. (Text *bound inside a shape* via `containerId` wraps to the container width automatically, so size the container instead of adding `\n`.) ### Required properties (all elements) ```json { "id": "auth_service", "type": "rectangle", "x": 100, "y": 100, "width": 160, "height": 60, "angle": 0, "strokeColor": "#1e40af", "backgroundColor": "#dbeafe", "fillStyle": "solid", "strokeWidth": 2, "roughness": 0, "opacity": 100, "seed": 100001, "boundElements": [ { "id": "arrow_to_db", "type": "arrow" }, { "id": "label_auth", "type": "text" } ] } ``` Use **descriptive string IDs** (e.g., `"api_gateway"`, `"arrow_gw_to_auth"`) instead of random strings. Give each element a unique `seed` (integer). Namespace by section: 100xxx, 200xxx, 300xxx. ### JSON field rules - `boundElements`: use `null` when empty, never `[]` - `updated`: always use `1`, never timestamps - Do NOT include: `frameId`, `index`, `versionNonce`, `rawText` - `points` in arrows: always start at `[0, 0]` - `seed`: must be a positive integer, unique per element ### Property values Use only these values β€” all verified to render via Kroki and the local CLI: | Property | Valid values | |----------|--------------| | `fillStyle` | `"solid"`, `"hachure"`, `"cross-hatch"`, `"zigzag"` | | `strokeStyle` | `"solid"` (or omit), `"dashed"`, `"dotted"` | | `fontFamily` | `1` (Virgil, hand-drawn), `2` (Helvetica), `3` (Cascadia, code) | | `textAlign` | `"left"`, `"center"`, `"right"` | | `verticalAlign` | `"top"`, `"middle"`, `"bottom"` | | `startArrowhead` / `endArrowhead` | `null`, `"arrow"`, `"triangle"`, `"bar"`, `"dot"`, `"circle"`, `"diamond"`, `"crowfoot_many"` | Arrows default to `endArrowhead: "arrow"` and `startArrowhead: null` β€” omit both for a standard one-way arrow. Use `"triangle"` for UML inheritance, `"diamond"` for composition, and `"crowfoot_many"` for ER cardinality. > **Need copy-paste templates or the full property/arrowhead catalogue?** Read `references/schema-reference.md` β€” complete element templates (component+label, bound arrow, arrow label, swimlane zone, mind-map connector) and every verified property value. ### Text inside shapes (contained text) When text belongs inside a shape, bind them bidirectionally: ```json { "id": "label_auth", "type": "text", "text": "Auth Service", "fontSize": 20, "fontFamily": 2, "textAlign": "center", "verticalAlign": "middle", "strokeColor": "#1e293b", "containerId": "auth_service" } ``` **CRITICAL: Text `strokeColor` is the text color.** Always set it explicitly to a dark color from the text color palette. Never omit it β€” omitting `strokeColor` on text can cause invisible text that blends with the shape background. The parent shape must list the text in its `boundElements`: ```json "boundElements": [{ "id": "label_auth", "type": "text" }] ``` ### Arrow binding (bidirectional) Arrows must bind to shapes, and shapes must reference bound arrows: ```json { "id": "arrow_gw_to_auth", "type": "arrow", "points": [[0, 0], [200, 0]], "startBinding": { "elementId": "api_gateway", "gap": 5, "focus": 0 }, "endBinding": { "elementId": "auth_service", "gap": 5, "focus": 0 } } ``` Both `api_gateway` and `auth_service` must include in their `boundElements`: ```json "boundElements": [{ "id": "arrow_gw_to_auth", "type": "arrow" }] ``` **Endpoints must reach the shape borders.** `startBinding`/`endBinding` (and their `gap`) only affect interactive editing on excalidraw.com β€” they do **NOT** clip the line when exporting via Kroki or the local CLI. The exporter draws your `points` literally. So compute endpoints edge-to-edge: set the arrow's `x`/`y` to the source shape's border (the side facing the target) and the last point to the target's border. Center-to-center points draw the line straight *through* both shapes. ### Arrow labels To label an arrow, bind a `text` element to it exactly like shape text: set the label's `containerId` to the **arrow's** id, and add the label to the arrow's `boundElements`. Excalidraw then centers the label on the arrow and masks the line behind the text, so it stays readable (no strike-through). ```json { "id": "arrow_valid_to_grant", "type": "arrow", "points": [[0, 0], [0, 120]], "boundElements": [{ "id": "lbl_yes", "type": "text" }] } ``` ```json { "id": "lbl_yes", "type": "text", "text": "Yes", "fontSize": 14, "width": 36, "strokeColor": "#1e293b", "containerId": "arrow_valid_to_grant" } ``` **CRITICAL: the label `width` must fit the text (`charCount * 9`), NOT the arrow length.** Excalidraw masks the line behind the label's full bounding box β€” a label as wide as the arrow masks the *entire* arrow, so the line disappears and only floating text remains. Keep label widths small. ### Arrow routing **L-shaped (elbow) arrows** β€” orthogonal routing with 3+ points: ```json "points": [[0, 0], [100, 0], [100, 150]] ``` **Elbowed arrows** β€” automatic right-angle routing: ```json { "type": "arrow", "points": [[0, 0], [0, -50], [200, -50], [200, 0]], "elbowed": true } ``` **Curved arrows** β€” smooth routing with waypoints: ```json { "type": "arrow", "points": [[0, 0], [50, -40], [200, 0]], "roundness": { "type": 2 } } ``` ### Grouping Related elements share `groupIds`. Nested groups list IDs innermost-first: ```json "groupIds": ["inner_group", "outer_group"] ``` ## Diagram Patterns Choose the right visual pattern for each diagram type. ### Relationship-to-layout map Before locking in a *diagram type*, pick the *visual metaphor* that matches the relationship in the idea β€” it drives the layout more than the type label does: | Relationship in the idea | Visual metaphor | Build with | |---|---|---| | One β†’ many (broadcast, dispatch) | **Fan-out** | one node, arrows radiating outward | | Many β†’ one (aggregate, merge) | **Convergence** | several inputs, arrows into one node | | Parent β†’ children (hierarchy) | **Tree** | trunk + branch *lines*, free-floating text | | Repeating cycle (loop, feedback) | **Cycle** | nodes in a ring, curved arrows back to start | | Input β†’ transform β†’ output | **Assembly line** | left-to-right pipeline of steps | | A vs B (comparison) | **Side-by-side** | two parallel columns on a shared baseline | | Before / after, phase break | **Gap** | whitespace or a dashed divider between groups | | Fuzzy / overlapping state | **Cloud** | overlapping ellipses, no hard borders | ### Spacing Reference | Scenario | Spacing | |----------|---------| | Labeled arrow gap (between shapes) | 150–200px | | Unlabeled arrow gap | 100–120px | | Column spacing (labeled arrows) | 400px (220px box + 180px gap) | | Column spacing (unlabeled arrows) | 340px (220px box + 120px gap) | | Row spacing | 280–350px (150px box + 130–200px gap) | | Zone/container padding | 50–60px around children | | Zone/container opacity | 25–40 | | Minimum gap between any elements | 40px | ### Flowchart (LR or TB) - Ellipse for start/end, diamond for decisions, rectangle for process - 200px horizontal spacing, 150px vertical spacing - Decision branches: "Yes" goes forward, "No" goes down - 3–10 steps (max 15) ### Architecture / System Diagram - Column spacing per table above; use labeled arrow spacing when connections have labels - Group related services in dashed `Neutral` containers (opacity: 30, padding: 50px) - Gateway/entry at left or top, databases at right or bottom - 3–8 entities (max 12) ### Sequence Diagram - 200px between participants (rectangles at top) - Vertical lifelines as dashed lines - Horizontal arrows for messages, 60px vertical spacing - Solid arrow = request, dashed arrow = response ### Mind Map - Central node: largest (200x100), `Trigger` color - Level 1: 150x70, `Primary` color, radial around center - Level 2: 120x50, `Process` color - Level 3: 90x40, `Neutral` color - Use lines (not arrows) for connections - 4–6 branches (max 8), 2–4 sub-topics per branch - **Place level-1 branches on a circle** of radius `R β‰ˆ 280` around the center `(cx, cy)`: for branch `i` of `n`, `angle = 2π·i/n`, `x = cx + RΒ·cos(angle)`, `y = cy + RΒ·sin(angle)`. Even spacing prevents the crossed-line tangle that ad-hoc placement produces. ### Swimlane - Large transparent rectangles (`Neutral` fill, `"dashed"` stroke, opacity: 30) as lane boundaries - Lane label as free-standing text at top-left of lane (not bound to rectangle), 28px font - Elements flow left-to-right within lanes - Arrows cross lanes for handoffs ## Section-by-Section Construction For diagrams with **10+ elements**, do NOT generate the entire JSON at once. Build in sections: 1. **Plan all sections first** β€” list element IDs, positions, and cross-section bindings 2. **Write section 1** β€” create the file with initial elements 3. **Append section 2** β€” read the file, add new elements to the `elements` array 4. **Repeat** β€” continue until all sections are done 5. **Final pass** β€” verify all `boundElements` and `startBinding`/`endBinding` references are consistent Namespace element seeds by section (100xxx, 200xxx, 300xxx) to avoid collisions. ## Export ### Option A: Kroki API (SVG only β€” zero install) ```bash # SVG via Kroki API curl -s -X POST https://kroki.io/excalidraw/svg \ -H "Content-Type: text/plain" \ --data-binary "@diagram.excalidraw" \ -o diagram.svg # Via local Kroki Docker (offline) curl -s -X POST http://localhost:8000/excalidraw/svg \ -H "Content-Type: text/plain" \ --data-binary "@diagram.excalidraw" \ -o diagram.svg ``` ### Option B: Local CLI (PNG + SVG) ```bash # PNG at 2x scale, with background baked in (recommended) excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.png -f png -s 2 -b true # PNG at 1x scale excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.png -f png -s 1 -b true # SVG excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.svg -f svg -s 1 -b true ``` **Required flags:** `-f` (format: `png` or `svg`) and `-s` (scale: `1`, `2`, or `3`). **Optional flags:** `-b true` bakes the `viewBackgroundColor` into the image β€” **the export is transparent by default**, so omit `-b` (or pass `-b false`) only when you want a transparent background. `-d true` exports dark mode; `-e true` embeds the scene so the PNG/SVG reopens as an editable drawing in excalidraw.com. (Long forms also work: `--background`, `--dark-mode`, `--embed-scene`, `--format`, `--scale`, `--input`, `--output`.) ## Verify the Render **You cannot judge a diagram from its JSON.** The JSON can look perfect while the image has clipped text, overlapping boxes, or an arrow slicing through a shape. After exporting, *look at the result and fix it* β€” this is the single highest-leverage step. 1. **Render to PNG** (the image must be viewable β€” PNG, not SVG, even if the user ultimately wants SVG): ```bash excalidraw-brute-export-cli -i diagram.excalidraw -o /tmp/check.png -f png -s 2 -b true ``` View `/tmp/check.png` (Claude can read PNGs directly). *Visual audit needs the local CLI; with Kroki-only (SVG), fall back to the structural checks below.* 2. **Audit the image:** | Look for | Fix | |----------|-----| | Text clipped / overflowing its shape | Widen the shape (`max(160, charCount * 9)`, Γ—2 for CJK) or pre-wrap with `\n` | | Boxes or labels overlapping | Re-space using the Spacing Reference (β‰₯40px gap) | | Arrow cutting straight through a shape | Move endpoints to the shape borders, not centers | | Arrow invisible β€” only its label shows | Shrink the label `width` to fit the text | | Element off-canvas or floating with no connection | Reposition / connect it | | **Isomorphism Test:** mentally delete all text β€” does the structure alone still convey the idea? | If not, the *layout* is wrong, not the labels β€” restructure | 3. **Fix the JSON and re-export.** Repeat until clean β€” typically 1–3 passes. Skip only for trivial 2–3 element diagrams. ## Review Loop Verify-the-render fixes *defects*; the review loop incorporates *the user's* wishes. After the render is clean, show it and collect feedback, then apply the **minimal `.excalidraw` edit** for each request and re-export: | User request | Edit action | |---|---| | Change a label | Edit the `text` (or the bound label element) | | Change a color | Update `backgroundColor` / `strokeColor` on the element | | Add / remove an element | Append or delete the element (fix any `boundElements` / binding refs) | | Move / resize | Update `x` / `y` / `width` / `height` | | Restructure / re-route | Re-apply the pattern's spacing & routing rules, or regenerate | - Overwrite the same `diagram.excalidraw` / output file each round β€” don't create `v1`, `v2`, … - Re-run **Verify the Render** after each edit (a change can introduce a new clip / overlap). - **Safety valve:** after 5 rounds, suggest the user fine-tune in [excalidraw.com](https://excalidraw.com) β€” the output preserves arrow binding, so it opens fully editable. ## Anti-Patterns **Never put `text` on large background/zone rectangles.** Excalidraw centers text in the middle of the shape, overlapping contained elements. Instead, use a free-standing `text` element positioned at the top of the zone. **Avoid cross-zone arrows.** Long diagonal arrows create visual spaghetti. Route arrows within zones or along zone edges. If a cross-zone connection is unavoidable, route it along the perimeter. **Use arrow labels sparingly.** Bind labels to the arrow (see **Arrow labels**) so the line is masked behind the text instead of striking through it β€” but keep the label `width` to the text, never the arrow length. Keep labels to ≀12 characters and ensure β‰₯120px clear space between connected shapes. Omit labels when the connection meaning is obvious from context. **Don't use filled backgrounds on containers that hold other elements.** Use `opacity: 30` (or 25-40 range) for zone/container rectangles so contained elements remain visible. **Always set explicit `strokeColor` on text elements.** Text `strokeColor` is the rendered text color. If omitted, text may inherit the parent shape's background color and become invisible. Use `#1e293b` (title), `#334155` (label), or `#64748b` (description) from the text color palette. ## Common Mistakes | Mistake | Fix | |---------|-----| | Kroki returns HTTP 400 | Send `-H "Content-Type: text/plain"` (NOT `application/json`, which Kroki reads as a `{"diagram_source": ...}` wrapper and rejects); ensure valid JSON with `"type": "excalidraw"` and `"elements"` array | | Kroki only outputs SVG | Use local CLI (`excalidraw-brute-export-cli`) for PNG | | Export fails with "Missing required flag" | Always pass `-f png` and `-s 2` | | Export fails with "Executable doesn't exist" | Run `npx playwright install firefox` | | macOS: timeout waiting for file chooser | Apply the macOS Meta patch above | | Arrow `points` not relative to origin | `points` always start at `[0,0]` | | Missing `id` on elements | Use descriptive string IDs per element | | Overlapping elements | Use spacing reference table; minimum 40px gap | | Arrows not interactive in excalidraw.com | Add `boundElements` to shapes referencing all bound arrows/text | | Arrow/line cuts straight through the shapes | Compute endpoints at the shape borders, not centers β€” bindings don't clip the static export | | Arrow invisible β€” only its label shows | Bound label `width` spans the whole arrow and masks the line; set label `width` to fit the text (`charCount * 9`) | | Exported PNG/SVG has no background | CLI export is transparent by default; pass `-b true` to bake in `viewBackgroundColor` | | Text not centered in shape | Set `containerId` on text AND add text to shape's `boundElements` | | All text same size | Use font size hierarchy: 28 β†’ 24 β†’ 20 β†’ 16 β†’ 14 | | Diagram looks monotone | Apply semantic colors from the palette, follow 60-30-10 rule | | Text invisible / same color as background | Always set `strokeColor` on text elements to a dark color (`#1e293b`, `#334155`, or `#64748b`) | | Text overlaps inside zone/container | Don't bind text to zone rectangles; use free-standing text at top | | Text truncated in shapes | Use width formula: `max(160, charCount * 9)`, double for CJK | | `boundElements: []` causes issues | Use `null` for empty boundElements, never `[]` |