
Drawio Skill
- 6.7k installs
- 29 repo stars
- Updated August 2, 2026
- agents365-ai/365-skills
drawio-skill is an agent skill for draw.io XML diagram generation and local PNG SVG PDF export via desktop CLI.
About
The drawio-skill generates .drawio XML and exports PNG, SVG, PDF, or JPG locally through the native draw.io desktop CLI without browser automation. Use it for polished precise diagrams including architecture, network topology, strict UML, ERD, and ML model figures when custom styling, swimlanes, or embeddable exports matter. PNG, SVG, and PDF support --embed-diagram so exported files retain editable XML, signaled with double extensions like name.drawio.png. Route casual whiteboard looks to excalidraw or tldraw, diagrams-as-code in git to mermaid or plantuml, and freeform sketching to tldraw per skill guidance. macOS installs via brew install --cask drawio or GitHub releases; resolve the draw.io binary path before running export commands. Critical XML rule: every edge mxCell must include mxGeometry relative=1 child or diagrams break. The skill covers shape vocabulary, swimlanes, ML architecture templates, and CLI export flags for agent-generated diagrams.
- Generates draw.io XML and exports via native desktop CLI without browser automation.
- PNG SVG PDF JPG exports support --embed-diagram for editable double-extension files.
- Routes whiteboard style to excalidraw and git-friendly diagrams to mermaid or plantuml.
- Requires mxGeometry child on every edge mxCell or diagrams fail validation.
- Install draw.io desktop via brew cask or jgraph drawio-desktop releases on macOS.
Drawio Skill by the numbers
- 6,663 all-time installs (skills.sh)
- +417 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #63 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
drawio-skill capabilities & compatibility
- Capabilities
- draw.io xml generation · desktop cli png svg pdf export · embed diagram double extension exports · diagram type routing versus mermaid excalidraw · ml and architecture template patterns
- Use cases
- ui design · documentation · frontend
npx skills add https://github.com/agents365-ai/365-skills --skill drawio-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.7k |
|---|---|
| repo stars | ★ 29 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | agents365-ai/365-skills ↗ |
How do I create precise architecture or UML diagrams exportable as embedded editable draw.io files?
Create architecture, flowchart, UML, ERD, and ML diagram draw.io XML files and export PNG, SVG, PDF, or JPG via native draw.io desktop CLI.
Who is it for?
Developers needing polished diagrams with draw.io shape vocabulary and local CLI export.
Skip if: Skip for simple mermaid in markdown; use mermaid-diagrams instead per skill routing table.
When should I use this skill?
User requests architecture diagrams, flowcharts, UML, ERD, or draw.io exports with embed-diagram.
What you get
Valid draw.io XML with CLI-exported PNG SVG or PDF artifacts and embed-diagram when needed.
- draw.io XML files
- Exported PNG SVG PDF diagrams
By the numbers
- Supports PNG SVG PDF JPG export formats
- embed-diagram uses double extensions like name.drawio.png
Files
Draw.io Diagrams
Overview
Generate .drawio XML files and export to PNG/SVG/PDF/JPG locally using the native draw.io desktop app CLI.
Supported formats: PNG, SVG, PDF, JPG — no browser automation needed.
PNG, SVG, and PDF exports support --embed-diagram (-e) — the exported file contains the full diagram XML, so opening it in draw.io recovers the editable diagram. Use double extensions (name.drawio.png) to signal embedded XML.
When to use / when NOT to use
Use this skill for: polished, precise diagrams (architecture, network, strict UML, ERD), anything needing solid opaque fills, 10,000+ stock/branded shapes, swimlanes, or custom geometry, exported as editable PNG/SVG/PDF.
Do NOT use it — route elsewhere — for:
- A casual hand-drawn / whiteboard look → excalidraw or tldraw.
- Diagrams-as-code that live in git / render in Markdown → mermaid (general) or plantuml (UML).
- Freeform infinite-canvas sketching or freehand strokes → tldraw.
Bundled resources
When the workflow references one of these, read it on demand — none of them need to be in context up front.
| File | Read it when |
|---|---|
references/diagram-types.md | The user names a specific diagram type (ERD, UML class, sequence, architecture, ML/DL, flowchart) |
references/shapes.md + scripts/shapesearch.py | The diagram needs a specific shape — a cloud icon (AWS/Azure/GCP), Cisco/Kubernetes/network symbol, UML/BPMN/ER/electrical/P&ID element — or any time you'd otherwise guess a style= string. shapesearch.py "<keywords>" returns the exact official style for 10k+ shapes |
scripts/aiicons.py | The diagram involves an AI/LLM brand (OpenAI, Claude, Gemini, Mistral, Llama, HuggingFace, Ollama, LangChain, …) — aiicons.py "<brand>" returns a draw.io image style for the brand logo (lobe-icons via CDN; --embed to inline). draw.io has no built-in AI logos. See references/shapes.md → "AI / LLM brand logos" |
references/style-presets.md | The user asks to learn / save / list / set-default / delete a style preset, or you've resolved an active preset and need the application rules |
references/style-extraction.md | You're inside the Learn flow and need the extraction procedure (called from style-presets.md) |
references/troubleshooting.md | An export fails, vision rejects a PNG, or a rendering looks wrong |
scripts/repair_png.py | After every -e PNG export — fixes draw.io's truncated IEND chunk (issue #8) |
scripts/encode_drawio_url.py | The CLI is unavailable and you need a browser-fallback diagrams.net URL (--edit for an editable editor URL) |
references/autolayout.md | The diagram is large or layout-heavy (dependency/call graph, code structure, >~15 nodes) and you want Graphviz to place nodes + route edges instead of hand-placing coordinates |
scripts/pyimports.py · jsimports.py · goimports.py · rustimports.py | The user wants to visualize a Python, JS/TS, Go, or Rust project structure — extracts the import graph (transitive-reduced, optional --group containers, nested by sub-package) for autolayout |
scripts/pyclasses.py | The user wants a Python class hierarchy / class diagram — extracts classes + inheritance edges (boxed by module with --group) for autolayout |
scripts/validate.py | You generated a .drawio (especially via autolayout or for a large hand-placed diagram) and want a fast deterministic structural lint (dangling edges, dup/reserved ids, broken parents, overlaps) before the vision self-check |
Prerequisites
The draw.io desktop app must be installed and the CLI accessible:
macOS sandbox / sandbox isolation note (e.g., codex.app): In some sandboxed macOS environments, invoking the draw.io desktop CLI (even drawio --version) can crash the draw.io process or produce no output. If that happens, treat the CLI as unavailable in this sandbox isolation — do not keep retrying inside the sandbox. Prefer a non-sandboxed host environment (outside sandbox isolation) for any CLI export work, or use the browser fallback / XML-only outputs.
# macOS (Homebrew — recommended; CLI binary is `drawio`, not `draw.io`)
brew install --cask drawio
drawio --version
# macOS (full path if not in PATH)
/Applications/draw.io.app/Contents/MacOS/draw.io --version
# Windows
"C:\Program Files\draw.io\draw.io.exe" --version
# Linux
drawio --versionInstall draw.io desktop if missing:
- macOS:
brew install --cask drawioor download from https://github.com/jgraph/drawio-desktop/releases - Windows: download installer from https://github.com/jgraph/drawio-desktop/releases
- Linux: download
.deb/.rpmfrom https://github.com/jgraph/drawio-desktop/releases — do not use snap (AppArmor sandbox denies secrets/keyring on servers, causes crash)
Workflow
Before starting the workflow, assess whether the user's request is specific enough. If key details are missing, ask 1-3 focused questions:
- Diagram type — which preset? (ERD, UML, Sequence, Architecture, ML/DL, Flowchart, or general)
- Output format — PNG (default), SVG, PDF, or JPG?
- Output location — default is the user's working dir; honor any explicit path the user gives (e.g. "put it in
./artifacts/"). Don't ask if they didn't mention one. - Scope/fidelity — how many components? Any specific technologies or labels?
Skip clarification if the request already specifies these details or is clearly simple (e.g., "draw a flowchart of X").
Step 0 — Resolve active preset. Determine which (if any) user-defined style preset applies to this generation.
- Scan the user's message for a phrase that clearly names a style preset: "use my
<name>style", "with my<name>style", "in<name>mode", "in the style of<name>". A barewith <name>does not count — "draw a diagram with redis" names a component, not a style. If a clear match is found → active preset =<name>. - Else, check
~/.drawio-skill/styles/for any file with"default": true. If found → active preset = that one. - Else → no preset active; fall through to the built-in color/shape/edge conventions for the rest of the workflow.
Load the preset JSON from ~/.drawio-skill/styles/<name>.json, falling back to <this-skill-dir>/styles/built-in/<name>.json. If the named preset exists in neither location, tell the user the name is unknown, list the available presets (user dir + built-in), and stop — do not silently fall back to defaults.
When a preset loads successfully, mention it in the first line of the reply: "Using preset `<name>` (confidence: `<level>`)." See the Applying a preset subsection below for how the preset changes color/shape/edge/font decisions.
1. Check deps — resolve which name the binary has on this system and use that name verbatim in every subsequent command in this workflow. Try in order: (a) drawio --version (the canonical name for Homebrew cask, jgraph .deb/.rpm, Arch AUR), (b) draw.io --version (older builds, some custom symlinks, some distro packages), (c) macOS .app direct: /Applications/draw.io.app/Contents/MacOS/draw.io --version, (d) Windows: "C:\Program Files\draw.io\draw.io.exe" --version. The first one that prints a version is your binary; remember the exact path/name and substitute it for drawio in every export command below. Do not copy the example commands verbatim if your binary is named differently — the examples use drawio only because it's the most common. On macOS-Homebrew, drawio is just a thin wrapper script that execs /Applications/draw.io.app/Contents/MacOS/draw.io — they run the same engine, so candidate (c) is only needed when the drawio wrapper is absent (e.g. the app was installed by drag-and-drop without the cask). 2. Plan — identify shapes, relationships, layout (LR or TB), group by tier/layer 3. Generate — write .drawio XML file to disk. Hand-place coordinates for small/styled diagrams. For large or layout-heavy diagrams (dependency/call graphs, code structure, >~15 nodes), don't hand-place — describe the graph as JSON and run python3 <this-skill-dir>/scripts/autolayout.py graph.json -o <name>.drawio to compute node positions + orthogonal edge routing via Graphviz (see references/autolayout.md). For a Python / JS-TS / Go / Rust project, the matching importer (scripts/pyimports.py, jsimports.py, goimports.py, or rustimports.py) extracts the import graph (transitive-reduced; add --group to box modules by sub-package, nested for deep trees) ready for autolayout; for a Python class hierarchy, scripts/pyclasses.py extracts classes + inheritance instead. After generating any .drawio, run python3 <this-skill-dir>/scripts/validate.py <name>.drawio for a fast structural lint (dangling edges, dup ids, overlaps) before exporting. Default output dir is the user's working dir; if the user specified an output path or directory (e.g. ./artifacts/, docs/images/), use that instead — mkdir -p the target dir first. Apply the same dir choice to PNG/SVG/PDF exports in steps 4 and 7. 4. Export draft — run CLI to produce a preview PNG. Do NOT pass `-e` at this step — the embedded zTXt mxGraphModel chunk it adds causes vision APIs (Claude included) to return 400 "Could not process image" in step 5. Cap the preview width with `--width 2000` (not `-s 2`) — Claude's vision API rejects images larger than 2576×2576px with "Unable to resize image — dimensions exceed the 2576x2576px limit", and -s 2 on a medium-or-larger diagram easily overshoots that ceiling. Save the clean preview as <name>.png (single extension). Embedding and full-resolution scale are for the final export only (step 7). 5. Self-check — use the agent's built-in vision capability to read the exported PNG, catch obvious issues, auto-fix before showing user (requires a vision-enabled model such as Claude Sonnet/Opus). If reading the PNG returns a 400 / "Could not process image" error, you almost certainly exported with -e by mistake — re-export without -e and retry once. If it still fails, skip self-check and continue to step 6. 6. Review loop — show image to user, collect feedback, apply targeted XML edits, re-export, repeat until approved 7. Final export — re-export the approved version to all requested formats. Use -e here (PNG/SVG/PDF) so the deliverable stays editable in draw.io; save as <name>.drawio.png to signal embedded XML. For PNG with `-e`, run `python3 <this-skill-dir>/scripts/repair_png.py <name>.drawio.png` immediately after — draw.io's CLI truncates the IEND chunk in -e PNG output (8 bytes missing), producing a corrupt file that vision APIs and strict PNG decoders reject (issue #8). Report file paths.
If `drawio --version` crashes or prints nothing (common in restricted macOS sandbox isolation like codex.app):
- Do not keep retrying CLI invocations inside the sandbox.
- Skip steps 4, 5, 6, and 7 (CLI export + PNG-based review) and use Browser fallback (
scripts/encode_drawio_url.py) or deliver the.drawioXML only. - If the user needs PNG/SVG/PDF outputs, ask them to run the export commands in a non-sandboxed host environment (outside sandbox isolation) and share the resulting files.
Escalation rule:
- If the binary exists on PATH (or known app path exists) but execution fails with abnormal exit, empty output, Electron startup failure, display/session error, or likely sandbox restriction, prefer one escalated retry before falling back.
- If the binary is missing entirely, do not escalate just to search more aggressively; go to install guidance or fallback.
Step 5: Self-Check
After exporting the draft PNG, use the agent's vision capability (e.g., Claude's image input) to read the image and check for these issues before showing the user. If the agent does not support vision, skip self-check and show the PNG directly.
Important: the draft PNG read here must have been exported without -e. Draw.io's -e flag emits a PNG with a truncated IEND chunk (8 bytes of type+CRC missing) that the Anthropic vision API rejects with 400 "Could not process image" (issue #8). The simplest fix for the preview step is to skip -e entirely; the final export in step 7 keeps -e and runs the repair snippet. If you see the 400 error here, re-export without -e and retry once; if it still fails (any other reason), skip self-check and proceed to step 6.
| Check | What to look for | Auto-fix action |
|---|---|---|
| Overlapping shapes | Two or more shapes stacked on top of each other | Shift shapes apart by ≥200px |
| Clipped labels | Text cut off at shape boundaries | Increase shape width/height to fit label |
| Missing connections | Arrows that don't visually connect to shapes | Verify source/target ids match existing cells |
| Off-canvas shapes | Shapes at negative coordinates or far from the main group | Move to positive coordinates near the cluster |
| Edge-shape overlap | An edge/arrow visually crosses through an unrelated shape | Add waypoints (<Array as="points">) to route around the shape, or increase spacing between shapes |
| Stacked edges | Multiple edges overlap each other on the same path | Distribute entry/exit points across the shape perimeter (use different exitX/entryX values) |
- Max 2 self-check rounds — if issues remain after 2 fixes, show the user anyway
- Re-export after each fix and re-read the new PNG
Step 6: Review Loop
After self-check, show the exported image and ask the user for feedback.
Targeted edit rules — for each type of feedback, apply the minimal XML change:
| User request | XML edit action |
|---|---|
| Change color of X | Find mxCell by value matching X, update fillColor/strokeColor in style |
| Add a new node | Append a new mxCell vertex with next available id, position near related nodes |
| Remove a node | Delete the mxCell vertex and any edges with matching source/target |
| Move shape X | Update x/y in the mxGeometry of the matching mxCell |
| Resize shape X | Update width/height in the mxGeometry of the matching mxCell |
| Add arrow from A to B | Append a new mxCell edge with source/target matching A and B ids |
| Change label text | Update the value attribute of the matching mxCell |
| Change layout direction | Full regeneration — rebuild XML with new orientation |
Rules:
- For single-element changes: edit existing XML in place — preserves layout tuning from prior iterations
- For layout-wide changes (e.g., swap LR↔TB, "start over"): regenerate full XML
- Overwrite the same
{name}.png(no-e) each iteration — do not createv1,v2,v3files.-eis reserved for the final export in step 7. - After applying edits, re-export and show the updated image
- Loop continues until user says approved / done / LGTM
- Safety valve: after 5 iteration rounds, suggest the user open the
.drawiofile in draw.io desktop for fine-grained adjustments
Step 7: Final Export
Once the user approves:
- Export to all requested formats (PNG, SVG, PDF, JPG) — default to PNG if not specified
- Report file paths for both the
.drawiosource file and exported image(s) - Auto-launch: offer to open the
.drawiofile in draw.io desktop for fine-tuning —open diagram.drawio(macOS),xdg-open(Linux),start(Windows) - Confirm files are saved and ready to use
Style Presets
A style preset is a named JSON file capturing a user's visual preferences (palette, shapes, font, edges). When active, it fully replaces the built-in color/shape conventions in this skill.
Lookup order when SKILL.md's Step 0 resolves a preset name: 1. ~/.drawio-skill/styles/<name>.json — user presets (survive git pull) 2. <this-skill-dir>/styles/built-in/<name>.json — shipped built-ins (default, corporate, handdrawn)
Always lowercase the user-provided name before any file operation — the schema enforces lowercase.
For everything else — Learn flow (extracting a preset from a file), management ops (list/default/delete/rename), application rules (color lookup, shape keywords, edges, fonts, extras, interaction with diagram-type presets), and validation — read `references/style-presets.md`. It's only needed when the user invokes those flows or when an active preset must be applied to the current generation.
Draw.io XML Structure
File skeleton
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="drawio" version="26.0.0">
<diagram name="Page-1">
<mxGraphModel>
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- user shapes start at id="2" -->
</root>
</mxGraphModel>
</diagram>
</mxfile>Rules:
id="0"andid="1"are required root cells — never omit them- User shapes start at
id="2"and increment sequentially - All shapes have
parent="1"(unless inside a container — then use container's id) - All text uses
html=1in style for proper rendering - Never use `--` inside XML comments — it's illegal per XML spec and causes parse errors
- Escape special characters in attribute values:
&,<,>," - Multi-line text in labels: use

for line breaks insidevalueattributes (not literal\n). Example:value="Line 1
Line 2"
Shape types (vertex)
| Style keyword | Use for |
|---|---|
rounded=0 | plain rectangle (default) |
rounded=1 | rounded rectangle — services, modules |
ellipse; | circles/ovals — start/end, databases |
rhombus; | diamond — decision points |
shape=mxgraph.aws4.resourceIcon; | AWS icons |
shape=cylinder3; | cylinder — databases |
swimlane; | group/container with title bar |
For vendor/branded icons (AWS/Azure/GCP/Cisco/Kubernetes) and any non-trivial shape, don't guess the shape=mxgraph.* name — a wrong name renders as a blank box. Run python3 <this-skill-dir>/scripts/shapesearch.py "<keywords>" to get the exact official style + size, or see references/shapes.md for the hand-writable cheatsheet. For AI/LLM brand logos (OpenAI, Claude, Gemini, …), which draw.io has none of, use python3 <this-skill-dir>/scripts/aiicons.py "<brand>".
Required properties
<!-- Rectangle / rounded box -->
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="160" height="60" as="geometry" />
</mxCell>
<!-- Cylinder (database) -->
<mxCell id="3" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="350" y="100" width="120" height="80" as="geometry" />
</mxCell>
<!-- Diamond (decision) -->
<mxCell id="4" value="Check?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="100" y="220" width="160" height="80" as="geometry" />
</mxCell>Containers and groups
For architecture diagrams with nested elements, use draw.io's parent-child containment — do not just place shapes on top of larger shapes.
| Type | Style | When to use |
|---|---|---|
| Group (invisible) | group;pointerEvents=0; | No visual border needed, container has no connections |
| Swimlane (titled) | swimlane;startSize=30; | Container needs a visible title bar, or container itself has connections |
| Custom container | Add container=1;pointerEvents=0; to any shape | Any shape acting as a container without its own connections |
Key rules:
- Add
pointerEvents=0;to container styles that should not capture connections between children - Children set
parent="containerId"and use coordinates relative to the container
<!-- Swimlane container -->
<mxCell id="svc1" value="User Service" style="swimlane;startSize=30;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="300" height="200" as="geometry"/>
</mxCell>
<!-- Child inside container — coordinates relative to parent -->
<mxCell id="api1" value="REST API" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="svc1">
<mxGeometry x="20" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="db1" value="Database" style="shape=cylinder3;whiteSpace=wrap;html=1;" vertex="1" parent="svc1">
<mxGeometry x="160" y="40" width="120" height="60" as="geometry"/>
</mxCell>Connector (edge)
CRITICAL: Every edge mxCell must contain a <mxGeometry relative="1" as="geometry" /> child element. Self-closing edge cells (<mxCell ... edge="1" ... />) are invalid and will not render. Always use the expanded form.
<!-- Directed arrow — always include rounded, orthogonalLoop, jettySize for clean routing -->
<mxCell id="10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Arrow with label + explicit entry/exit points to control direction -->
<mxCell id="11" value="HTTP/REST" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Arrow with waypoints — use when edge must route around other shapes -->
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="500" y="50" />
</Array>
</mxGeometry>
</mxCell>Edge style rules:
- Animated connectors: add
flowAnimation=1;to any edge style to show a moving dot animation along the arrow. Works in SVG export and draw.io desktop — ideal for data-flow and pipeline diagrams. Example:style="edgeStyle=orthogonalEdgeStyle;flowAnimation=1;rounded=1;..." - Always include
rounded=1;orthogonalLoop=1;jettySize=auto— these enable smart routing that avoids overlaps - Pin
exitX/exitY/entryX/entryYon every edge when a node has 2+ connections — distributes lines across the shape perimeter - Add
<Array as="points">waypoints when an edge must detour around an intermediate shape - Leave room for arrowheads: the final straight segment between the last bend and the target shape must be ≥20px long. If too short, the arrowhead overlaps the bend and looks broken. Fix by increasing node spacing or adding explicit waypoints
Distributing connections on a shape
When multiple edges connect to the same shape, assign different entry/exit points to prevent stacking:
| Position | exitX/entryX | exitY/entryY | Use when |
|---|---|---|---|
| Top center | 0.5 | 0 | connecting to node above |
| Top-left | 0.25 | 0 | 2nd connection from top |
| Top-right | 0.75 | 0 | 3rd connection from top |
| Right center | 1 | 0.5 | connecting to node on right |
| Bottom center | 0.5 | 1 | connecting to node below |
| Left center | 0 | 0.5 | connecting to node on left |
Rule: if a shape has N connections on one side, space them evenly (e.g., 3 connections on bottom → exitX = 0.25, 0.5, 0.75)
Color palette (fillColor / strokeColor)
Used only when no preset is active (see "Applying a preset" above).
| Color name | fillColor | strokeColor | Use for |
|---|---|---|---|
| Blue | #dae8fc | #6c8ebf | services, clients |
| Green | #d5e8d4 | #82b366 | success, databases |
| Yellow | #fff2cc | #d6b656 | queues, decisions |
| Orange | #ffe6cc | #d79b00 | gateways, APIs |
| Red/Pink | #f8cecc | #b85450 | errors, alerts |
| Grey | #f5f5f5 | #666666 | external/neutral |
| Purple | #e1d5e7 | #9673a6 | security, auth |
Layout tips
Spacing — scale with complexity:
| Diagram complexity | Nodes | Horizontal gap | Vertical gap |
|---|---|---|---|
| Simple | ≤5 | 200px | 150px |
| Medium | 6–10 | 280px | 200px |
| Complex | >10 | 350px | 250px |
Routing corridors: between shape rows/columns, leave an extra ~80px empty corridor where edges can route without crossing shapes. Never place a shape in a gap that edges need to traverse.
Grid alignment: snap all x, y, width, height values to multiples of 10 — this ensures shapes align cleanly on draw.io's default grid and makes manual editing easier.
General rules:
- Plan a grid before assigning x/y coordinates — sketch node positions on paper/mentally first
- Group related nodes in the same horizontal or vertical band
- Use
swimlanecells for logical grouping with visible borders - Place heavily-connected "hub" nodes centrally so edges radiate outward instead of crossing
- To force straight vertical connections, pin entry/exit points explicitly on edges:
exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0
- Always center-align a child node under its parent (same center x) to avoid diagonal routing
- Event bus pattern: place Kafka/bus nodes in the center of the service row, not below — services on either side can reach it with short horizontal arrows (
exitX=1left side,exitX=0right side), eliminating all line crossings - Horizontal connections (
exitX=1orexitX=0) never cross vertical nodes in the same row; use them for peer-to-peer and publish connections
Avoiding edge-shape overlap:
- Before finalizing coordinates, trace each edge path mentally — if it must cross an unrelated shape, either move the shape or add waypoints
- For tree/hierarchical layouts: assign nodes to layers (rows), connect only between adjacent layers to minimize crossings
- For star/hub layouts: place the hub center, satellites around it — edges stay short and radial
- When an edge must span multiple rows/columns, route it along the outer corridor, not through the middle of the diagram
Export
Commands
There are two export modes:
- Preview / self-check (step 4 of the workflow) — no
-e. Outputdiagram.png. Required for vision self-check; using-ehere triggers a 400 "Could not process image" error from the vision API (issue #8). - Final / deliverable (step 7) — pass
-e. Outputdiagram.drawio.png. The embedded XML keeps the file editable in draw.io.
All commands below writedrawioas a placeholder for the binary you resolved in Step 1. If your binary is on PATH asdraw.io(with dot — some older or distro-packaged installs), substitutedraw.iothroughout. If only the macOS.appor Windows.exeis available, use the full path variant shown a few lines down.
# Preview PNG (use this in step 4, before self-check) — NO -e, width-capped to stay under vision's 2576px ceiling
drawio -x -f png --width 2000 -o diagram.png input.drawio
# Final PNG (step 7, after user approval) — WITH -e, double extension
drawio -x -f png -e -s 2 -o diagram.drawio.png input.drawio
# macOS — full path (if not in PATH); preview / final variants
/Applications/draw.io.app/Contents/MacOS/draw.io -x -f png --width 2000 -o diagram.png input.drawio
/Applications/draw.io.app/Contents/MacOS/draw.io -x -f png -e -s 2 -o diagram.drawio.png input.drawio
# Windows
"C:\Program Files\draw.io\draw.io.exe" -x -f png -e -s 2 -o diagram.drawio.png input.drawio
# Linux (headless — requires xvfb-run; on servers add HOME and --disable-gpu)
export HOME=${HOME:-/tmp}
xvfb-run -a --server-args="-screen 0 1280x1024x24" \
drawio -x -f png -e -s 2 -o diagram.drawio.png input.drawio --disable-gpu
# Running as root (CI / Docker)? Append --no-sandbox AT THE END (placing it earlier makes drawio treat it as the input filename)
# SVG export (final — -e is safe; SVG is text)
drawio -x -f svg -e -o diagram.svg input.drawio
# PDF export (final)
drawio -x -f pdf -e -o diagram.pdf input.drawio
# Custom output directory (e.g. CI artifacts dir) — create if missing, then export there
mkdir -p ./artifacts && drawio -x -f png -e -s 2 -o ./artifacts/diagram.drawio.png input.drawioPost-export PNG repair (required after -e PNG export)
draw.io CLI truncates the IEND chunk when emitting -e PNGs — the file ends with the 4-byte IEND length field but the IEND type + CRC (8 bytes) are missing. Result: vision APIs return 400 "Could not process image" and strict PNG decoders error out. SVG/PDF are unaffected.
Run this immediately after every -e PNG export:
python3 <this-skill-dir>/scripts/repair_png.py diagram.drawio.pngThe script's endswith(IEND) guard makes it a no-op once draw.io fixes the bug upstream — safe to run unconditionally.
Key flags:
-x— export mode (required)-f— format:png,svg,pdf,jpg-e— embed diagram XML in output (PNG, SVG, PDF) — exported file remains editable in draw.io. Skip for the preview PNG used in step 5 self-check —-ePNGs have a truncated IEND chunk that vision APIs reject (issue #8). For final PNG export, keep-eand runscripts/repair_png.py(see Post-export PNG repair). SVG/PDF unaffected.-s— scale:1,2,3(2 recommended for final PNG; do NOT use for the step-4 preview — see--width)--width <px>— target width in pixels (no short form;-wdoes not exist and silently breaks the input-file parser). Use--width 2000for the step-4 preview to keep the PNG under Claude's 2576×2576 vision ceiling. There's also a--height <px>flag for tall-narrow diagrams. Don't combine--widthwith-s.-o— output file path; accepts any directory (e.g../artifacts/diagram.drawio.png) —mkdir -pthe target dir first. Use.drawio.pngdouble extension when embedding.-b— border width around diagram (default: 0, recommend 10)-t— transparent background (PNG only)--page-index 0— export specific page (default: all)
Browser fallback (no CLI needed)
When the draw.io desktop CLI is unavailable, generate a client-side URL:
python3 <this-skill-dir>/scripts/encode_drawio_url.py input.drawio # read-only viewer
python3 <this-skill-dir>/scripts/encode_drawio_url.py --edit input.drawio # opens in the editorDefault prints a https://viewer.diagrams.net/...#R… viewer URL; --edit prints a https://app.diagrams.net/...#create=… URL that opens straight into the editable editor. Either way the diagram XML is encodeURIComponent-encoded, deflate-compressed, and base64'd into the URL fragment — the fragment (after #) is never sent to the server, so nothing is uploaded. The encodeURIComponent step is mandatory: without it, any diagram containing a literal % or non-ASCII (e.g. CJK) label makes the browser throw "URI malformed" and the diagram never opens.
Open the URL with open "$URL" (macOS) / xdg-open "$URL" (Linux). On WSL2 / Windows, cmd.exe drops the #fragment — write a .url shortcut file and open that instead (see references/troubleshooting.md → "WSL2 / Windows specifics").
Fallback chain
When tools are unavailable, degrade gracefully:
| Scenario | Behavior |
|---|---|
| draw.io CLI missing, Python available | Use browser fallback (diagrams.net URL) |
| draw.io CLI missing, Python missing | Generate .drawio XML only; instruct user to open in draw.io desktop or diagrams.net manually |
| draw.io CLI crashes / no output in macOS sandbox isolation | Treat CLI as unavailable in-sandbox; use browser fallback / XML-only; ask user to run CLI exports in a non-sandboxed host environment |
| Vision unavailable for self-check | Skip self-check (step 5); proceed directly to showing user the exported PNG |
| Export fails (Chromium/display issues) | On Linux, retry with xvfb-run -a; if still failing, deliver .drawio XML and suggest manual export |
| Export fails on Linux server (headless) | Try in order: (1) xvfb-run -a, (2) append --no-sandbox at the very end if root, (3) add --disable-gpu, (4) export HOME=/tmp, (5) install apt deps (libgtk-3-0 libnotify4 libnss3 libgbm1 libasound2t64 etc.), (6) fall back to tomkludy/drawio-renderer Docker (REST API for headless export) |
Checking if drawio is in PATH
# Prefer the Homebrew / Linux-package binary name (no dot)
if command -v drawio &>/dev/null; then
DRAWIO="drawio"
# Fall back to the dot-named binary (older installs, manual symlinks)
elif command -v draw.io &>/dev/null; then
DRAWIO="draw.io"
# macOS .app bundle (binary inside the bundle keeps the dot)
elif [ -f "/Applications/draw.io.app/Contents/MacOS/draw.io" ]; then
DRAWIO="/Applications/draw.io.app/Contents/MacOS/draw.io"
# WSL2: the CLI is the Windows desktop exe, reached via /mnt/c (note the space)
elif grep -qi microsoft /proc/version 2>/dev/null && [ -f "/mnt/c/Program Files/draw.io/draw.io.exe" ]; then
DRAWIO="/mnt/c/Program Files/draw.io/draw.io.exe"
else
echo "drawio not found — install from https://github.com/jgraph/drawio-desktop/releases (Homebrew: brew install --cask drawio)"
fiOn WSL2 / native Windows, opening exported files and browser-fallback URLs needs path conversion + a .url-file workaround (cmd.exe drops URL #fragments) — see the "WSL2 / Windows specifics" section in references/troubleshooting.md.
Common Mistakes
When something looks wrong (export fails, vision rejects a PNG, layout broken, edges misroute), see references/troubleshooting.md for a row-by-row mistake → fix table.
Diagram Type Presets
When the user requests a specific diagram type, read references/diagram-types.md for the matching preset (shapes, edges, layout direction). Pick by user phrasing:
| User says | Section in references/diagram-types.md |
|---|---|
| "ER diagram", "schema diagram", "data model" | ERD |
| "UML class diagram", "class diagram" | UML Class |
| "sequence diagram", "interaction diagram", "lifeline" | Sequence |
| "architecture", "system diagram", "service diagram" | Architecture |
| "neural network", "model architecture", "ML diagram", "deep learning" | ML / Deep Learning Model |
| "flowchart", "decision tree", "process flow" | Flowchart |
The diagram-type preset sets structural style keywords. If a user style preset is also active (see ## Style Presets), keep the structural keywords and layer color/font/edge/extras on top — read references/style-presets.md → "Interaction with diagram-type presets" for the merge rules.
{
"package": "@lobehub/icons-static-svg",
"version": "1.91.0",
"cdn": "https://unpkg.com/@lobehub/icons-static-svg@1.91.0/icons/",
"icons": [
"ace",
"ace-text",
"adobe",
"adobe-color",
"adobe-text",
"adobefirefly",
"adobefirefly-color",
"adobefirefly-text",
"agentvoice",
"agentvoice-color",
"agentvoice-text",
"agui",
"agui-text",
"ai2",
"ai2-color",
"ai2-text",
"ai21",
"ai21-brand",
"ai21-brand-color",
"ai21-text",
"ai302",
"ai302-color",
"ai302-text",
"ai360",
"ai360-color",
"ai360-text",
"aihubmix",
"aihubmix-color",
"aihubmix-text",
"aimass",
"aimass-color",
"aimass-text",
"aionlabs",
"aionlabs-color",
"aionlabs-text",
"airjelly",
"airjelly-color",
"airjelly-text",
"aistudio",
"aistudio-text",
"akashchat",
"akashchat-color",
"akashchat-text",
"alephalpha",
"alephalpha-text",
"alibaba",
"alibaba-brand",
"alibaba-brand-color",
"alibaba-color",
"alibaba-text",
"alibaba-text-cn",
"alibabacloud",
"alibabacloud-color",
"alibabacloud-text",
"alibabacloud-text-cn",
"amp",
"amp-color",
"amp-text",
"antgroup",
"antgroup-brand",
"antgroup-brand-color",
"antgroup-color",
"antgroup-text",
"antgroup-text-cn",
"anthropic",
"anthropic-text",
"antigravity",
"antigravity-color",
"antigravity-text",
"anyscale",
"anyscale-color",
"anyscale-text",
"apertis",
"apertis-color",
"apertis-text",
"apple",
"apple-text",
"arcee",
"arcee-color",
"arcee-text",
"askverdict",
"askverdict-color",
"askverdict-text",
"assemblyai",
"assemblyai-color",
"assemblyai-text",
"atlascloud",
"atlascloud-text",
"automatic",
"automatic-color",
"automatic-text",
"aws",
"aws-brand",
"aws-brand-color",
"aws-color",
"aws-text",
"aya",
"aya-color",
"aya-text",
"azure",
"azure-color",
"azure-text",
"azureai",
"azureai-color",
"azureai-text",
"baai",
"baai-text",
"baichuan",
"baichuan-color",
"baichuan-text",
"baidu",
"baidu-brand",
"baidu-brand-color",
"baidu-color",
"baidu-text",
"baidu-text-cn",
"baiducloud",
"baiducloud-color",
"baiducloud-text",
"bailian",
"bailian-color",
"bailian-text",
"baseten",
"baseten-text",
"bedrock",
"bedrock-color",
"bedrock-text",
"bfl",
"bfl-text",
"bilibili",
"bilibili-color",
"bilibili-text",
"bilibiliindex",
"bilibiliindex-text",
"bing",
"bing-color",
"bing-text",
"briaai",
"briaai-color",
"briaai-text",
"burncloud",
"burncloud-color",
"burncloud-text",
"bytedance",
"bytedance-brand",
"bytedance-brand-color",
"bytedance-color",
"bytedance-text",
"bytedance-text-cn",
"capcut",
"capcut-text",
"centml",
"centml-brand",
"centml-brand-color",
"centml-color",
"centml-text",
"cerebras",
"cerebras-brand",
"cerebras-brand-color",
"cerebras-color",
"cerebras-text",
"chatglm",
"chatglm-color",
"chatglm-text",
"cherrystudio",
"cherrystudio-color",
"cherrystudio-text",
"civitai",
"civitai-color",
"civitai-text",
"civitai-text-color",
"claude",
"claude-color",
"claude-text",
"claudecode",
"claudecode-color",
"claudecode-text",
"cline",
"cline-text",
"clipdrop",
"clipdrop-text",
"cloudflare",
"cloudflare-color",
"cloudflare-text",
"codebuddy",
"codebuddy-color",
"codebuddy-text",
"codeflicker",
"codeflicker-color",
"codeflicker-text",
"codegeex",
"codegeex-color",
"codegeex-text",
"codex",
"codex-color",
"codex-text",
"cogvideo",
"cogvideo-color",
"cogvideo-text",
"cogview",
"cogview-color",
"cogview-text",
"cohere",
"cohere-color",
"cohere-text",
"colab",
"colab-color",
"colab-text",
"cometapi",
"cometapi-color",
"cometapi-text",
"comfyui",
"comfyui-color",
"comfyui-text",
"commanda",
"commanda-color",
"commanda-text",
"copilot",
"copilot-color",
"copilot-text",
"copilotkit",
"copilotkit-color",
"copilotkit-text",
"coqui",
"coqui-color",
"coqui-text",
"coze",
"coze-text",
"crewai",
"crewai-brand",
"crewai-brand-color",
"crewai-color",
"crewai-text",
"crusoe",
"crusoe-color",
"crusoe-text",
"cursor",
"cursor-text",
"cybercut",
"cybercut-text",
"dalle",
"dalle-color",
"dalle-text",
"dbrx",
"dbrx-brand",
"dbrx-brand-color",
"dbrx-color",
"dbrx-text",
"deepai",
"deepai-text",
"deepcogito",
"deepcogito-color",
"deepcogito-text",
"deepinfra",
"deepinfra-color",
"deepinfra-text",
"deepl",
"deepl-color",
"deepl-text",
"deepmind",
"deepmind-color",
"deepmind-text",
"deepseek",
"deepseek-color",
"deepseek-text",
"devin",
"devin-color",
"devin-text",
"dify",
"dify-color",
"dify-text",
"doc2x",
"doc2x-color",
"doc2x-text",
"docsearch",
"docsearch-color",
"docsearch-text",
"dolphin",
"dolphin-text",
"doubao",
"doubao-color",
"doubao-text",
"dreammachine",
"dreammachine-text",
"elevenlabs",
"elevenlabs-text",
"elevenx",
"elevenx-text",
"essentialai",
"essentialai-color",
"essentialai-text",
"exa",
"exa-color",
"exa-text",
"fal",
"fal-color",
"fal-text",
"fastgpt",
"fastgpt-color",
"fastgpt-text",
"featherless",
"featherless-color",
"featherless-text",
"figma",
"figma-color",
"figma-text",
"fireworks",
"fireworks-color",
"fireworks-text",
"fishaudio",
"fishaudio-text",
"flora",
"flora-text",
"flowith",
"flowith-text",
"flux",
"flux-text",
"friendli",
"friendli-text",
"gemini",
"gemini-color",
"gemini-text",
"geminicli",
"geminicli-color",
"geminicli-text",
"gemma",
"gemma-color",
"gemma-text",
"giteeai",
"giteeai-text",
"github",
"github-text",
"githubcopilot",
"githubcopilot-text",
"glama",
"glama-text",
"glif",
"glif-text",
"glmv",
"glmv-color",
"glmv-text",
"google",
"google-brand",
"google-brand-color",
"google-color",
"googlecloud",
"googlecloud-brand",
"googlecloud-brand-color",
"googlecloud-color",
"goose",
"goose-text",
"gradio",
"gradio-color",
"gradio-text",
"greptile",
"greptile-color",
"greptile-text",
"grok",
"grok-text",
"groq",
"groq-text",
"hailuo",
"hailuo-color",
"hailuo-text",
"haiper",
"haiper-text",
"hedra",
"hedra-text",
"hermesagent",
"hermesagent-text",
"higress",
"higress-color",
"higress-text",
"huawei",
"huawei-color",
"huawei-text",
"huawei-text-cn",
"huaweicloud",
"huaweicloud-color",
"huaweicloud-text",
"huaweicloud-text-cn",
"huggingface",
"huggingface-color",
"huggingface-text",
"hunyuan",
"hunyuan-color",
"hunyuan-text",
"hyperbolic",
"hyperbolic-color",
"hyperbolic-text",
"ibm",
"ibm-text",
"ideogram",
"ideogram-text",
"iflytekcloud",
"iflytekcloud-color",
"iflytekcloud-text",
"inception",
"inception-text",
"inference",
"inference-text",
"infermatic",
"infermatic-color",
"infermatic-text",
"infinigence",
"infinigence-color",
"infinigence-text",
"infinigence-text-cn",
"inflection",
"inflection-text",
"internlm",
"internlm-color",
"internlm-text",
"jimeng",
"jimeng-color",
"jimeng-text",
"jina",
"jina-text",
"junie",
"junie-color",
"junie-text",
"kilocode",
"kilocode-text",
"kimi",
"kimi-color",
"kimi-text",
"kiro",
"kiro-color",
"kiro-text",
"kling",
"kling-color",
"kling-text",
"kluster",
"kluster-color",
"kluster-text",
"kolors",
"kolors-color",
"kolors-text",
"krea",
"krea-text",
"kwaikat",
"kwaikat-text",
"kwaikat-text-color",
"kwaipilot",
"kwaipilot-color",
"kwaipilot-text",
"lambda",
"lambda-text",
"langchain",
"langchain-color",
"langchain-text",
"langfuse",
"langfuse-color",
"langfuse-text",
"langgraph",
"langgraph-color",
"langgraph-text",
"langsmith",
"langsmith-color",
"langsmith-text",
"leptonai",
"leptonai-color",
"leptonai-text",
"lg",
"lg-color",
"lg-text",
"lightricks",
"lightricks-text",
"liquid",
"liquid-text",
"livekit",
"livekit-color",
"livekit-text",
"llamaindex",
"llamaindex-color",
"llamaindex-text",
"llava",
"llava-color",
"llava-text",
"llmapi",
"llmapi-color",
"llmapi-text",
"lmstudio",
"lmstudio-text",
"lobehub",
"lobehub-color",
"lobehub-text",
"longcat",
"longcat-color",
"longcat-text",
"lovable",
"lovable-color",
"lovable-text",
"lovart",
"lovart-text",
"luma",
"luma-color",
"luma-text",
"magic",
"magic-text",
"make",
"make-color",
"make-text",
"manus",
"manus-text",
"mastra",
"mastra-text",
"mcp",
"mcp-text",
"mcpso",
"mcpso-color",
"mcpso-text",
"menlo",
"menlo-color",
"menlo-text",
"meshy",
"meshy-color",
"meshy-text",
"meta",
"meta-brand",
"meta-brand-color",
"meta-color",
"meta-text",
"metaai",
"metaai-color",
"metaai-text",
"metagpt",
"metagpt-text",
"microsoft",
"microsoft-color",
"microsoft-text",
"midjourney",
"midjourney-text",
"minimax",
"minimax-color",
"minimax-text",
"mistral",
"mistral-color",
"mistral-text",
"modelscope",
"modelscope-color",
"modelscope-text",
"monica",
"monica-color",
"monica-text",
"moonshot",
"moonshot-text",
"morph",
"morph-color",
"morph-text",
"moxt",
"moxt-color",
"moxt-text",
"myshell",
"myshell-color",
"myshell-text",
"n8n",
"n8n-color",
"n8n-text",
"nanobanana",
"nanobanana-color",
"nanobanana-text",
"nebius",
"nebius-text",
"newapi",
"newapi-color",
"newapi-text",
"notebooklm",
"notebooklm-text",
"notion",
"notion-text",
"nousresearch",
"nousresearch-text",
"nova",
"nova-color",
"nova-text",
"novelai",
"novelai-text",
"novita",
"novita-color",
"novita-text",
"nplcloud",
"nplcloud-color",
"nplcloud-text",
"nvidia",
"nvidia-color",
"nvidia-text",
"obsidian",
"obsidian-color",
"obsidian-text",
"ollama",
"ollama-text",
"openai",
"openai-text",
"openchat",
"openchat-color",
"openchat-text",
"openclaw",
"openclaw-color",
"openclaw-text",
"opencode",
"opencode-text",
"openhands",
"openhands-color",
"openhands-text",
"openhuman",
"openhuman-text",
"openrouter",
"openrouter-text",
"openwebui",
"openwebui-text",
"palm",
"palm-color",
"palm-text",
"parasail",
"parasail-text",
"perplexity",
"perplexity-color",
"perplexity-text",
"phidata",
"phidata-color",
"phidata-text",
"phind",
"phind-text",
"pika",
"pika-text",
"pixverse",
"pixverse-color",
"pixverse-text",
"player2",
"player2-color",
"player2-text",
"poe",
"poe-color",
"poe-text",
"pollinations",
"pollinations-text",
"ppio",
"ppio-color",
"ppio-text",
"ppio-text-cn",
"prunaai",
"prunaai-color",
"prunaai-text",
"pydanticai",
"pydanticai-color",
"pydanticai-text",
"qingyan",
"qingyan-color",
"qingyan-text",
"qiniu",
"qiniu-color",
"qiniu-text",
"qoder",
"qoder-color",
"qoder-text",
"qwen",
"qwen-color",
"qwen-text",
"railway",
"railway-text",
"recraft",
"recraft-text",
"relace",
"relace-text",
"replicate",
"replicate-brand",
"replicate-text",
"replit",
"replit-color",
"replit-text",
"reve",
"reve-text",
"roocode",
"roocode-text",
"rsshub",
"rsshub-color",
"rsshub-text",
"runway",
"runway-text",
"rwkv",
"rwkv-color",
"rwkv-text",
"sambanova",
"sambanova-color",
"sambanova-text",
"search1api",
"search1api-color",
"search1api-text",
"searchapi",
"searchapi-text",
"sensenova",
"sensenova-brand",
"sensenova-brand-color",
"sensenova-color",
"sensenova-text",
"siliconcloud",
"siliconcloud-color",
"siliconcloud-text",
"sillytavern",
"sillytavern-color",
"sillytavern-text",
"skywork",
"skywork-color",
"skywork-text",
"slock",
"slock-text",
"smithery",
"smithery-color",
"smithery-text",
"snowflake",
"snowflake-color",
"snowflake-text",
"sophnet",
"sophnet-color",
"sophnet-text",
"sora",
"sora-color",
"sora-text",
"spark",
"spark-color",
"spark-text",
"speedai",
"speedai-color",
"speedai-text",
"stability",
"stability-brand",
"stability-brand-color",
"stability-color",
"stability-text",
"statecloud",
"statecloud-color",
"statecloud-text",
"stepfun",
"stepfun-color",
"stepfun-text",
"straico",
"straico-color",
"straico-text",
"streamlake",
"streamlake-color",
"streamlake-text",
"submodel",
"submodel-color",
"submodel-text",
"suno",
"suno-text",
"sync",
"sync-text",
"targon",
"targon-color",
"targon-text",
"tavily",
"tavily-color",
"tavily-text",
"tencent",
"tencent-brand",
"tencent-brand-color",
"tencent-color",
"tencent-text",
"tencent-text-cn",
"tencentcloud",
"tencentcloud-color",
"tencentcloud-text",
"tiangong",
"tiangong-color",
"tiangong-text",
"tii",
"tii-color",
"tii-text",
"together",
"together-brand",
"together-brand-color",
"together-color",
"together-text",
"topazlabs",
"topazlabs-text",
"trae",
"trae-color",
"trae-text",
"tripo",
"tripo-color",
"tripo-text",
"turix",
"turix-text",
"udio",
"udio-color",
"udio-text",
"unstructured",
"unstructured-color",
"unstructured-text",
"upstage",
"upstage-color",
"upstage-text",
"v0",
"vectorizerai",
"vectorizerai-text",
"venice",
"venice-color",
"venice-text",
"vercel",
"vercel-text",
"vertexai",
"vertexai-color",
"vertexai-text",
"vidu",
"vidu-color",
"vidu-text",
"viggle",
"viggle-text",
"vllm",
"vllm-color",
"vllm-text",
"volcengine",
"volcengine-color",
"volcengine-text",
"voyage",
"voyage-color",
"voyage-text",
"wenxin",
"wenxin-color",
"wenxin-text",
"windsurf",
"windsurf-text",
"workersai",
"workersai-color",
"workersai-text",
"worldrouter",
"worldrouter-text",
"xai",
"xai-text",
"xiaomimimo",
"xiaomimimo-text",
"xinference",
"xinference-color",
"xinference-text",
"xpay",
"xpay-color",
"xpay-text",
"xuanyuan",
"xuanyuan-color",
"xuanyuan-text",
"yandex",
"yandex-text",
"yi",
"yi-color",
"yi-text",
"youmind",
"youmind-text",
"yuanbao",
"yuanbao-color",
"yuanbao-text",
"zai",
"zai-text",
"zapier",
"zapier-color",
"zapier-text",
"zeabur",
"zeabur-color",
"zeabur-text",
"zencoder",
"zencoder-color",
"zencoder-text",
"zenmux",
"zenmux-text",
"zeroone",
"zeroone-color",
"zeroone-text",
"zhipu",
"zhipu-color",
"zhipu-text"
]
}Shape index attribution
shape-index.json.gz is a gzipped copy of the shape search index from jgraph/drawio-mcp (shape-search/search-index.json), which is generated from the official draw.io / diagrams.net client shape libraries. Both upstream sources are licensed under the Apache License 2.0.
- Each entry is
{style, w, h, title, tags, type}for one palette shape. - 10,446 shapes spanning AWS, Azure, GCP, Cisco, Kubernetes, UML, BPMN, P&ID,
electrical, flowchart, network, and the general shape sets.
- Used read-only by
scripts/shapesearch.pyto resolve exact official style
strings instead of hand-guessing them.
To refresh against a newer draw.io release, regenerate upstream with shape-search/generate-index.js in the drawio-mcp repo, then re-gzip:
gzip -9 -c search-index.json > data/shape-index.json.gz
Auto-layout (Graphviz)
Read this when a diagram is large or layout-heavy — dependency/call graphs, code/module structure, or roughly more than ~15 nodes — where hand-placing x/y coordinates is slow, error-prone, and overlap-prone.
Instead of computing coordinates by hand in the Generate step, describe the graph as JSON and let scripts/autolayout.py place the nodes and route the edges with Graphviz, then continue the normal workflow (Export draft → Self-check → …) on the produced .drawio.
For small or carefully-styled diagrams, keep hand-placing — auto-layout trades fine control for scale.
Dependency
Requires Graphviz dot on PATH:
# macOS
brew install graphviz
# Debian/Ubuntu
sudo apt install graphvizThe script exits with a clear message if dot is missing — fall back to hand-placed coordinates in that case.
Usage
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o diagram.drawioIt prints wrote diagram.drawio (N nodes, M edges) to stderr and writes a normal .drawio file. From there, continue at the Export draft step of the main workflow (preview PNG with --width 2000, self-check, review loop, final export with -e + repair_png.py).
Input format
{
"direction": "TB",
"nodes": [
{"id": "client", "label": "Web Client", "style": "rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;"},
{"id": "gw", "label": "API Gateway", "group": "edge", "groupLabel": "Edge tier"},
{"id": "db", "label": "User DB", "style": "shape=cylinder3;whiteSpace=wrap;html=1;", "width": 120, "height": 80, "group": "data"}
],
"edges": [
{"source": "client", "target": "gw", "label": "HTTPS"},
{"source": "gw", "target": "db"}
]
}Fields
| Field | Required | Default | Notes |
|---|---|---|---|
direction | no | TB | TB (top→bottom) or LR (left→right) — the layout rank direction |
nodes[].id | yes | — | Unique; must not be 0 or 1 (reserved for draw.io root cells) |
nodes[].label | no | the id | Display text; auto XML-escaped |
nodes[].style | no | group colour, else blue | Any draw.io style string — reuse the role/shape styles from diagram-types.md and the active preset. A styleless node is tinted by its group (see Containers / grouping); an explicit style always wins |
nodes[].width / height | no | 120 / 60 | Pixels; dot lays out at this real size |
nodes[].group | no | none | Group key, or a /-delimited path ("core/db") for nested containers — nodes sharing a path are boxed together (see Containers / grouping) |
nodes[].groupLabel | no | last path segment | Title shown on the node's deepest container (first node with the path wins) |
edges[].source / target | yes | — | Must match node ids |
edges[].label | no | empty | Edge text |
How it places things
- Node positions come from
dot(hierarchical layered layout), converted to draw.io pixels and snapped to the grid (multiples of 10). - Edges use
splines=ortho: dot's orthogonal route is replayed as draw.io waypoints, so edges go around nodes instead of through them. - Apply the active style preset by setting each node's
styleto the preset's role/shape values before calling the script — the script does not know about presets.
Containers / grouping
Give nodes a group key and the script wraps each group in a labeled container (a dashed box with the group title at top) and tells dot to keep that group's nodes together via a Graphviz cluster. Grouped nodes become children of their container (parent="<container>", relative coordinates); ungrouped nodes stay at the top level. This turns a flat hairball into a "boxes of related modules" architecture view.
Nesting. A group value with / separators builds nested containers: "core/db" puts the node inside a db box that itself sits inside a core box. Every path prefix becomes a container, so an arbitrarily deep package tree maps to nested boxes. A node can also sit directly in a parent box (group: "core") alongside a sibling sub-box (group: "core/db").
- Colour by group. Each top-level group is assigned a colour from the skill's own palette (
styles/built-in/default.json, cycled in role order: blue → green → orange → purple → yellow → red → grey). A node with nostyleof its own is tinted with its group's colour, and the container's border + title match — so related modules read as a coloured cluster instead of monochrome boxes. A node that carries its ownstyle(e.g. from an applied preset) is left untouched. Pass--monoto turn colouring off (dashed grey boxes, default-blue nodes — the previous look). Ungrouped graphs are unaffected. - Each container box is the bounding box of its members and child boxes plus a uniform padding. The dot cluster margin is set to that same padding, so each box equals dot's cluster box — which dot keeps non-overlapping at any nesting depth.
- The title sits in the top padding (
verticalAlign=top); the box title is the path's last segment, or a member'sgroupLabel. - Containers are visual only (no edges of their own). Edges still connect node→node and route across containers normally.
- If a container's top padding would cross the page origin, the whole diagram is shifted so nothing lands at a negative coordinate.
Validate before previewing
scripts/validate.py is a deterministic structural linter — run it on the produced .drawio before the (slower, vision-based) self-check:
python3 <this-skill-dir>/scripts/validate.py diagram.drawioIt catches dangling edge endpoints, duplicate/reserved ids, broken parent references (errors), plus off-grid/negative geometry and overlapping sibling nodes (warnings) — without launching draw.io. Exit status is non-zero on any error (or any warning with --strict), so it can gate the workflow. Auto-layout output should always pass clean; a failure means a malformed input graph (e.g. an edge referencing a missing node id).
Importers — visualize code structure
Bundled importers turn a codebase into a graph JSON ready for autolayout, so "visualize this project" is a two-step pipeline:
| Language | Script | Node = | Edge = |
|---|---|---|---|
| Python | scripts/pyimports.py <dir> | module / package (ast) | intra-project import / from |
| JS / TS | scripts/jsimports.py <dir> | source file (.ts/.tsx/.js/.jsx/.mjs/.cjs) | resolved relative import/export from/require()/import() |
| Go | scripts/goimports.py <dir> | package (directory, via go.mod) | intra-module package import |
| Rust | scripts/rustimports.py <dir> | module (.rs file / mod) | intra-crate use crate:: / super:: / self:: |
| Python (classes) | scripts/pyclasses.py <dir> | class (ast) | subclass → base (inheritance) |
python3 <this-skill-dir>/scripts/pyimports.py myproject -o graph.json
python3 <this-skill-dir>/scripts/autolayout.py graph.json -o diagram.drawioEach keeps only intra-project edges (third-party/stdlib imports are ignored), shortens node labels (drops the shared package/module/directory prefix; ids stay fully qualified), and shares the same flags: --direction TB|LR (default TB), --group, --no-reduce.
- Python (
pyimports.py): if the directory is itself a package (__init__.pypresent), module names are package-qualified so the project's own absolute imports resolve; nested subpackages (pkg.sub.mod) are handled. - JS/TS (
jsimports.py): resolution is path-based (tries the source extensions and directoryindexfiles);node_modulesand bare specifiers are skipped. Scanning is regex-based, not a full parser. - Go (
goimports.py): reads themodulepath fromgo.mod; each directory of.gofiles is one package;*_test.goandvendor/are skipped. - Rust (
rustimports.py): each.rsfile is a module (mod.rs/main.rs/lib.rsname the enclosing module); edges come fromusepaths rooted atcrate::/super::/self::(brace groups expanded).std/external crates andtarget/are skipped. Regex-based — inlinemod { … }blocks aren't split out, and 2015-edition bare intra-crate paths aren't resolved. - Python classes (
pyclasses.py): a finer granularity — one node per class, edges from each subclass to the project base classes it extends, so the result is an auto-generated class hierarchy. Bases are matched by name (preferring the same module); external bases (object, third-party) are ignored. With--group, classes are boxed by their module, so a deep package tree nests naturally. Inheritance only — function-level call graphs are out of scope (static call resolution in Python is unreliable).
Density reduction is on by default — this is the key to a readable result. Real import graphs are dense (asyncio: 33 modules / ~149 edges); without reduction they render as a hairball. Every importer applies transitive reduction (Graphviz tred — drops edges already implied by a longer path), which on asyncio cuts ~149 edges to ~46 and turns the hairball into a clean, traceable diagram. Pass --no-reduce to keep every edge.
`--group` assigns each node a container by its sub-package / directory path, so autolayout boxes related modules together — nested when the path has depth (see Containers / grouping). The fastest way to turn a large code graph into a tiered architecture view.
For any other language, produce the same graph JSON from any analyzer (e.g. dependency-cruiser for richer JS/TS resolution, go-callvis for Go call graphs) and feed it to autolayout the same way.
Limitations
- Placement is topological, not semantic — dot minimises edge crossings, which may put a node in a different column than you'd choose by hand. Re-export with the other
direction, or hand-tune the produced XML afterwards (it's a normal.drawio). - Import edges are static —
pyimports/jsimports/goimportsread static import statements (not dynamicimportlib, runtimerequire, or reflection);pyclassesresolves inheritance only, not method-level calls. - Parallel edges between the same
(source, target)pair share one route. - Containers don't add edges —
group/nesting only boxes nodes for layout; edges remain node→node. For hand-built swimlane/architecture containers with their own connections, see SKILL.md "Containers and groups".
Diagram Type Presets
When the user requests a specific diagram type, apply the matching preset below for shapes, styles, and layout conventions. These presets set structural style keywords (e.g. ERD's shape=table;childLayout=tableLayout); a user style preset (see references/style-presets.md) layers color/font/edge/extras on top.
Read this file when:
- The user names one of these diagram types (ERD, UML class, sequence, architecture, ML/DL model, flowchart)
- You're choosing shape vocabulary or layout direction for a new diagram
ERD (Entity-Relationship Diagram)
| Element | Style | Notes |
|---|---|---|
| Table | shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;strokeColor=#6c8ebf;fillColor=#dae8fc; | Each table is a container |
| Row (column) | shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12; | Child of table, parent=tableId |
| PK column | Bold text: fontStyle=1 on the row | Mark with PK prefix or key icon |
| FK relationship | Dashed edge: dashed=1;endArrow=ERmandOne;startArrow=ERmandOne; | Use ER notation arrows |
| Layout | TB, tables spaced 300px apart | Group related tables vertically |
UML Class Diagram
| Element | Style | Notes |
|---|---|---|
| Class box | swimlane;fontStyle=1;align=center;startSize=26;html=1; | 3-section: title / attributes / methods |
| Separator | line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=10;rotatable=0;labelPosition=left;points=[];portConstraint=eastwest; | Between sections |
| Inheritance | endArrow=block;endFill=0; | Hollow triangle arrow |
| Implementation | endArrow=block;endFill=0;dashed=1; | Dashed + hollow triangle |
| Composition | endArrow=diamondThin;endFill=1; | Filled diamond |
| Aggregation | endArrow=diamondThin;endFill=0; | Hollow diamond |
| Layout | TB, classes 250px apart | Interfaces above implementations |
Sequence Diagram
| Element | Style | Notes |
|---|---|---|
| Actor/Object | shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest; | Lifeline with dashed vertical line |
| Sync message | html=1;verticalAlign=bottom;endArrow=block; | Solid line, filled arrowhead |
| Async message | html=1;verticalAlign=bottom;endArrow=open;dashed=1; | Dashed line, open arrowhead |
| Return message | html=1;verticalAlign=bottom;endArrow=open;dashed=1;strokeColor=#999999; | Grey dashed |
| Activation box | shape=umlFrame;whiteSpace=wrap; on the lifeline | Narrow rectangle on lifeline |
| Layout | LR, lifelines spaced 200px apart | Time flows top to bottom |
Architecture Diagram
| Element | Style | Notes |
|---|---|---|
| Layer/tier | swimlane;startSize=30; | Containers for grouping: Client / API / Service / Data |
| Service | rounded=1;whiteSpace=wrap;html=1; + tier color | Use color palette by tier |
| Database | shape=cylinder3;whiteSpace=wrap;html=1; | Green palette |
| Queue/Bus | rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656; | Yellow — place centrally for hub pattern |
| Gateway/LB | shape=mxgraph.aws4.resourceIcon; or rounded=1; with orange | Orange palette |
| External | rounded=1;dashed=1;fillColor=#f5f5f5;strokeColor=#666666; | Dashed border for external systems |
| Layout | TB or LR by tier count; ≥4 tiers → TB | Hub nodes centered |
ML / Deep Learning Model Diagram
For neural network architecture diagrams — ideal for papers targeting NeurIPS, ICML, ICLR.
| Element | Style | Notes |
|---|---|---|
| Layer block | rounded=1;whiteSpace=wrap;html=1; + type color | Main building block |
| Input/Output | fillColor=#d5e8d4;strokeColor=#82b366; | Green |
| Conv / Pooling | fillColor=#dae8fc;strokeColor=#6c8ebf; | Blue |
| Attention / Transformer | fillColor=#e1d5e7;strokeColor=#9673a6; | Purple |
| RNN / LSTM / GRU | fillColor=#fff2cc;strokeColor=#d6b656; | Yellow |
| FC / Linear | fillColor=#ffe6cc;strokeColor=#d79b00; | Orange |
| Loss / Activation | fillColor=#f8cecc;strokeColor=#b85450; | Red/Pink |
| Skip connection | dashed=1;endArrow=block;curved=1; | Dashed curved arrow |
| Tensor shape label | Add shape annotation as secondary label: value="Conv2D
(B, 64, 32, 32)" | Use 
 for multi-line |
| Layout | TB (data flows top→bottom), layers 150px apart | Group encoder/decoder as swimlanes |
Tensor shape convention: annotate each layer with input/output tensor dimensions in (B, C, H, W) or (B, T, D) format. Place dimensions as the second line of the label using 
.
Flowchart (enhanced)
| Element | Style | Notes |
|---|---|---|
| Start/End | ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366; | Green oval |
| Process | rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf; | Blue rectangle |
| Decision | rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656; | Yellow diamond |
| I/O | shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00; | Orange parallelogram |
| Subprocess | rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6; + double border | Purple |
| Yes/No labels | value="Yes" / value="No" on decision edges | Always label decision branches |
| Layout | TB, 200px vertical gap | Decisions branch LR, merge back to center |
Shape vocabulary & search
Read this when a diagram needs a specific shape — a cloud-provider icon (AWS/Azure/GCP), a network/Cisco/Kubernetes symbol, a UML/BPMN/ER element, an electrical or P&ID part — or any time you'd otherwise guess a style= string.
There are two ways to get a style:
1. Search the official shape index (scripts/shapesearch.py) — 10,446 real draw.io palette shapes with their exact style, w, h. Use this for branded/vendor icons and anything non-trivial. *Always prefer a searched style over a hand-written `shape=mxgraph.` guess — guessed stencil names silently render as a blank box if the name is wrong. 2. The cheatsheet below** — the common built-in shapes whose style strings are short and stable enough to write by hand (rectangles, flowchart symbols, UML primitives, containers, edges).
Searching shapes
python3 <this-skill-dir>/scripts/shapesearch.py "aws lambda" --limit 5
python3 <this-skill-dir>/scripts/shapesearch.py "uml actor" --json- Query is space-separated keywords; matching is tag-based with Soundex
fuzziness and camelCase/digit splitting ("pid2valve" → pid valve).
- Prints each match as
Title (WxH)followed by its fullstyle=string. With
--json, emits [{style,w,h,title}] for programmatic use.
- Copy the
styleverbatim into anmxCell, and use the reportedw/has the
mxGeometry width/height (vendor icons are drawn at a fixed aspect ratio).
- Results are ranked by tag relevance, with shapes whose title contains the
query terms bubbled to the top of each score tier. Ranking is still a heuristic, though, and many shapes share a title (three Lambda variants: aws3/aws4/aws3d) — so run with --limit 5 and pick the row whose title and size match what you actually want rather than blindly taking #1.
<mxCell id="2" value="Lambda" style="<paste the searched style here>" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="78" height="78" as="geometry"/>
</mxCell>Covered libraries: AWS (aws3/aws4), Azure, GCP, Cisco, Kubernetes, UML, BPMN, ER, electrical, P&ID, mockup/wireframe, flowchart, network, and the general/basic sets. The bundled index (data/shape-index.json.gz) is the upstream draw.io shape data — see data/SHAPE-INDEX-NOTICE.md for attribution.
AI / LLM brand logos
draw.io's bundled libraries have no modern AI/LLM brand logos, so an "LLM app architecture" otherwise renders as generic boxes. scripts/aiicons.py resolves a brand name (OpenAI, Claude, Gemini, Mistral, Llama, HuggingFace, Ollama, LangChain, …321 brands) to a draw.io image style backed by the lobe-icons set (MIT).
python3 <this-skill-dir>/scripts/aiicons.py "claude" --json # CDN reference
python3 <this-skill-dir>/scripts/aiicons.py "openai" --embed # self-contained
python3 <this-skill-dir>/scripts/aiicons.py --list # all brands- Picks the
-colorvariant when it exists, else the mono logo (e.g. OpenAI is
mono-only). Returns a square image style; use the reported --size (default 48) for both width and height.
- Default references the icon by CDN URL — the SVG lives on unpkg, not in
this repo, so draw.io needs network access when the diagram is rendered or opened; an offline export draws a blank box. Pass --embed to fetch the SVG once and inline it as a data URI (portable, renders offline, larger XML).
- Logos are trademarks of their respective owners, referenced for identification
only — the same basis on which draw.io ships AWS/Azure icons.
- Data stores common in RAG/LLM apps that lobe lacks (Qdrant, Redis,
Postgres, Mongo, Elasticsearch, Milvus, Supabase, Neo4j, ClickHouse, Kafka, Snowflake, Databricks, …) resolve via the simple-icons CDN (CC0) as an automatic fallback — same command, same output shape. A brand in neither set has no logo; use a cylinder (shape=cylinder3;, see below) or scripts/shapesearch.py "<name> database".
Cheatsheet — hand-writable styles
These are stable enough to write without searching. Combine with whiteSpace=wrap;html=1;.
Common shapes (shape= keyword)
| Need | style |
|---|---|
| Rectangle / rounded box | rounded=0; / rounded=1; |
| Circle / ellipse | ellipse; (aspect=fixed; for a true circle) |
| Diamond (decision) | rhombus; |
| Cylinder (database) | shape=cylinder3; |
| Cloud | cloud; |
| Cube (3D) | shape=cube; |
| Sticky note | shape=note; |
| Document (curled bottom) | shape=document; |
| Folder | shape=folder; |
| Card (cut corner) | shape=card; |
| Process (double border) | shape=process; |
| Step / chevron | shape=step; |
| Parallelogram (I/O) | shape=parallelogram;perimeter=parallelogramPerimeter; |
| Trapezoid | shape=trapezoid;perimeter=trapezoidPerimeter; |
| Hexagon | shape=hexagon;perimeter=hexagonPerimeter2; |
| Manual input | shape=manualInput; |
| Data storage | shape=dataStorage; |
| Off-page connector | shape=offPageConnector; |
| Delay | shape=delay; |
| OR / XOR gate | shape=or; / shape=xor; |
| Block arrow | shape=singleArrow; / shape=doubleArrow; |
| Callout (speech bubble) | shape=callout; |
UML primitives
| Element | style |
|---|---|
| Actor (stick figure) | shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top; |
| Boundary | shape=umlBoundary; |
| Control | shape=umlControl; |
| Entity | shape=umlEntity; |
| Lifeline | shape=umlLifeline;perimeter=lifelinePerimeter;container=1; |
| Frame | shape=umlFrame; |
| Provided interface (lollipop) | shape=lollipop;direction=south; |
| Required interface | shape=requires;direction=north; |
| Component | shape=component; |
Containers (parent-child; children use relative coords)
| Type | style | When |
|---|---|---|
| Invisible group | group;pointerEvents=0; | No border, no own connections |
| Titled swimlane | swimlane;startSize=30; | Visible title bar / has connections |
| Any shape as container | append container=1;pointerEvents=0; | Box without own connections |
Edges
| Need | add to style |
|---|---|
| Orthogonal routing | edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto; |
| Curved | curved=1; |
| No arrowhead | endArrow=none; |
| Open/thin arrow | endArrow=open; / endArrow=classicThin; |
| Dashed | dashed=1; (pattern via dashPattern=8 8;) |
| Flow animation | flowAnimation=1; |
| Label background | labelBackgroundColor=#ffffff; |
Useful property knobs
fontStyleis a bitmask:1=bold,2=italic,4=underline (add to combine:3=bold+italic).direction=north|south|east|westrotates a shape in 90° steps;rotation=<deg>for free rotation.gradientColor=#RRGGBB;+gradientDirection=north;for a gradient fill.sketch=1;gives a hand-drawn look (set globally via a style preset instead when possible).
For richer per-shape detail, the upstream source is jgraph/drawio-mcp's shared/style-reference.md (Apache-2.0).
Style Extraction — agent reference
Loaded on demand by SKILL.md when the user asks to learn a style ("learn my style from <path> as <name>") or when the agent needs to render a sample after extraction.
Sample diagram (for approval render)
After extracting a candidate preset, render this seven-node sample using the candidate's palette/shapes/fonts/edges. Each role appears exactly once; six edges, one dashed, exercise edges.arrow, edges.style, and edges.dashedFor.
Layout (TB):
- Row 1 (y=40):
gatewaycentered at x=340 - Row 2 (y=180):
security(x=80),service(x=340),queue(x=600) - Row 3 (y=340):
database(x=80),external(x=340),error(x=600)
Template — substitute `{{...}}` placeholders from the candidate preset.
The vertex style for role R is built as: <shapes[R]>;whiteSpace=wrap;html=1;fillColor=<palette[roles[R]].fillColor>;strokeColor=<palette[roles[R]].strokeColor>;fontFamily=<font.fontFamily>;fontSize=<font.fontSize>
- If
extras.sketch=true, append;sketch=1to every vertex style AND every edge style. - If
extras.globalStrokeWidth !== 1(i.e., any value other than the drawio default of 1, including0.5), append;strokeWidth=<n>to every vertex style AND every edge style.
The edge style is built as: <edges.style>;<edges.arrow>
- Per-edge routing keys (
exitX/entryX/...) are added as literals below. - Edge 15 exercises
edges.dashedFor: - If
edges.dashedForis non-empty, use its first entry as the edge'svalue(label) AND append;dashed=1to the edge style. - If
edges.dashedForis empty ([]), use the labelcross-calland do NOT append;dashed=1— the preset has no dashed convention, so the sample must not fake one.
Placeholder expansion (applied when filling the XML):
{{VSTYLE:<role>}}expands to the vertex-style formula above withR = <role>. Write the result as a literal string; do not URL-encode.{{ESTYLE}}expands to the edge-style formula above.{{EDGE15_LABEL}}and{{EDGE15_DASH}}follow the Edge-15 rule above.
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="drawio" version="26.0.0">
<diagram name="Preset Sample">
<mxGraphModel>
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Row 1: gateway -->
<mxCell id="2" value="Gateway" style="{{VSTYLE:gateway}}" vertex="1" parent="1">
<mxGeometry x="340" y="40" width="160" height="60" as="geometry" />
</mxCell>
<!-- Row 2: security | service | queue -->
<mxCell id="3" value="Auth" style="{{VSTYLE:security}}" vertex="1" parent="1">
<mxGeometry x="80" y="180" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="4" value="Service" style="{{VSTYLE:service}}" vertex="1" parent="1">
<mxGeometry x="340" y="180" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="5" value="Queue" style="{{VSTYLE:queue}}" vertex="1" parent="1">
<mxGeometry x="600" y="180" width="160" height="60" as="geometry" />
</mxCell>
<!-- Row 3: database | external | error -->
<mxCell id="6" value="Database" style="{{VSTYLE:database}}" vertex="1" parent="1">
<mxGeometry x="80" y="340" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="7" value="External API" style="{{VSTYLE:external}}" vertex="1" parent="1">
<mxGeometry x="340" y="340" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="8" value="Error Sink" style="{{VSTYLE:error}}" vertex="1" parent="1">
<mxGeometry x="600" y="340" width="160" height="60" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="10" value="" style="{{ESTYLE}};exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="11" value="" style="{{ESTYLE}};exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="" style="{{ESTYLE}};exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="2" target="5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="13" value="" style="{{ESTYLE}};exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="14" value="" style="{{ESTYLE}};exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="15" value="{{EDGE15_LABEL}}" style="{{ESTYLE}}{{EDGE15_DASH}};exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0" edge="1" parent="1" source="4" target="8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>Rendering the sample
1. Write the filled XML to /tmp/drawio-preset-<name>.drawio. 2. Run the same drawio -x -f png -e -s 2 -o <preset-name>-sample.png <tmp>.drawio command the main workflow uses (substitute the binary name you resolved in SKILL.md Step 1 if it isn't drawio). 3. Repair the IEND chunk: python3 <this-skill-dir>/scripts/repair_png.py <preset-name>-sample.png — the -e flag truncates the PNG the same way the main workflow's step 7 does, so the sample needs the same fix to be readable. 4. Save the PNG as ./preset-<name>-sample.png (the user's working directory). 5. Show the user: preset summary table + PNG path + provenance/confidence line.
Approval loop
- "save" / "looks good" → write candidate to
~/.drawio-skill/styles/<name>.json; delete tempfile and sample PNG. - "change <field> to <value>" → edit the in-memory candidate; re-render; re-ask.
- "cancel" → delete tempfile and sample PNG; no save.
If sample render fails (draw.io CLI missing / export error)
Still show the summary table and the provenance line. Note: "Could not render sample PNG (CLI unavailable). Save anyway on your OK." Do not block.
XML extraction path
Input: a .drawio file path. Output: candidate preset JSON. Deterministic, no LLM inference.
Steps
1. Parse the file. Read the XML, collect every <mxCell> with a style= attribute, split into vertices (vertex="1") and edges (edge="1"). 2. Tokenize each `style=` string on ;. Each element is either key=value or a bare keyword (e.g., rhombus, ellipse, rounded=1). 3. Extract palette. For every vertex, take the (fillColor, strokeColor) pair (skip vertices with neither). Count frequency. Keep the top ≤7 pairs. 4. Extract shape vocabulary + role mapping. For each vertex determine a shape class by precedence: cylinder3 > ellipse > rhombus > swimlane > rounded=1 > rounded=0. Then infer the semantic role from the vertex's shape class and its value (label) attribute. Evaluate the rules below in order; first match wins.
cylinder3→databaserhombus→decisionswimlane→containerdashed=1present + grey-family fill (hex where the R, G, and B channels all fall within ±16 of each other, i.e., near-achromatic) →external- label matches
/queue|bus|kafka|rabbit/i→queue - label matches
/gateway|api|lb|load/i→gateway - label matches
/auth|login|jwt|oauth/i→security - label matches
/error|fail|alert/i→error - everything else →
service
For each role that has a canonical palette slot — service, database, queue, gateway, error, external, security — the most frequent (role, color-pair) mapping wins. The pair goes into the role's canonical palette slot: service→primary, database→success, queue→warning, gateway→accent, error→danger, external→neutral, security→secondary. Set roles[role] to that slot name.
Decision and container shapes do not get a `roles[...]` entry — they are recorded only in shapes.decision and shapes.container. Any color pairs observed on decision/container vertices still participate in the palette (they can fill leftover slots) but are not tied to a semantic role.
Leftover color pairs (not claimed by any role-slot mapping) fill remaining empty palette slots in descending-frequency order.
Record the shape class string used per role in shapes[role]. The six named shape keys are service, database, queue, decision, external, container — gateway, error, and security roles inherit shapes.service and do not get their own shapes[...] entry. Example: shapes.database = "shape=cylinder3".
5. Extract fonts. Compute modal fontFamily and fontSize across vertices; emit them as font.fontFamily and font.fontSize. Also track fontStyle per vertex as a working variable (not an output field — the schema has no top-level font.fontStyle). If a distinguishable subset of vertices uses a larger fontSize combined with fontStyle=1 (bold), treat that subset as titles: set font.titleFontSize to their modal size and font.titleBold: true. Otherwise omit both title fields.
6. Extract edge defaults. Take the modal edge style string, but strip these per-edge coordinate keys before counting: entryX, entryY, exitX, exitY, entryDx, entryDy, exitDx, exitDy. Record arrow style from endArrow/endFill separately in edges.arrow. If any edges have dashed=1, collect their value (label) attributes. If ≥2 share a common token (e.g., all are labeled "async" or "optional"), add that token to edges.dashedFor.
7. Extract extras. sketch=1 seen on any vertex or edge → extras.sketch = true. Modal strokeWidth across vertices → extras.globalStrokeWidth (default 1).
8. Set provenance.
{
"source": { "type": "xml", "path": "<input absolute path>", "extracted_at": "YYYY-MM-DD" },
"confidence": "high"
}XML edge cases
| Situation | Behavior |
|---|---|
| Source has <3 distinct color pairs | Leave unfilled slots as null. Downgrade confidence to "medium". Summary warns the user. |
| Source has >7 color pairs | Keep the top 7 by frequency. Summary warns that some colors were dropped. |
Non-standard shape= keywords (e.g., shape=mxgraph.aws4.*) | These do not match the Step 4 precedence ladder, so the vertex falls through to rounded=0 for shape-class purposes. Iconography is lost; color, label, and edge style are still captured. Role inference still runs via the label-regex rules. Summary notes: "Non-standard shape library detected — iconography not preserved in preset (color and label captured)." |
| Non-English labels | The English-keyword regexes in step 4 will mostly miss; most vertices collapse to service. Palette/shapes/font/edges still captured correctly (they don't depend on label text). confidence stays "high". Summary notes: "Role labels not in English — `service`/`database`/`decision`/`container`/`external` inferred from shape class; other roles not mapped." |
File has no <mxCell vertex="1"> at all | Stop. Refuse to save. Message: "Nothing to learn from — source file has no shapes." |
Image extraction path
Input: path to a PNG/JPG (or any vision-readable image format). Output: candidate preset JSON. Inference-based; confidence: "medium" at best.
Prerequisite: the agent's vision capability must be available (same mechanism the main workflow's self-check uses). If vision is not available, stop and tell the user: "Image-based learning needs a vision-enabled model (Claude Sonnet or Opus). Re-run on such a model, or provide the `.drawio` source file instead."
Steps
1. Read the image. Use the agent's vision input — the same path the main workflow's step 5 uses to read exported PNGs during self-check.
2. Extract palette by visual inspection. Identify distinct fill-color regions on shape bodies.
For each distinct fill:
fillColor— quantize each RGB channel to the nearest multiple of 16. If the resulting HSL lightness is below 0.75, raise it to 0.85 (keep hue and saturation; set L=0.85; HSL→RGB round-trip). Emit as#RRGGBB. Drawio-standard pastels occupy L≈0.85–0.96; below 0.75 reads as "too dark for a fill color" and this step lifts it back into that range.strokeColor— read the matching border. If unreadable, derive from fill by darkening ~25% (match HSL, drop L by 0.25).
Map each (fillColor, strokeColor) pair to a named slot using this decision order:
1. Grey check first. If the fill has R, G, and B channels all within ±16 of each other (same definition as the XML path's grey-family rule), OR HSL saturation < 0.20, classify as neutral. This check wins regardless of hue angle. 2. Hue band otherwise. Use these explicit HSL hue ranges:
- 180°–260° →
primary(blue) - 80°–170° →
success(green) - 45°–65° →
warning(yellow) - 20°–44° →
accent(orange) - 0°–19° or 320°–360° →
danger(red/pink) - 260°–320° →
secondary(purple)
3. No band matched (gap regions at 65°–80° or 170°–180°) → spill to the nearest band by angular distance.
Collision rule. If ≥2 distinct fills land in the same slot, sort them by total pixel area covered in the image (descending). The largest keeps the canonical slot. Remaining fills spill to the nearest empty slot measured by hue-band angular distance — first to adjacent bands on either side, then farther out. If every slot is already filled, drop the extras and warn in the summary.
3. Extract shape vocabulary. Classify every visible shape by silhouette:
- rounded rectangle →
rounded=1 - sharp rectangle →
rounded=0 - circle / oval →
ellipse - diamond →
rhombus - cylinder (rectangle with curved top/bottom) →
shape=cylinder3 - titled container (header bar + nested children inside) →
swimlane;startSize=30 - dashed-bordered rectangle →
rounded=1;dashed=1
Role assignment uses the same label-text + shape rules as the XML path step 4. Visible labels are read via vision.
4. Extract fonts. Best-effort. Distinguishable categories:
- clearly serif →
fontFamily: "Georgia" - clearly monospaced →
fontFamily: "Courier New" - otherwise →
fontFamily: "Helvetica"
Size by relative appearance:
- small →
fontSize: 11 - medium →
fontSize: 12 - large →
fontSize: 14
If titles/container headers are distinctly larger or bolder → set titleFontSize accordingly and titleBold: true.
5. Extract edge defaults.
- Right-angle orthogonal arrows →
edges.style = "edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1". - Curved arrows → append
;curved=1toedges.style. - Filled triangle arrowheads →
edges.arrow = "endArrow=classic;endFill=1". - Open V-shaped arrowheads →
edges.arrow = "endArrow=open;endFill=0". - Any dashed arrows near labels like "optional", "async", "fallback", "secondary" → add those label tokens to
edges.dashedFor.
6. Extract extras.
- Visibly hand-drawn / rough / sketch look (wavy strokes, uneven fills) →
extras.sketch = true. - Heavy strokes (clearly >1.5× normal) →
extras.globalStrokeWidth = 2. - Otherwise default:
extras = { "sketch": false, "globalStrokeWidth": 1 }.
7. Set provenance and confidence.
{
"source": { "type": "image", "path": "<input absolute path>", "extracted_at": "YYYY-MM-DD" },
"confidence": "medium"
}Adjustments:
- <3 distinct shapes identifiable →
confidence: "low". - Image path stays at
"medium"by default. The only path to"high"is a strictly-verifiable signal: the source image was exported from drawio itself (recognizable drawio default chrome, grid, or a visible drawio watermark), and all seven palette slots are filled, and all seven roles are labeled. This preserves the semantic gap between inference-based (image) and parse-based (XML) provenance.
Image edge cases
| Situation | Behavior |
|---|---|
| Vision unavailable | Stop as described above — do not fall back to guessing. |
| Image has <3 identifiable shapes | Continue; mark confidence: "low"; summary explicitly warns the user that the preset is a loose approximation. |
| Image has no visible labels | Role assignment collapses to shape-class only: cylinders → database, diamonds → decision, swimlanes → container, dashed-bordered rectangles with grey fill → external, everything else → service. Palette/font/edges still captured. Summary notes: "No labels readable — semantic roles beyond shape-class not inferred." |
| Two palette slots would land in the same hue family | Keep the more frequent one in its canonical slot; spill the other to the adjacent empty slot (rule in step 2). |
| Image has more than 7 distinct fills | Keep the 7 most area-covering fills per the Step 2 collision rule. Summary warns that some colors were dropped. |
Style Presets — Learn, Apply, Manage
A style preset is a named JSON file capturing a user's visual preferences — palette, shape vocabulary, fonts, edge style. When a preset is active, it fully replaces the built-in conventions in SKILL.md's color/shape/edge tables.
Read this file when:
- The user asks to "learn", "save", "remember", or "extract" a style from a file
- The user wants to manage existing presets (list, set default, delete, rename)
- You've resolved an active preset in Step 0 and need the application rules
- You need to validate a preset file before loading it
Locations and lookup order
1. ~/.drawio-skill/styles/<name>.json — user presets (survive git pull). 2. <this-skill-dir>/styles/built-in/<name>.json — built-ins shipped with the skill (default, corporate, handdrawn).
A user preset shadows a built-in of the same name.
Only user presets can have "default": true. When the user says "make `<built-in-name>` my default", copy the built-in JSON to ~/.drawio-skill/styles/<name>.json first, then set default: true on the copy — leave the shipped built-in untouched.
Name normalisation: always lowercase the user-provided name before writing or looking up files (the preset schema enforces lowercase; uppercase names will fail validation).
Applying a preset
When SKILL.md's Step 0 identified a preset, it fully replaces the built-in palette, shape keywords, edge defaults, and font for this diagram — do not mix values from the built-in color table.
Color lookup. For each role a shape plays (service / database / queue / gateway / error / external / security), resolve preset.roles[role] to a slot name, then preset.palette[<slot>] to the (fillColor, strokeColor) pair. If roles[role] is unset or the resolved slot is null, follow this fallback ladder:
1. Try the role's canonical slot (service→primary, database→success, queue→warning, gateway→accent, error→danger, external→neutral, security→secondary). 2. If that slot is also empty, pick the most-populated non-null slot in the preset. 3. Never reach into the built-in color table — the preset is authoritative.
Decision and container shapes are not in preset.roles — they have shape vocabulary (preset.shapes.decision, preset.shapes.container) but no role-to-slot mapping. Pick their colors as follows:
- Decision (rhombus) → use
preset.palette.warning(the canonical yellow slot in the built-in conventions). Ifwarningis empty, apply the slot-fallback ladder above starting fromwarning. - Container (swimlane) → use the palette slot matching the tier/grouping the container represents (e.g. a "Services" tier container uses
primary; a "Data" tier usessuccess). If no tier signal is available, default toprimary.
Shape keywords. Use preset.shapes[role] as the prefix of the vertex style string (before whiteSpace=wrap;html=1;...). Example: for a database role, if preset.shapes.database = "shape=cylinder3", the vertex style starts shape=cylinder3;whiteSpace=wrap;html=1;fillColor=.... The six named shape keys are service, database, queue, decision, external, container. Roles gateway, error, and security reuse preset.shapes.service unless the preset explicitly populates a key with their name.
Edges. Use preset.edges.style as the base edge style string. Append preset.edges.arrow. Per-edge routing keys (exitX/exitY/entryX/entryY/...) are still added by the usual routing rules in SKILL.md. If the flow between two shapes matches a token from preset.edges.dashedFor (either because the user's prompt used that word, or because one end of the edge plays a role whose typical relation is "optional"), append ;dashed=1 to the edge style.
Fonts. Append fontFamily=<preset.font.fontFamily>;fontSize=<preset.font.fontSize> to every vertex style. Container headers and swimlane titles additionally get fontSize=<preset.font.titleFontSize>;fontStyle=1 when preset.font.titleBold is true.
Extras.
preset.extras.sketch === true→ appendsketch=1to every vertex style and every edge style.preset.extras.globalStrokeWidth !== 1(any value other than the drawio default of 1, including0.5) → appendstrokeWidth=<n>to every vertex style and every edge style.
Interaction with diagram-type presets (ERD / UML / Sequence / ML / Flowchart). Diagram-type presets set structural style keywords that the user preset must preserve (e.g. ERD tables rely on shape=table;startSize=30;container=1;childLayout=tableLayout;...). The rule: keep the diagram-type preset's structural keywords, then layer the user preset's color / font / edge / extras on top. When a diagram-type preset hardcodes a color (fillColor=#dae8fc, etc.) that conflicts with the user preset, the user preset's color wins. Exception: fillColor=none is structural — do not replace it with a palette color.
Learn flow
Triggers: "learn my style from <path> as <name>", "save this as <name> style", "remember this style as <name>".
Dispatch by file extension:
.drawio,.xml→ XML path.png,.jpg,.jpeg,.svg(rasterized flat image) → image path
Steps:
1. Load the extraction reference. Read references/style-extraction.md into context. 2. Extract following the XML path or image path procedure in the reference. 3. Normalize and build candidate. Convert the user-provided preset name to lowercase. Use this normalized name for ALL file paths in this flow. Build the candidate preset JSON and write it to /tmp/drawio-preset-<name>.json (where <name> is the already-normalized name). Do not save to ~/.drawio-skill/styles/<name>.json yet. 4. Render a sample using the sample-diagram skeleton in references/style-extraction.md, parameterized by the candidate preset. Export PNG to ./preset-<name>-sample.png using the same drawio -x -f png -e -s 2 -o ./preset-<name>-sample.png /tmp/drawio-preset-<name>.drawio command the main workflow uses, then run repair_png.py on it (see the Rendering the sample steps in style-extraction.md). 5. Show the user:
- Preset summary table (palette hex values, shapes per role, font, edge style, extras).
- The sample PNG path (and embed the image if the environment supports it).
- Provenance line:
source.type,source.path,extracted_at,confidence.
6. Wait for approval:
- "save" / "looks good" → write candidate to
~/.drawio-skill/styles/<name>.json. Create~/.drawio-skill/styles/if it doesn't exist. Delete tempfile and sample PNG. - "change
<field>to<value>" → edit the in-memory candidate, re-render, re-ask. - "cancel" / "abort" / "no" → delete tempfile and sample PNG; nothing saved.
Error behavior:
| Failure | Behavior |
|---|---|
| Source path does not exist | Stop; report path not found. |
| XML parse fails | Stop; report the parse error; suggest opening the file in drawio desktop to repair. |
| Image vision unavailable | Stop; tell user to re-run on a vision-capable model or provide the .drawio file. |
| Extraction yields 0 vertices / shapes | Stop; refuse to save. |
| Extraction yields <3 distinct color pairs | Continue; mark confidence: "low" (image) or "medium" (XML); warn in summary. |
| Preset name collides with existing user preset | Ask: overwrite, or pick a new name. |
| Preset name collides with a built-in preset | Save to user dir (shadows the built-in); warn once. |
| Sample render fails | Still show summary; note "could not render sample — saving on your OK anyway". Do not block. |
Management operations
All operations are natural language — no slash commands.
Apply name normalisation (lowercase) to all `<name>`, `<a>`, `<b>` arguments before any file operation.
| User says | Agent does |
|---|---|
| "list my styles", "what styles do I have", "show me my style presets" | Read ~/.drawio-skill/styles/ and <this-skill-dir>/styles/built-in/. Print a table: name, location (user/built-in), source.type, confidence, default flag. Built-ins shadowed by a user preset are marked so. |
"show my <name> style", "what's in <name>" | Print the preset JSON (pretty-printed) + a one-line summary (source, confidence, is-default). |
"make <name> the default", "set <name> as default" | If <name> is a user preset: set default: true on it; clear default on any other user preset that had it; save both files. If <name> is a built-in: copy <this-skill-dir>/styles/built-in/<name>.json → ~/.drawio-skill/styles/<name>.json first, then set default: true on the copy. Never mutate the shipped built-in. |
| "remove default", "unset default" | Clear default: true from whichever user preset has it. |
"delete <name>", "remove <name>" | Confirm first. Then rm ~/.drawio-skill/styles/<name>.json. Refuse to delete files under <this-skill-dir>/styles/built-in/ — suggest shadowing with a user preset of the same name. |
"rename <a> to <b>" | mv ~/.drawio-skill/styles/<a>.json ~/.drawio-skill/styles/<b>.json, then update the name field inside. Fails if <a> is a built-in (offer to copy-then-rename instead). |
"learn my style from <path> as <name>" | Dispatch to the Learn flow above. |
Preset file validation
When loading any preset (for generation or management), do a lightweight structural check:
- Required top-level fields present (
name,version,palette,roles,shapes,font,edges). version === 1.- Every populated palette slot has both
fillColorandstrokeColoras#RRGGBB. confidence∈ {"low","medium","high"} if present.
On validation failure:
- During generation: warn the user, fall back to built-in conventions for this one diagram, do not mutate the file.
- During learn: refuse to save the candidate; report which field failed.
Troubleshooting — Common Mistakes
Read this when something looks wrong in the output (rendering, export, layout, edges) or when a CLI invocation fails. Most rows have a one-line fix.
| Mistake | Fix |
|---|---|
Missing id="0" and id="1" root cells | Always include both at the top of <root> |
| Shapes not connected | source and target on edge must match existing shape id values |
Self-closing edge mxCell (<mxCell ... edge="1" />) | Use the expanded form with <mxGeometry relative="1" as="geometry" /> child — self-closing edges won't render |
-- inside XML comments | Illegal per XML spec — use single hyphens or rephrase |
Special characters in value | Use XML entities: & < > " |
Literal \n in label text | Use 
 for line breaks in value attributes |
| Overlapping shapes | Scale spacing with complexity (200–350px); leave routing corridors |
| Edges crossing through shapes | Add waypoints, distribute entry/exit points, or increase spacing |
| Arrowhead overlaps bend | Final edge segment before target must be ≥20px — increase spacing or add waypoints |
| Iteration loop never ends | After 5 rounds, suggest user open .drawio in draw.io desktop for fine-tuning |
command not found: draw.io after brew install --cask drawio | Homebrew installs the binary as drawio (no dot). Use drawio --version, not draw.io --version. The dot-name only exists inside the .app bundle (/Applications/draw.io.app/Contents/MacOS/draw.io) and on Windows (draw.io.exe). |
| Export command not found on macOS | Try full path /Applications/draw.io.app/Contents/MacOS/draw.io |
| Vision returns "Unable to resize image — dimensions exceed the 2576x2576px limit" | The preview PNG is too large for Claude's vision API. Re-export with --width 2000 instead of -s 2 (the flag is --width; there is no short -w — passing -w 2000 silently breaks input-file parsing and drawio errors with "input file/directory not found"). For very tall-narrow diagrams that still overshoot, use --height 2000 instead. |
| Linux: blank/error output headlessly | Prefix command with xvfb-run -a |
Linux: --no-sandbox placed before input file (parsed as filename) | Move --no-sandbox to the very end of the command (drawio-desktop#249, #1056) |
Linux: Failed to get 'appData' path / Home directory not accessible | export HOME=/tmp before invoking drawio (drawio-desktop#127) |
Linux server: segfault / EGL / MESA failed to load driver errors | Add --disable-gpu (suppresses Chromium GL init when no GPU available) |
| PDF export fails | Ensure Chromium is available (draw.io bundles it on desktop) |
| Background color wrong in CLI export | Known CLI bug; add --transparent flag or set background via style |
| Vision returns 400 "Could not process image" on draft PNG | Re-export the preview without -e (issue #8). Root cause is a truncated IEND chunk in -e PNGs, not the zTXt chunk itself — but skipping -e for the preview is the simplest fix. |
Final -e PNG won't open in image viewers / vision APIs | Run python3 <this-skill-dir>/scripts/repair_png.py <path>. draw.io CLI emits -e PNGs with an 8-byte truncation at IEND. SVG/PDF unaffected. |
WSL2: drawio / draw.io not found | The CLI lives on the Windows side. Use the Windows desktop exe via /mnt/c: "/mnt/c/Program Files/draw.io/draw.io.exe" (or per-user "/mnt/c/Users/<you>/AppData/Local/Programs/draw.io/draw.io.exe"). |
WSL2: opening an exported file fails with a /mnt/c/...-style path | cmd.exe can't resolve WSL paths — convert first: cmd.exe /c start "" "$(wslpath -w diagram.drawio.png)". The empty "" after start is the (required) window title. |
| Browser URL opens to a blank/empty diagram (Windows/WSL2) | cmd.exe's start treats & as a separator and drops everything after # — so the #R…/#create=… fragment (the whole diagram) is lost. Never pass the URL straight to start. Write a .url shortcut file and open that (see "WSL2 / Windows" below). |
WSL2 / Windows specifics
Locate the CLI. Detect WSL2 with grep -qi microsoft /proc/version. On WSL2 the export CLI is the Windows desktop exe, reached through /mnt/c (quote the path — it contains a space):
"/mnt/c/Program Files/draw.io/draw.io.exe" --version
# per-user install fallback:
"/mnt/c/Users/$USER/AppData/Local/Programs/draw.io/draw.io.exe" --versionOpen a file. Convert the WSL path to a Windows path first; cmd.exe cannot follow /mnt/c/...:
cmd.exe /c start "" "$(wslpath -w diagram.drawio.png)"Open a browser-fallback URL. cmd.exe /c start strips the URL fragment (& ends the command, #… is dropped) — and the fragment carries the entire diagram. Write a .url shortcut and open it instead, so the URL survives intact:
URL=$(python3 <this-skill-dir>/scripts/encode_drawio_url.py --edit diagram.drawio)
TMP=$(mktemp --suffix=.url)
printf '[InternetShortcut]\r\nURL=%s\r\n' "$URL" > "$TMP"
cmd.exe /c start "" "$(wslpath -w "$TMP")"On native Windows the same .url-file trick applies (start "" "%TEMP%\d.url"). On macOS/Linux just open "$URL" / xdg-open "$URL" — no workaround needed.
#!/usr/bin/env python3
"""Find AI / LLM brand logos (OpenAI, Claude, Gemini, ...) as draw.io styles.
draw.io's bundled shape libraries have no modern AI/LLM brand logos, so an
"LLM app architecture" renders as generic boxes. This resolves a brand name to a
draw.io `image` style that references the matching SVG from the lobe-icons set
(https://github.com/lobehub/lobe-icons, MIT) on the unpkg CDN.
python3 aiicons.py "openai"
python3 aiicons.py "claude" --json
python3 aiicons.py "langchain" --variant mono --size 48
The icon is referenced by URL (data/lobe-icons.json carries only the name list,
not the assets), so draw.io fetches it from the CDN when the diagram is rendered
or opened. That means **network is required at render time**; an offline export
draws a blank box. Use --embed to fetch the SVG once and inline it as a
self-contained data URI instead (portable, no network at render time).
The logos are trademarks of their respective owners and are referenced here for
identification only — the same basis on which draw.io ships AWS/Azure icons.
Usage: python3 aiicons.py <query> [--limit N] [--variant color|mono|text]
[--size PX] [--embed] [--json] [--list]
"""
import argparse
import base64
import json
import os
import re
import sys
import urllib.request
MANIFEST = os.path.join(os.path.dirname(__file__), "..", "data", "lobe-icons.json")
STYLE = ("shape=image;html=1;imageAspect=0;aspect=fixed;"
"verticalLabelPosition=bottom;verticalAlign=top;image=")
_VARIANT = re.compile(r"-(color|text)$")
# Common RAG/LLM data stores that lobe-icons lacks, mapped to simple-icons
# slugs (https://simpleicons.org, CC0). Served from the simple-icons CDN. Each
# slug below is verified to return HTTP 200 at https://cdn.simpleicons.org/<slug>.
_SIMPLEICONS_CDN = "https://cdn.simpleicons.org/"
_SUPPLEMENT = {
"qdrant": "qdrant",
"milvus": "milvus",
"supabase": "supabase",
"redis": "redis",
"postgresql": "postgresql",
"mongodb": "mongodb",
"elasticsearch": "elasticsearch",
"neo4j": "neo4j",
"kafka": "apachekafka",
"clickhouse": "clickhouse",
"duckdb": "duckdb",
"mysql": "mysql",
"sqlite": "sqlite",
"cassandra": "apachecassandra",
"snowflake": "snowflake",
"databricks": "databricks",
"mariadb": "mariadb",
"couchbase": "couchbase",
}
def families(icons):
"""base brand name -> set of its variant filenames (without .svg)."""
fam = {}
for name in icons:
base = _VARIANT.sub("", name)
fam.setdefault(base, set()).add(name)
return fam
def squish(s):
return re.sub(r"[^a-z0-9]", "", s.lower())
def search(fam, query, limit):
"""Rank brand bases against the query (squished + per-token matching)."""
q = squish(query)
tokens = [t for t in re.findall(r"[a-z0-9]+", query.lower()) if t]
scored = {}
for base in fam:
b = squish(base)
s = 0
if q and q == b:
s = 100
elif q and b.startswith(q):
s = 60
elif q and q in b:
s = 40
for t in tokens:
if t == b:
s = max(s, 90)
elif len(t) >= 3 and b.startswith(t):
s = max(s, 50)
elif len(t) >= 3 and t in b:
s = max(s, 30)
if s:
scored[base] = s
return sorted(scored, key=lambda base: (-scored[base], base))[:limit]
def search_supplement(query):
"""Fall back to the simple-icons supplement (exact or substring match)."""
q = squish(query)
if not q:
return None
if q in _SUPPLEMENT:
return q
for brand in _SUPPLEMENT:
if q in brand or brand in q:
return brand
return None
def pick_variant(base, variants, prefer):
order = {"color": ["-color", "", "-text"],
"mono": ["", "-color", "-text"],
"text": ["-text", "-color", ""]}[prefer]
for suffix in order:
cand = base + suffix
if cand in variants:
return cand
return next(iter(sorted(variants)), None)
def main():
ap = argparse.ArgumentParser(description="Find AI/LLM brand logos as draw.io styles (lobe-icons via CDN).")
ap.add_argument("query", nargs="?", help='brand name, e.g. "openai" or "claude"')
ap.add_argument("--limit", type=int, default=8)
ap.add_argument("--variant", choices=["color", "mono", "text"], default="color")
ap.add_argument("--size", type=int, default=48, help="cell width/height in px (icons are square)")
ap.add_argument("--embed", action="store_true",
help="inline the SVG as a data URI (fetches it now; portable, no network at render time)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--list", action="store_true", help="list all brand names and exit")
args = ap.parse_args()
if not os.path.exists(MANIFEST):
sys.exit(f"error: manifest not found at {MANIFEST}")
manifest = json.load(open(MANIFEST, encoding="utf-8"))
fam = families(manifest["icons"])
cdn = manifest["cdn"]
if args.list:
for base in sorted(fam):
print(base)
return
if not args.query:
ap.error("a query is required (or use --list)")
matches = search(fam, args.query, args.limit)
results = []
if matches:
for base in matches:
file = pick_variant(base, fam[base], args.variant)
url = f"{cdn}{file}.svg"
if args.embed:
try:
svg = urllib.request.urlopen(url, timeout=15).read()
except Exception as exc: # noqa: BLE001 - report and skip
sys.stderr.write(f"warning: could not fetch {url} ({exc})\n")
continue
# Rewrite the 1em intrinsic size so draw.io scales the inlined SVG.
svg = svg.replace(b'width="1em"', b'width="24"').replace(b'height="1em"', b'height="24"')
image = "data:image/svg+xml;base64," + base64.b64encode(svg).decode()
else:
image = url
results.append({"brand": base, "file": file, "w": args.size, "h": args.size,
"style": STYLE + image})
else:
# lobe has no logo for this brand; fall back to the simple-icons supplement.
brand = search_supplement(args.query)
if brand:
slug = _SUPPLEMENT[brand]
url = _SIMPLEICONS_CDN + slug
image = url
if args.embed:
try:
svg = urllib.request.urlopen(url, timeout=15).read()
image = "data:image/svg+xml;base64," + base64.b64encode(svg).decode()
except Exception as exc: # noqa: BLE001 - keep the CDN URL
sys.stderr.write(f"warning: could not fetch {url} ({exc}); using CDN URL\n")
results.append({"brand": brand, "file": f"simpleicons:{slug}",
"w": args.size, "h": args.size, "style": STYLE + image})
if not results:
sys.exit(f"no logo for {args.query!r} — for a data store try a cylinder "
f"(shape=cylinder3) or shapesearch.py '{args.query} database'")
if args.json:
print(json.dumps(results, indent=2, ensure_ascii=False))
else:
for r in results:
shown = r["style"] if len(r["style"]) < 160 else r["style"][:157] + "..."
print(f"{r['brand']} ({r['file']}, {r['w']}x{r['h']})\n {shown}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Auto-layout a logical graph into draw.io XML using Graphviz.
Minimal layout pass for the drawio skill: takes a graph (nodes + edges as
JSON), runs `dot` to position the nodes, and emits a .drawio file with the
mxGeometry x/y filled in. draw.io routes the edges itself (orthogonal style).
This removes the manual-coordinate ceiling for medium/large diagrams.
Input JSON:
{
"direction": "TB", # TB (top-bottom, default) or LR (left-right)
"nodes": [
{"id": "a", "label": "Service A", "style": "rounded=1;...",
"width": 120, "height": 60}
],
"edges": [
{"source": "a", "target": "b", "label": "calls"}
]
}
Only "id" is required per node; label defaults to id and style/width/height
have defaults. Node ids must be unique and must not be "0" or "1" (reserved
for the draw.io root cells). Requires Graphviz `dot` on PATH.
Usage: python3 autolayout.py graph.json [-o diagram.drawio]
"""
import argparse
import json
import os
import shlex
import subprocess
import sys
from xml.sax.saxutils import escape
DEFAULT_W, DEFAULT_H = 120, 60
NODE_STYLE = "rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;"
EDGE_STYLE = "html=1;rounded=0;"
GROUP_STYLE = ("rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#999999;"
"verticalAlign=top;fontStyle=2;dashed=1;")
# Group colours come from the skill's own palette (styles/built-in/default.json)
# so there is a single source of truth, not a second list baked in here. When a
# grouped graph is laid out, each top-level group takes the next colour (cycled
# in a fixed, harmonious role order) so related modules read as a coloured
# cluster. Nodes that carry their own `style` keep it; only styleless grouped
# nodes are tinted. Disable with --mono.
_PALETTE_ORDER = ["primary", "success", "accent", "secondary", "warning", "danger", "neutral"]
_PALETTE_FILE = os.path.join(os.path.dirname(__file__), "..", "styles", "built-in", "default.json")
_FALLBACK_PALETTE = [("#dae8fc", "#6c8ebf"), ("#d5e8d4", "#82b366"), ("#ffe6cc", "#d79b00"),
("#e1d5e7", "#9673a6"), ("#fff2cc", "#d6b656"), ("#f8cecc", "#b85450")]
def load_palette():
"""Ordered (fill, stroke) list from the default preset's palette; fall back
to the same colours inline if the preset file can't be read."""
try:
with open(_PALETTE_FILE, encoding="utf-8") as fh:
pal = json.load(fh)["palette"]
colors = [(pal[r]["fillColor"], pal[r]["strokeColor"]) for r in _PALETTE_ORDER if r in pal]
if colors:
return colors
except (OSError, KeyError, ValueError):
pass
return _FALLBACK_PALETTE
PALETTE = load_palette()
# Uniform container padding; the title sits in the top pad (verticalAlign=top).
# dot's cluster margin is set to this same value so each container box equals
# dot's cluster box — which dot guarantees never overlaps, at any nesting depth.
GROUP_PAD = 24
def attr(value):
return escape(str(value), {'"': """})
def dot_quote(value):
# Wrap as a DOT double-quoted string, escaping backslash and quote so ids
# with those characters can't corrupt the Graphviz input.
return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
def snap(value, grid=10):
# Align to the grid the skill uses everywhere (multiples of 10).
return int(round(value / grid) * grid)
def group_tree(nodes):
"""Parse hierarchical `group` paths ("a/b") into a container tree.
Returns (gpath, direct, children, ordered):
gpath[node_id] = tuple of path segments (the node's deepest container)
direct[path] = node ids whose group is exactly this path
children[path] = child container paths
ordered = all container paths, shallow-to-deep (stable)
"""
gpath, direct, paths = {}, {}, set()
for node in nodes:
g = node.get("group")
if g is None or str(g).strip("/") == "":
continue
t = tuple(str(g).strip("/").split("/"))
gpath[node["id"]] = t
direct.setdefault(t, []).append(node["id"])
for k in range(1, len(t) + 1):
paths.add(t[:k])
children = {}
for p in sorted(paths):
if len(p) > 1:
children.setdefault(p[:-1], []).append(p)
ordered = sorted(paths, key=lambda p: (len(p), p))
return gpath, direct, children, ordered
def build_dot(graph):
rankdir = "LR" if str(graph.get("direction", "TB")).upper() == "LR" else "TB"
# splines=ortho makes dot route edges as orthogonal polylines; we replay
# those bends as draw.io waypoints so edges go around nodes, not through them.
lines = [f"digraph G {{ rankdir={rankdir}; splines=ortho; node [shape=box fixedsize=true];"]
# Group nodes into (possibly nested) clusters so dot keeps each group
# together; a node's first appearance fixes its cluster, so list members
# before the size attributes. The cluster margin reserves room for the
# padded container boxes we draw below (extra on Y for the title strip) so
# neighbouring boxes do not overlap.
_, direct, children, ordered = group_tree(graph["nodes"])
cidx = {p: i for i, p in enumerate(ordered)}
def emit_cluster(p, pad):
lines.append(f'{pad}subgraph cluster_{cidx[p]} {{ margin={GROUP_PAD};')
for c in children.get(p, []):
emit_cluster(c, pad + " ")
lines.extend(f'{pad} {dot_quote(m)};' for m in direct.get(p, []))
lines.append(pad + "}")
for root in [p for p in ordered if len(p) == 1]:
emit_cluster(root, "")
for node in graph["nodes"]:
# Pass our pixel sizes to dot as inches so it lays out at the real size.
w = node.get("width", DEFAULT_W) / 72.0
h = node.get("height", DEFAULT_H) / 72.0
lines.append(f'{dot_quote(node["id"])} [width={w:.4f} height={h:.4f}];')
for edge in graph.get("edges", []):
lines.append(f'{dot_quote(edge["source"])} -> {dot_quote(edge["target"])};')
lines.append("}")
return "\n".join(lines)
def layout(dot_src):
"""Run `dot -Tplain`; return (height_in, {id: (xc, yc)}, {(src, dst): [(x, y), ...]}).
Node coords are inches (bottom-left origin); each edge's value is the list
of orthogonal control points dot computed for routing, endpoints included.
"""
try:
proc = subprocess.run(
["dot", "-Tplain"], input=dot_src,
capture_output=True, text=True, check=True,
)
except FileNotFoundError:
sys.exit("error: Graphviz `dot` not found on PATH (brew install graphviz)")
except subprocess.CalledProcessError as exc:
sys.exit(f"error: dot failed: {exc.stderr.strip()}")
height, pos, edges = 0.0, {}, {}
for line in proc.stdout.splitlines():
tok = shlex.split(line)
if not tok:
continue
if tok[0] == "graph":
height = float(tok[3]) # graph scale width height
elif tok[0] == "node":
pos[tok[1]] = (float(tok[2]), float(tok[3])) # node name x y ...
elif tok[0] == "edge": # edge tail head n x1 y1 ... xn yn
n = int(tok[3])
edges[(tok[1], tok[2])] = [
(float(tok[4 + 2 * i]), float(tok[5 + 2 * i])) for i in range(n)
]
return height, pos, edges
def group_style(stroke):
"""Container box styled with a group's colour (coloured border + title)."""
return (f"rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor={stroke};"
f"fontColor={stroke};verticalAlign=top;fontStyle=2;dashed=1;")
def to_drawio(graph, height, pos, edge_pts, color=True):
nodes = graph["nodes"]
# Absolute snapped rect for every placed node.
rects = {}
for node in nodes:
nid = node["id"]
if nid not in pos:
continue
w, h = node.get("width", DEFAULT_W), node.get("height", DEFAULT_H)
xc, yc = pos[nid]
x = snap(xc * 72 - w / 2)
y = snap((height - yc) * 72 - h / 2) # flip: dot origin is bottom-left
rects[nid] = (x, y, w, h)
# Parse the (possibly nested) group tree and assign each container a
# collision-free id and a title (the path's last segment, or a member's groupLabel).
gpath, direct, children, ordered = group_tree(nodes)
# Assign each top-level group a palette colour, in order of first appearance.
top_order = []
for node in nodes:
t = gpath.get(node["id"])
if t and t[0] not in top_order:
top_order.append(t[0])
def gcolor(seg):
return PALETTE[top_order.index(seg) % len(PALETTE)]
used = {n["id"] for n in nodes}
label_override = {}
for node in nodes:
if node["id"] in gpath and "groupLabel" in node:
label_override.setdefault(gpath[node["id"]], str(node["groupLabel"]))
gid, glabel = {}, {}
for i, p in enumerate(ordered):
cid = f"group_{i}"
while cid in used: # never collide with a node id
cid += "_"
used.add(cid)
gid[p] = cid
glabel[p] = label_override.get(p, p[-1])
# Container bounding box (members + nested children + uniform padding),
# computed deepest-first so a parent can wrap its already-sized children.
gbox = {}
for p in sorted(ordered, key=len, reverse=True):
xs = [(rects[m][0], rects[m][1], rects[m][0] + rects[m][2], rects[m][1] + rects[m][3])
for m in direct.get(p, []) if m in rects]
xs += [(gbox[c][0], gbox[c][1], gbox[c][0] + gbox[c][2], gbox[c][1] + gbox[c][3])
for c in children.get(p, []) if c in gbox]
if not xs:
continue
x0 = min(b[0] for b in xs) - GROUP_PAD
y0 = min(b[1] for b in xs) - GROUP_PAD
x1 = max(b[2] for b in xs) + GROUP_PAD
y1 = max(b[3] for b in xs) + GROUP_PAD
gbox[p] = (x0, y0, x1 - x0, y1 - y0)
# Shift everything positive: a container's top padding can push its top edge
# above the page origin. Only translates when something would be negative.
absx = [r[0] for r in rects.values()] + [b[0] for b in gbox.values()]
absy = [r[1] for r in rects.values()] + [b[1] for b in gbox.values()]
dx = GROUP_PAD - min(absx) if absx and min(absx) < 0 else 0
dy = GROUP_PAD - min(absy) if absy and min(absy) < 0 else 0
def rebase(x, y, parent_path):
"""Absolute -> coordinates relative to parent_path's box (or shifted if top-level)."""
if parent_path is None:
return x + dx, y + dy, "1"
px, py, _, _ = gbox[parent_path]
return x - px, y - py, gid[parent_path]
cells = []
# Containers shallow-first so each parent precedes its children.
for p in ordered:
if p not in gbox:
continue
gx, gy, gw, gh = gbox[p]
x, y, parent = rebase(gx, gy, p[:-1] if len(p) > 1 else None)
gstyle = group_style(gcolor(p[0])[1]) if color else GROUP_STYLE
cells.append(
f' <mxCell id="{attr(gid[p])}" value="{attr(glabel[p])}" '
f'style="{gstyle}" vertex="1" parent="{attr(parent)}">\n'
f' <mxGeometry x="{x}" y="{y}" width="{gw}" height="{gh}" as="geometry"/>\n'
f" </mxCell>"
)
for node in nodes:
nid = node["id"]
if nid not in rects:
continue
rx, ry, w, h = rects[nid]
x, y, parent = rebase(rx, ry, gpath.get(nid) if gpath.get(nid) in gbox else None)
if node.get("style"):
style = node["style"] # explicit style always wins
elif color and nid in gpath:
fill, stroke = gcolor(gpath[nid][0]) # tint styleless nodes by group
style = f"rounded=1;whiteSpace=wrap;html=1;fillColor={fill};strokeColor={stroke};"
else:
style = NODE_STYLE
cells.append(
f' <mxCell id="{attr(nid)}" value="{attr(node.get("label", nid))}" '
f'style="{attr(style)}" vertex="1" parent="{attr(parent)}">\n'
f' <mxGeometry x="{x}" y="{y}" width="{w}" height="{h}" as="geometry"/>\n'
f" </mxCell>"
)
for i, edge in enumerate(graph.get("edges", [])):
# Drop the first/last points (they sit on the node borders, where
# draw.io attaches anyway) and replay the interior bends as waypoints.
interior = edge_pts.get((edge["source"], edge["target"]), [])[1:-1]
if interior:
points = "".join(
f'<mxPoint x="{snap(x * 72) + dx}" y="{snap((height - y) * 72) + dy}"/>'
for x, y in interior
)
geom = (f'<mxGeometry relative="1" as="geometry">'
f'<Array as="points">{points}</Array></mxGeometry>')
else:
geom = '<mxGeometry relative="1" as="geometry"/>'
cells.append(
f' <mxCell id="e{i}" value="{attr(edge.get("label", ""))}" '
f'style="{EDGE_STYLE}" edge="1" parent="1" '
f'source="{attr(edge["source"])}" target="{attr(edge["target"])}">\n'
f" {geom}\n"
f" </mxCell>"
)
return (
'<mxfile>\n <diagram id="autolayout" name="Page-1">\n'
' <mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" '
'tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" '
'pageWidth="850" pageHeight="1100" math="0" shadow="0">\n'
" <root>\n"
' <mxCell id="0"/>\n'
' <mxCell id="1" parent="0"/>\n'
+ "\n".join(cells)
+ "\n </root>\n </mxGraphModel>\n </diagram>\n</mxfile>\n"
)
def main():
ap = argparse.ArgumentParser(description="Auto-layout a graph JSON into draw.io XML.")
ap.add_argument("input", help="graph JSON file")
ap.add_argument("-o", "--output", help="output .drawio path (default: stdout)")
ap.add_argument("--mono", action="store_true",
help="don't colour groups by palette (monochrome boxes)")
args = ap.parse_args()
with open(args.input, encoding="utf-8") as f:
graph = json.load(f)
height, pos, edge_pts = layout(build_dot(graph))
xml = to_drawio(graph, height, pos, edge_pts, color=not args.mono)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(xml)
print(f"wrote {args.output} ({len(graph['nodes'])} nodes, "
f"{len(graph.get('edges', []))} edges)", file=sys.stderr)
else:
sys.stdout.write(xml)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Repair truncated IEND chunk in draw.io -e PNG exports (issue #8).
draw.io's CLI emits -e PNGs with the 4-byte IEND length field but missing
the 8 bytes of "IEND" type + CRC. Strict PNG decoders and vision APIs
(Anthropic included) reject the file with 400 "Could not process image".
SVG/PDF are unaffected.
Usage: python3 repair_png.py <path/to/diagram.drawio.png>
Idempotent: the endswith(IEND) guard makes this a no-op once draw.io
fixes the bug upstream, so it's safe to run unconditionally after every
-e PNG export.
"""
import sys
IEND = b"\x00\x00\x00\x00IEND\xaeB`\x82"
def repair(path: str) -> bool:
with open(path, "rb") as f:
data = f.read()
if data.endswith(IEND):
return False
if data.endswith(b"\x00\x00\x00\x00"):
data = data[:-4]
with open(path, "wb") as f:
f.write(data + IEND)
return True
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: repair_png.py <path>", file=sys.stderr)
sys.exit(2)
if repair(sys.argv[1]):
print(f"repaired {sys.argv[1]}")
Related skills
Forks & variants (1)
Drawio Skill has 1 known copy in the catalog totaling 1.7k installs. They canonicalize to this original listing.
- agents365-ai - 1.7k installs
How it compares
Pick drawio-skill for editable architecture and flow diagrams; use UI design skills when the deliverable is production screen mockups.
FAQ
Who is drawio-skill for?
Developers creating precise diagrams as draw.io XML with local desktop CLI export.
When should I use drawio-skill?
When diagrams need swimlanes, strict UML, embeddable exports, or draw.io native styling.
Is drawio-skill safe to install?
Review the Security Audits panel; it runs local draw.io desktop CLI on user machine.