Create .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.
RoleWidthHeightLevel-1 node (main step)20070Level-2 node (sub-step)18060Title banner40050Footer / badge16040
Colors must communicate semantics, not decorate. Pick ONE role per diagram:
Role A — Flow / Process (arrows show direction):
MeaningFill ColorHexStart / InputLight Blue#a5d8ffStep / ActionLight Blue#a5d8ffDecisionLight Yellow#fff3bfSuccess / OutputLight Green#b2f2bbExternal / PendingLight Orange#ffd8a8Error / AlertLight Red#ffc9c9
Role B — Layered Architecture (zones show grouping):
MeaningFill ColorHexUI / Frontend layerBlue zone#dbe4ffLogic / Agent layerPurple zone#e5dbffData / Tool layerGreen zone#d3f9d8
Role C — Comparison (side-by-side):
MeaningFill ColorHexThis sideLight Blue#a5d8ffThat sideLight 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 labelMinimum box width1–8 chars120 px9–14 chars160 px15–20 chars200 px21–28 chars240 px29+ charssplit 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:
json
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 70 }
roundness: { "type": 3 } for rounded cornersbackgroundColor: "#a5d8ff", fillStyle: "solid" for filledEllipse:
json
{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }
Diamond:
json
{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }
Labeled shape (container binding) — do NOT use "label": { "text": "..." } on shapes:
json
{
"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:
json
{
"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):
json
{
"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:
json
{
"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"json
{
"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 + 40UseFill ColorHexPrimary / InputLight Blue#a5d8ffSuccess / OutputLight Green#b2f2bbWarning / ExternalLight Orange#ffd8a8Processing / SpecialLight Purple#d0bfffError / CriticalLight Red#ffc9c9Notes / DecisionsLight Yellow#fff3bfStorage / DataLight Teal#c3fae8AnalyticsLight Pink#eebefa
UseFill ColorHexUI / Frontend layerBlue zone#dbe4ffLogic / Agent layerPurple zone#e5dbffData / Tool layerGreen zone#d3f9d8
#757575. Default #1e1e1e is best.#15803d not #22c55e, #2563eb not #4a9eed)#9a5030 not #c4795b)#b0b0b0, #999) on white — unreadablejson
{
"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.
bash
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:
bash
# 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:
bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-screenshot &
Then use the screenshot script:
bash
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.