
Excalidraw
- 324 installs
- 226k repo stars
- Updated August 5, 2026
- nousresearch/hermes-agent
Have Hermes agents create and update Excalidraw diagrams for architecture sketches, user flows, whiteboards, and shareable design artifacts during planning and build.
About
The excalidraw skill lets Hermes agents produce hand-drawn-style diagrams for architectures, sequences, and user journeys, turning conversational reasoning into persistent visual documents for engineering and product alignment.
- Architecture and flow diagrams
- Editable whiteboard artifacts
- Agent-authored visual docs
- Shareable design communication
Excalidraw by the numbers
- 324 all-time installs (skills.sh)
- +20 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #770 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nousresearch/hermes-agent --skill excalidrawAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 324 |
|---|---|
| repo stars | ★ 226k |
| Last updated | August 5, 2026 |
| Repository | nousresearch/hermes-agent ↗ |
What it does
Have Hermes agents create and update Excalidraw diagrams for architecture sketches, user flows, whiteboards, and shareable design artifacts during planning and build.
Files
Excalidraw Diagram Skill
Create diagrams by writing standard Excalidraw element JSON and saving as .excalidraw files. These files can be drag-and-dropped onto excalidraw.com for viewing and editing. No accounts, no API keys, no rendering libraries -- just JSON.
When to use
Generate .excalidraw files for architecture diagrams, flowcharts, sequence diagrams, concept maps, and more. Files can be opened at excalidraw.com or uploaded for shareable links.
Workflow
1. Load this skill (you already did) 2. Write the elements JSON -- an array of Excalidraw element objects 3. Save the file using write_file to create a .excalidraw file 4. Optionally upload for a shareable link using scripts/upload.py via terminal
Saving a Diagram
Wrap your elements array in the standard .excalidraw envelope and save with write_file:
{
"type": "excalidraw",
"version": 2,
"source": "hermes-agent",
"elements": [ ...your elements array here... ],
"appState": {
"viewBackgroundColor": "#ffffff"
}
}Save to any path, e.g. ~/diagrams/my_diagram.excalidraw.
Uploading for a Shareable Link
Run the upload script (located in this skill's scripts/ directory) via terminal:
python skills/diagramming/excalidraw/scripts/upload.py ~/diagrams/my_diagram.excalidrawThis uploads to excalidraw.com (no account needed) and prints a shareable URL. Requires the cryptography pip package (pip install cryptography).
---
Element Format Reference
Required Fields (all elements)
type, id (unique string), x, y, width, height
Defaults (skip these -- they're applied automatically)
strokeColor:"#1e1e1e"backgroundColor:"transparent"fillStyle:"solid"strokeWidth:2roughness:1(hand-drawn look)opacity:100
Canvas background is white.
Element Types
Rectangle:
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }roundness: { "type": 3 }for rounded cornersbackgroundColor: "#a5d8ff",fillStyle: "solid"for filled
Ellipse:
{ "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) -- create a text element bound to the shape:
WARNING: Do NOT use "label": { "text": "..." } on shapes. This is NOT a validExcalidraw property and will be silently ignored, producing blank shapes. You MUST
use the container binding approach below.
The shape needs boundElements listing the text, and the text needs containerId pointing back:
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 80,
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
"boundElements": [{ "id": "t_r1", "type": "text" }] },
{ "type": "text", "id": "t_r1", "x": 105, "y": 110, "width": 190, "height": 25,
"text": "Hello", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "r1", "originalText": "Hello", "autoResize": true }- Works on rectangle, ellipse, diamond
- Text is auto-centered by Excalidraw when
containerIdis set - The text
x/y/width/heightare approximate -- Excalidraw recalculates them on load originalTextshould matchtext- Always include
fontFamily: 1(Virgil/hand-drawn font)
Labeled arrow -- same container binding approach:
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "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": 130, "width": 60, "height": 20,
"text": "connects", "fontSize": 16, "fontFamily": 1, "strokeColor": "#1e1e1e",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "a1", "originalText": "connects", "autoResize": true }Standalone text (titles and annotations only -- no container):
{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20,
"fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Hello", "autoResize": true }xis the LEFT edge. To center at positioncx:x = cx - (text.length * fontSize * 0.5) / 2- Do NOT rely on
textAlignorwidthfor positioning
Arrow:
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
"points": [[0,0],[200,0]], "endArrowhead": "arrow" }points:[dx, dy]offsets from elementx,yendArrowhead:null|"arrow"|"bar"|"dot"|"triangle"strokeStyle:"solid"(default) |"dashed"|"dotted"
Arrow Bindings (connect arrows to shapes)
{
"type": "arrow", "id": "a1", "x": 300, "y": 150, "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]
Drawing Order (z-order)
- Array order = z-order (first = back, last = front)
- Emit progressively: background zones → shape → its bound text → its arrows → next shape
- BAD: all rectangles, then all texts, then all arrows
- GOOD: bg_zone → shape1 → text_for_shape1 → arrow1 → arrow_label_text → shape2 → text_for_shape2 → ...
- Always place the bound text element immediately after its container shape
Sizing Guidelines
Font sizes:
- Minimum
fontSize: 16 for body text, labels, descriptions - Minimum
fontSize: 20 for titles and headings - Minimum
fontSize: 14 for secondary annotations only (sparingly) - NEVER use
fontSizebelow 14
Element sizes:
- Minimum shape size: 120x60 for labeled rectangles/ellipses
- Leave 20-30px gaps between elements minimum
- Prefer fewer, larger elements over many tiny ones
Color Palette
See references/colors.md for full color tables. Quick reference:
| 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 |
Tips
- Use the color palette consistently across the diagram
- Text contrast is CRITICAL -- never use light gray on white backgrounds. Minimum text color on white:
#757575 - Do NOT use emoji in text -- they don't render in Excalidraw's font
- For dark mode diagrams, see
references/dark-mode.md - For larger examples, see
references/examples.md
Excalidraw Color Palette
Use these colors consistently across diagrams.
Primary Colors (for strokes, arrows, and accents)
| Name | Hex | Use |
|---|---|---|
| Blue | #4a9eed | Primary actions, links, data series 1 |
| Amber | #f59e0b | Warnings, highlights, data series 2 |
| Green | #22c55e | Success, positive, data series 3 |
| Red | #ef4444 | Errors, negative, data series 4 |
| Purple | #8b5cf6 | Accents, special items, data series 5 |
| Pink | #ec4899 | Decorative, data series 6 |
| Cyan | #06b6d4 | Info, secondary, data series 7 |
| Lime | #84cc16 | Extra, data series 8 |
Pastel Fills (for shape backgrounds)
| Color | Hex | Good For |
|---|---|---|
| Light Blue | #a5d8ff | Input, sources, primary nodes |
| Light Green | #b2f2bb | Success, output, completed |
| Light Orange | #ffd8a8 | Warning, pending, external |
| Light Purple | #d0bfff | Processing, middleware, special |
| Light Red | #ffc9c9 | Error, critical, alerts |
| Light Yellow | #fff3bf | Notes, decisions, planning |
| Light Teal | #c3fae8 | Storage, data, memory |
| Light Pink | #eebefa | Analytics, metrics |
Background Zones (use with opacity: 30-35 for layered diagrams)
| Color | Hex | Good For |
|---|---|---|
| Blue zone | #dbe4ff | UI / frontend layer |
| Purple zone | #e5dbff | Logic / agent layer |
| Green zone | #d3f9d8 | Data / tool layer |
Text Contrast Rules
- On white backgrounds: minimum text color is
#757575. Default#1e1e1eis best. - Colored text on light fills: use dark variants (
#15803dnot#22c55e,#2563ebnot#4a9eed) - White text: only on dark backgrounds (
#9a5030not#c4795b) - Never: light gray (
#b0b0b0,#999) on white -- unreadable
Excalidraw Dark Mode Diagrams
To create a dark-themed diagram, use a massive dark background rectangle as the first element in the array. Make it large enough to cover any viewport:
{
"type": "rectangle", "id": "darkbg",
"x": -4000, "y": -3000, "width": 10000, "height": 7500,
"backgroundColor": "#1e1e2e", "fillStyle": "solid",
"strokeColor": "transparent", "strokeWidth": 0
}Then use the following color palettes for elements on the dark background.
Text Colors (on dark)
| Color | Hex | Use |
|---|---|---|
| White | #e5e5e5 | Primary text, titles |
| Muted | #a0a0a0 | Secondary text, annotations |
| NEVER | #555 or darker | Invisible on dark bg! |
Shape Fills (on dark)
| Color | Hex | Good For |
|---|---|---|
| Dark Blue | #1e3a5f | Primary nodes |
| Dark Green | #1a4d2e | Success, output |
| Dark Purple | #2d1b69 | Processing, special |
| Dark Orange | #5c3d1a | Warning, pending |
| Dark Red | #5c1a1a | Error, critical |
| Dark Teal | #1a4d4d | Storage, data |
Stroke and Arrow Colors (on dark)
Use the standard Primary Colors from the main color palette -- they're bright enough on dark backgrounds:
- Blue
#4a9eed, Amber#f59e0b, Green#22c55e, Red#ef4444, Purple#8b5cf6
For subtle shape borders, use #555555.
Example: Dark mode labeled rectangle
Use container binding (NOT the "label" property, which doesn't work). On dark backgrounds, set text strokeColor to "#e5e5e5" so it's visible:
[
{
"type": "rectangle", "id": "r1",
"x": 100, "y": 100, "width": 200, "height": 80,
"backgroundColor": "#1e3a5f", "fillStyle": "solid",
"strokeColor": "#4a9eed", "strokeWidth": 2,
"roundness": { "type": 3 },
"boundElements": [{ "id": "t_r1", "type": "text" }]
},
{
"type": "text", "id": "t_r1",
"x": 105, "y": 120, "width": 190, "height": 25,
"text": "Dark Node", "fontSize": 20, "fontFamily": 1,
"strokeColor": "#e5e5e5",
"textAlign": "center", "verticalAlign": "middle",
"containerId": "r1", "originalText": "Dark Node", "autoResize": true
}
]Note: For standalone text elements on dark backgrounds, always set "strokeColor": "#e5e5e5" explicitly. The default #1e1e1e is invisible on dark.
Excalidraw Diagram Examples
Complete, copy-pasteable examples. Wrap each in the .excalidraw envelope before saving:
{
"type": "excalidraw",
"version": 2,
"source": "hermes-agent",
"elements": [ ...elements from examples below... ],
"appState": { "viewBackgroundColor": "#ffffff" }
}IMPORTANT: All text labels on shapes and arrows use container binding (containerId+boundElements).
Do NOT use the non-existent "label" property -- it will be silently ignored, producing blank shapes.---
Example 1: Two Connected Labeled Boxes
A minimal flowchart with two boxes and an arrow between them.
[
{ "type": "text", "id": "title", "x": 280, "y": 30, "text": "Simple Flow", "fontSize": 28, "fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Simple Flow", "autoResize": true },
{ "type": "rectangle", "id": "b1", "x": 100, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid", "boundElements": [{ "id": "t_b1", "type": "text" }, { "id": "a1", "type": "arrow" }] },
{ "type": "text", "id": "t_b1", "x": 105, "y": 130, "width": 190, "height": 25, "text": "Start", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e", "textAlign": "center", "verticalAlign": "middle", "containerId": "b1", "originalText": "Start", "autoResize": true },
{ "type": "rectangle", "id": "b2", "x": 450, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid", "boundElements": [{ "id": "t_b2", "type": "text" }, { "id": "a1", "type": "arrow" }] },
{ "type": "text", "id": "t_b2", "x": 455, "y": 130, "width": 190, "height": 25, "text": "End", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e", "textAlign": "center", "verticalAlign": "middle", "containerId": "b2", "originalText": "End", "autoResize": true },
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0, "points": [[0,0],[150,0]], "endArrowhead": "arrow", "startBinding": { "elementId": "b1", "fixedPoint": [1, 0.5] }, "endBinding": { "elementId": "b2", "fixedPoint": [0, 0.5] } }
]---
Example 2: Photosynthesis Process Diagram
A larger diagram with background zones, multiple nodes, and directional arrows showing inputs/outputs.
[
{"type":"text","id":"ti","x":280,"y":10,"text":"Photosynthesis","fontSize":28,"fontFamily":1,"strokeColor":"#1e1e1e","originalText":"Photosynthesis","autoResize":true},
{"type":"text","id":"fo","x":245,"y":48,"text":"6CO2 + 6H2O --> C6H12O6 + 6O2","fontSize":16,"fontFamily":1,"strokeColor":"#757575","originalText":"6CO2 + 6H2O --> C6H12O6 + 6O2","autoResize":true},
{"type":"rectangle","id":"lf","x":150,"y":90,"width":520,"height":380,"backgroundColor":"#d3f9d8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","strokeWidth":1,"opacity":35},
{"type":"text","id":"lfl","x":170,"y":96,"text":"Inside the Leaf","fontSize":16,"fontFamily":1,"strokeColor":"#15803d","originalText":"Inside the Leaf","autoResize":true},
{"type":"rectangle","id":"lr","x":190,"y":190,"width":160,"height":70,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_lr","type":"text"},{"id":"a1","type":"arrow"},{"id":"a2","type":"arrow"},{"id":"a3","type":"arrow"},{"id":"a5","type":"arrow"}]},
{"type":"text","id":"t_lr","x":195,"y":205,"width":150,"height":20,"text":"Light Reactions","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"lr","originalText":"Light Reactions","autoResize":true},
{"type":"arrow","id":"a1","x":350,"y":225,"width":120,"height":0,"points":[[0,0],[120,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_a1","type":"text"}]},
{"type":"text","id":"t_a1","x":390,"y":205,"width":40,"height":20,"text":"ATP","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"a1","originalText":"ATP","autoResize":true},
{"type":"rectangle","id":"cc","x":470,"y":190,"width":160,"height":70,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","boundElements":[{"id":"t_cc","type":"text"},{"id":"a1","type":"arrow"},{"id":"a4","type":"arrow"},{"id":"a6","type":"arrow"}]},
{"type":"text","id":"t_cc","x":475,"y":205,"width":150,"height":20,"text":"Calvin Cycle","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"cc","originalText":"Calvin Cycle","autoResize":true},
{"type":"rectangle","id":"sl","x":10,"y":200,"width":120,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_sl","type":"text"},{"id":"a2","type":"arrow"}]},
{"type":"text","id":"t_sl","x":15,"y":210,"width":110,"height":20,"text":"Sunlight","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"sl","originalText":"Sunlight","autoResize":true},
{"type":"arrow","id":"a2","x":130,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
{"type":"rectangle","id":"wa","x":200,"y":360,"width":140,"height":50,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","boundElements":[{"id":"t_wa","type":"text"},{"id":"a3","type":"arrow"}]},
{"type":"text","id":"t_wa","x":205,"y":370,"width":130,"height":20,"text":"Water (H2O)","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"wa","originalText":"Water (H2O)","autoResize":true},
{"type":"arrow","id":"a3","x":270,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#4a9eed","strokeWidth":2,"endArrowhead":"arrow"},
{"type":"rectangle","id":"co","x":480,"y":360,"width":130,"height":50,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_co","type":"text"},{"id":"a4","type":"arrow"}]},
{"type":"text","id":"t_co","x":485,"y":370,"width":120,"height":20,"text":"CO2","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"co","originalText":"CO2","autoResize":true},
{"type":"arrow","id":"a4","x":545,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
{"type":"rectangle","id":"ox","x":540,"y":100,"width":100,"height":40,"backgroundColor":"#ffc9c9","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#ef4444","boundElements":[{"id":"t_ox","type":"text"},{"id":"a5","type":"arrow"}]},
{"type":"text","id":"t_ox","x":545,"y":105,"width":90,"height":20,"text":"O2","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"ox","originalText":"O2","autoResize":true},
{"type":"arrow","id":"a5","x":310,"y":190,"width":230,"height":-50,"points":[[0,0],[230,-50]],"strokeColor":"#ef4444","strokeWidth":2,"endArrowhead":"arrow"},
{"type":"rectangle","id":"gl","x":690,"y":195,"width":120,"height":60,"backgroundColor":"#c3fae8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","boundElements":[{"id":"t_gl","type":"text"},{"id":"a6","type":"arrow"}]},
{"type":"text","id":"t_gl","x":695,"y":210,"width":110,"height":25,"text":"Glucose","fontSize":18,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"gl","originalText":"Glucose","autoResize":true},
{"type":"arrow","id":"a6","x":630,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#22c55e","strokeWidth":2,"endArrowhead":"arrow"},
{"type":"ellipse","id":"sun","x":30,"y":110,"width":50,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","strokeColor":"#f59e0b","strokeWidth":2},
{"type":"arrow","id":"r1","x":55,"y":108,"width":0,"height":-14,"points":[[0,0],[0,-14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
{"type":"arrow","id":"r2","x":55,"y":162,"width":0,"height":14,"points":[[0,0],[0,14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
{"type":"arrow","id":"r3","x":28,"y":135,"width":-14,"height":0,"points":[[0,0],[-14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
{"type":"arrow","id":"r4","x":82,"y":135,"width":14,"height":0,"points":[[0,0],[14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null}
]---
Example 3: Sequence Diagram (UML-style)
Demonstrates a sequence diagram with actors, dashed lifelines, and message arrows.
[
{"type":"text","id":"title","x":200,"y":15,"text":"MCP Apps -- Sequence Flow","fontSize":24,"fontFamily":1,"strokeColor":"#1e1e1e","originalText":"MCP Apps -- Sequence Flow","autoResize":true},
{"type":"rectangle","id":"uHead","x":60,"y":60,"width":100,"height":40,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"boundElements":[{"id":"t_uHead","type":"text"}]},
{"type":"text","id":"t_uHead","x":65,"y":65,"width":90,"height":20,"text":"User","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"uHead","originalText":"User","autoResize":true},
{"type":"arrow","id":"uLine","x":110,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
{"type":"rectangle","id":"aHead","x":230,"y":60,"width":100,"height":40,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","strokeWidth":2,"boundElements":[{"id":"t_aHead","type":"text"}]},
{"type":"text","id":"t_aHead","x":235,"y":65,"width":90,"height":20,"text":"Agent","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"aHead","originalText":"Agent","autoResize":true},
{"type":"arrow","id":"aLine","x":280,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
{"type":"rectangle","id":"sHead","x":420,"y":60,"width":130,"height":40,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":2,"boundElements":[{"id":"t_sHead","type":"text"}]},
{"type":"text","id":"t_sHead","x":425,"y":65,"width":120,"height":20,"text":"Server","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"sHead","originalText":"Server","autoResize":true},
{"type":"arrow","id":"sLine","x":485,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
{"type":"arrow","id":"m1","x":110,"y":150,"width":170,"height":0,"points":[[0,0],[170,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_m1","type":"text"}]},
{"type":"text","id":"t_m1","x":165,"y":130,"width":60,"height":20,"text":"request","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m1","originalText":"request","autoResize":true},
{"type":"arrow","id":"m2","x":280,"y":200,"width":205,"height":0,"points":[[0,0],[205,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_m2","type":"text"}]},
{"type":"text","id":"t_m2","x":352,"y":180,"width":60,"height":20,"text":"tools/call","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m2","originalText":"tools/call","autoResize":true},
{"type":"arrow","id":"m3","x":485,"y":260,"width":-205,"height":0,"points":[[0,0],[-205,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","boundElements":[{"id":"t_m3","type":"text"}]},
{"type":"text","id":"t_m3","x":352,"y":240,"width":60,"height":20,"text":"result","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m3","originalText":"result","autoResize":true},
{"type":"arrow","id":"m4","x":280,"y":320,"width":-170,"height":0,"points":[[0,0],[-170,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","boundElements":[{"id":"t_m4","type":"text"}]},
{"type":"text","id":"t_m4","x":165,"y":300,"width":60,"height":20,"text":"response","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m4","originalText":"response","autoResize":true}
]---
Common Mistakes to Avoid
- Do NOT use `"label"` property -- this is the #1 mistake. It is NOT part of the Excalidraw file format and will be silently ignored, producing blank shapes with no visible text. Always use container binding (
containerId+boundElements) as shown in the examples above. - Every bound text needs both sides linked -- the shape needs
boundElements: [{"id": "t_xxx", "type": "text"}]AND the text needscontainerId: "shape_id". If either is missing, the binding won't work. - Include `originalText` and `autoResize: true` on all text elements -- Excalidraw uses these for proper text reflow.
- Include `fontFamily: 1` on all text elements -- without it, text may not render with the expected hand-drawn font.
- Elements overlap when y-coordinates are close -- always check that text, boxes, and labels don't stack on top of each other
- Arrow labels need space -- long labels like "ATP + NADPH" 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 -- cute illustrations (sun, stars, icons) should appear at the end of the array so they're drawn on top
#!/usr/bin/env python3
"""
Upload an .excalidraw file to excalidraw.com and print a shareable URL.
No account required. The diagram is encrypted client-side (AES-GCM) before
upload -- the encryption key is embedded in the URL fragment, so the server
never sees plaintext.
Requirements:
pip install cryptography
Usage:
python upload.py <path-to-file.excalidraw>
Example:
python upload.py ~/diagrams/architecture.excalidraw
# prints: https://excalidraw.com/#json=abc123,encryptionKeyHere
"""
import json
import os
import struct
import sys
import zlib
import base64
import urllib.request
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
except ImportError:
print("Error: 'cryptography' package is required for upload.")
print("Install it with: pip install cryptography")
sys.exit(1)
# Excalidraw public upload endpoint (no auth needed)
UPLOAD_URL = "https://json.excalidraw.com/api/v2/post/"
def concat_buffers(*buffers: bytes) -> bytes:
"""
Build the Excalidraw v2 concat-buffers binary format.
Layout: [version=1 (4B big-endian)] then for each buffer:
[length (4B big-endian)] [data bytes]
"""
parts = [struct.pack(">I", 1)] # version = 1
for buf in buffers:
parts.append(struct.pack(">I", len(buf)))
parts.append(buf)
return b"".join(parts)
def upload(excalidraw_json: str) -> str:
"""
Encrypt and upload Excalidraw JSON to excalidraw.com.
Args:
excalidraw_json: The full .excalidraw file content as a string.
Returns:
Shareable URL string.
"""
# 1. Inner payload: concat_buffers(file_metadata, data)
file_metadata = json.dumps({}).encode("utf-8")
data_bytes = excalidraw_json.encode("utf-8")
inner_payload = concat_buffers(file_metadata, data_bytes)
# 2. Compress with zlib
compressed = zlib.compress(inner_payload)
# 3. AES-GCM 128-bit encrypt
raw_key = os.urandom(16) # 128-bit key
iv = os.urandom(12) # 12-byte nonce
aesgcm = AESGCM(raw_key)
encrypted = aesgcm.encrypt(iv, compressed, None)
# 4. Encoding metadata
encoding_meta = json.dumps({
"version": 2,
"compression": "pako@1",
"encryption": "AES-GCM",
}).encode("utf-8")
# 5. Outer payload: concat_buffers(encoding_meta, iv, encrypted)
payload = concat_buffers(encoding_meta, iv, encrypted)
# 6. Upload
req = urllib.request.Request(UPLOAD_URL, data=payload, method="POST")
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status != 200:
raise RuntimeError(f"Upload failed with HTTP {resp.status}")
result = json.loads(resp.read().decode("utf-8"))
file_id = result.get("id")
if not file_id:
raise RuntimeError(f"Upload returned no file ID. Response: {result}")
# 7. Key as base64url (JWK 'k' format, no padding)
key_b64 = base64.urlsafe_b64encode(raw_key).rstrip(b"=").decode("ascii")
return f"https://excalidraw.com/#json={file_id},{key_b64}"
def main():
if len(sys.argv) < 2:
print("Usage: python upload.py <path-to-file.excalidraw>")
sys.exit(1)
file_path = sys.argv[1]
if not os.path.isfile(file_path):
print(f"Error: File not found: {file_path}")
sys.exit(1)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Basic validation: should be valid JSON with an "elements" key
try:
doc = json.loads(content)
except json.JSONDecodeError as e:
print(f"Error: File is not valid JSON: {e}")
sys.exit(1)
if "elements" not in doc:
print("Warning: File does not contain an 'elements' key. Uploading anyway.")
url = upload(content)
print(url)
if __name__ == "__main__":
main()