Install
openclaw skills install hermes-excalidrawHand-drawn Excalidraw JSON diagrams — architecture, flow, sequence. Generate clean, well-laid-out charts with consistent box sizing, meaningful colors, and zero line crossings.
openclaw skills install hermes-excalidrawCreate .excalidraw files that can be drag-dropped onto excalidraw.com or shared via URL. No account, no API keys, just JSON.
Architecture diagrams, flowcharts, sequence diagrams, concept maps, process maps — any visual you want to communicate clearly.
.excalidraw via write_filescripts/upload.py in terminalBad diagrams fail for 4 reasons: inconsistent box sizes, meaningless colors, crossing lines, disorganized arrangement. Fix all four with these rules:
Never place elements ad-hoc. Design on a virtual grid:
All boxes in the same logical row or category MUST have identical dimensions. Use one size for all level-1 nodes, another for all level-2 nodes. Only vary size for intentional hierarchy.
| Role | Width | Height |
|---|---|---|
| Level-1 node (main step) | 200 | 70 |
| Level-2 node (sub-step) | 180 | 60 |
| Title banner | 400 | 50 |
| Footer / badge | 160 | 40 |
Colors must communicate semantics, not decorate. Pick ONE role per diagram:
Role A — Flow / Process (arrows show direction):
| Meaning | Fill Color | Hex |
|---|---|---|
| Start / Input | Light Blue | #a5d8ff |
| Step / Action | Light Blue | #a5d8ff |
| Decision | Light Yellow | #fff3bf |
| Success / Output | Light Green | #b2f2bb |
| External / Pending | Light Orange | #ffd8a8 |
| Error / Alert | Light Red | #ffc9c9 |
Role B — Layered Architecture (zones show grouping):
| Meaning | Fill Color | Hex |
|---|---|---|
| UI / Frontend layer | Blue zone | #dbe4ff |
| Logic / Agent layer | Purple zone | #e5dbff |
| Data / Tool layer | Green zone | #d3f9d8 |
Role C — Comparison (side-by-side):
| Meaning | Fill Color | Hex |
|---|---|---|
| This side | Light Blue | #a5d8ff |
| That side | Light Purple | #d0bfff |
Pick ONE role per diagram. Do NOT mix Role A and Role B colors in the same diagram.
Arrows must never cross other content. Techniques:
points: [[0,0],[dx,0],[dx,dy]] to route around obstaclesstartBinding/endBinding with fixedPoint so arrows snap to shape edges cleanlyLayout must match the conceptual model:
This is the #1 reason diagrams look broken after upload. Text that overflows its container makes the diagram unreadable. Prevent it with these rules:
Minimum box widths by text length (fontSize 16, Virgel font):
| Characters in label | Minimum box width |
|---|---|
| 1–8 chars | 120 px |
| 9–14 chars | 160 px |
| 15–20 chars | 200 px |
| 21–28 chars | 240 px |
| 29+ chars | split into two lines or reduce fontSize |
Width formula: box_width >= text_length * 9.5 (at fontSize 16, Virgel = ~9.5px per character)
Arrow label width: The arrow must be at least label_chars * 9.5 + 40 px long to fit the label clearly. If the flow is too short, stack arrows vertically rather than making them short.
Text length hard limits:
line1\nline2 for longer text (max 2 lines).What to check before saving:
Warning signs of overflow:
\n or widen boxtype, id (unique string), x, y, width, height
strokeColor: "#1e1e1e"backgroundColor: "transparent"fillStyle: "solid"strokeWidth: 2roughness: 1 (hand-drawn look)opacity: 100Rectangle:
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 70 }
roundness: { "type": 3 } for rounded cornersbackgroundColor: "#a5d8ff", fillStyle: "solid" for filledEllipse:
{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }
Diamond:
{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }
Labeled shape (container binding) — do NOT use "label": { "text": "..." } on shapes:
{
"type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 70,
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
"boundElements": [{ "id": "t_r1", "type": "text" }]
},
{
"type": "text", "id": "t_r1", "x": 105, "y": 118, "width": 190, "height": 25,
"text": "Start", "fontSize": 18, "fontFamily": 1, "strokeColor": "#1e1e1e",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "r1", "originalText": "Start", "autoResize": true
}
containerId is setx/y/width/height are approximate — Excalidraw recalculates them on loadoriginalText must match textfontFamily: 1 (Virgil/hand-drawn font)Labeled arrow — same container binding approach:
{
"type": "arrow", "id": "a1", "x": 300, "y": 135, "width": 200, "height": 0,
"points": [[0,0],[200,0]], "endArrowhead": "arrow",
"boundElements": [{ "id": "t_a1", "type": "text" }]
},
{
"type": "text", "id": "t_a1", "x": 370, "y": 118, "width": 60, "height": 25,
"text": "goes to", "fontSize": 14, "fontFamily": 1, "strokeColor": "#1e1e1e",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "a1", "originalText": "goes to", "autoResize": true
}
Standalone text (titles and annotations only — no container):
{
"type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello",
"fontSize": 24, "fontFamily": 1, "strokeColor": "#1e1e1e",
"originalText": "Hello", "autoResize": true
}
x is the LEFT edgetextAlign or width for positioningArrow:
{
"type": "arrow", "id": "a1", "x": 300, "y": 135, "width": 200, "height": 0,
"points": [[0,0],[200,0]], "endArrowhead": "arrow"
}
points: [dx, dy] offsets from element x, yendArrowhead: null | "arrow" | "bar" | "dot" | "triangle"strokeStyle: "solid" (default) | "dashed" | "dotted"{
"type": "arrow", "id": "a1", "x": 300, "y": 135, "width": 150, "height": 0,
"points": [[0,0],[150,0]], "endArrowhead": "arrow",
"startBinding": { "elementId": "r1", "fixedPoint": [1, 0.5] },
"endBinding": { "elementId": "r2", "fixedPoint": [0, 0.5] }
}
fixedPoint coordinates: top=[0.5,0], bottom=[0.5,1], left=[0,0.5], right=[1,0.5]
Array order = z-order (first = back, last = front). Emit progressively: background zones → shape → its bound text → its arrows → next shape.
Font sizes:
fontSize: 20 for titles and headingsfontSize: 16 for body text, labels, descriptionsfontSize: 14 for secondary annotations only (sparingly)fontSize below 14Element sizes:
MANDATORY: Pre-check text fit before finalizing
label_chars × 9.5 + 40| Use | Fill Color | Hex |
|---|---|---|
| Primary / Input | Light Blue | #a5d8ff |
| Success / Output | Light Green | #b2f2bb |
| Warning / External | Light Orange | #ffd8a8 |
| Processing / Special | Light Purple | #d0bfff |
| Error / Critical | Light Red | #ffc9c9 |
| Notes / Decisions | Light Yellow | #fff3bf |
| Storage / Data | Light Teal | #c3fae8 |
| Analytics | Light Pink | #eebefa |
| Use | Fill Color | Hex |
|---|---|---|
| UI / Frontend layer | Blue zone | #dbe4ff |
| Logic / Agent layer | Purple zone | #e5dbff |
| Data / Tool layer | Green zone | #d3f9d8 |
#757575. Default #1e1e1e is best.#15803d not #22c55e, #2563eb not #4a9eed)#9a5030 not #c4795b)#b0b0b0, #999) on white — unreadable{
"type": "excalidraw",
"version": 2,
"source": "hermes-agent",
"elements": [ ...elements... ],
"appState": { "viewBackgroundColor": "#ffffff" }
}
Save with write_file to any path, e.g. ~/diagrams/my_diagram.excalidraw.
python ~/.hermes/skills/creative/excalidraw/scripts/upload.py ~/diagrams/my_diagram.excalidraw
This encrypts client-side (AES-GCM), embeds the key in the URL fragment, and uploads to excalidraw.com. Each upload gets a unique URL — avoids the "file already exists" confirmation prompt. No account needed.
Requires: pip install cryptography
TEXT OVERFLOWS THE BOX (the most common failure mode):
label_chars × 9.5 + 40 px longDo NOT use "label" on shapes — this is NOT a valid Excalidraw property. It will be silently ignored, producing blank shapes. Always use container binding (containerId + boundElements).
Every bound text needs both sides linked — the shape needs boundElements: [{"id": "t_xxx", "type": "text"}] AND the text needs containerId: "shape_id".
Elements overlap when y-coordinates are close — always check that text, boxes, and labels don't stack on top of each other.
Arrow labels overflow short arrows — keep labels short or make arrows wider.
Center titles relative to the diagram — estimate total width and center the title text over it.
Draw decorations LAST — sun, stars, icons should appear at the end of the array so they're on top.
Color without meaning — every color choice must map to a concept in the diagram. Random color assignment is noise.
After uploading, always verify the result with a real screenshot. This catches layout problems that JSON inspection can't.
Requires PinchTab Chrome extension installed:
# 1. Navigate to the Excalidraw URL
curl -X POST http://localhost:9867/navigate \
-H "Content-Type: application/json" \
-d '{"url": "https://excalidraw.com/#json=<FILE_ID>,<KEY>"}'
# 2. Wait for render
sleep 4
# 3. Screenshot
curl -X POST http://localhost:9867/screenshot \
-H "Content-Type: application/json" \
-d '{"format": "jpeg", "quality": 85}' \
--output ~/diagrams/my_diagram.jpg
Then load the screenshot with vision_analyze to verify layout quality.
The user must first start Chrome manually with debug port:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-screenshot &
Then use the screenshot script:
python ~/work/ai/omnimcp-opencli/scripts/chrome_screenshot.py \
"https://excalidraw.com/#json=<FILE_ID>,<KEY>" \
~/diagrams/my_diagram.jpg
If problems found: patch the JSON and re-upload. Iterate until 8+/10.
See references/examples.md for copy-pasteable complete diagrams covering:
See references/skill-selection-model.md for the 3-layer skill selection architecture (Hermes/OpenClaw model) — useful when designing skill catalogs or building workflow diagrams about agent behavior.