| description | allowed-tools | |||||
|---|---|---|---|---|---|---|
Create and edit Excalidraw diagrams — generate, modify, and clean .excalidraw files |
|
You can create and edit .excalidraw files directly as JSON. No CLI needed.
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [ ... ],
"appState": {
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}Every element needs these base properties. Use these defaults unless overridden:
{
"version": 1,
"versionNonce": <random 9-digit integer>,
"isDeleted": false,
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 2,
"opacity": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"seed": <random 9-digit integer>,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": <Date.now() timestamp>,
"link": null,
"locked": false
}Generate short descriptive IDs for new elements (e.g., "push_box", "arrow_push_to_merge"). For bound text labels, append _label (e.g., "push_box_label").
Elements use fractional indexing strings for z-order: "a0", "a1", ... "a9", "aA", ... "aZ", "aa", "ab", etc. When adding elements to an existing file, continue from the highest existing index.
{
"type": "rectangle",
"id": "my_box",
"x": 100, "y": 200,
"width": 200, "height": 80,
"roundness": { "type": 3 },
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"boundElements": [
{ "id": "my_box_label", "type": "text" }
]
}type:"rectangle","ellipse", or"diamond"roundness: { "type": 3 }= rounded corners (use for rectangles by default)roundness: { "type": 2 }= smooth curves (use for ellipses/diamonds)roundness: null= sharp corners
{
"type": "diamond",
"id": "gate",
"x": 600, "y": 150,
"width": 130, "height": 90,
"roundness": { "type": 2 },
"backgroundColor": "#fff3bf",
"fillStyle": "solid",
"boundElements": [
{ "id": "gate_label", "type": "text" }
]
}Diamonds render as rotated squares. The width/height define the bounding box — the visible diamond touches the midpoints of each side. Size diamonds ~1.5x larger than you'd expect since the usable interior is smaller.
"solid"— flat color fill (default, clean look)"hachure"— diagonal line fill (classic excalidraw hand-drawn feel)"cross-hatch"— cross-hatched lines (good for "proposed" or "in progress" elements)
0= Architect — clean, precise lines1= Artist — slight wobble2= Cartoonist — full hand-drawn feel (default)
| Color | Hex | Use for |
|---|---|---|
| Blue | #a5d8ff |
Standard steps |
| Purple | #d0bfff |
Tooling / automation |
| Green | #b2f2bb |
Success / prod deploy |
| Light green | #c3fae8 |
Positive state |
| Orange | #ffd8a8 |
Manual / warning |
| Yellow | #ffd43b |
Highlighted / attention |
| Light yellow | #fff3bf |
Decision gates |
| Red bg | #ffc9c9 |
Problem / danger |
| Red stroke | #ef4444 |
Problem border/text |
| Gray text | #757575 |
Annotations |
{
"type": "text",
"id": "my_label",
"x": 100, "y": 200,
"width": 0, "height": 0,
"text": "Some label",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Some label",
"autoResize": true,
"lineHeight": 1.25
}Note: width: 0, height: 0 is fine — excalidraw auto-calculates on first render.
{
"type": "text",
"id": "my_box_label",
"x": 110, "y": 210,
"width": 180, "height": 60,
"text": "Box Label",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "my_box",
"originalText": "Box Label",
"autoResize": true,
"lineHeight": 1.25
}Critical: The parent shape's boundElements must include { "id": "my_box_label", "type": "text" }.
5= Excalidraw default (hand-drawn, current — use this)1= Virgil (hand-drawn, legacy)2= Helvetica (clean sans-serif)3= Cascadia (monospace — good for code/technical labels)
- Title: 28
- Normal label: 16-18
- Annotation/note: 14
Plain lines without arrowheads — useful for separators, connectors, and underlines.
{
"type": "line",
"id": "separator",
"x": 100, "y": 300,
"width": 400, "height": 0,
"points": [[0, 0], [400, 0]],
"startArrowhead": null,
"endArrowhead": null
}- Same point system as arrows:
x, yis origin,pointsare relative - Can have multiple points for polylines
- Supports
strokeStyle:"solid","dashed","dotted" - Add
"roundness": { "type": 2 }for smooth curves through points
{
"type": "arrow",
"id": "arrow_a_to_b",
"x": 300, "y": 240,
"width": 50, "height": 0,
"points": [[0,0], [50, 0]],
"startBinding": {
"elementId": "box_a",
"mode": "orbit",
"fixedPoint": [1, 0.5]
},
"endBinding": {
"elementId": "box_b",
"mode": "orbit",
"fixedPoint": [0, 0.5]
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
}x, yis the starting position of the arrowpointsare relative tox, y: first point is always[0, 0]width= max x extent,height= max y extent of the points
fixedPoint: [x, y]where x,y are 0-1 relative to the target element[0, 0.5]= left center[1, 0.5]= right center[0.5, 0]= top center[0.5, 1]= bottom center
mode:"orbit"(default) or"inside"- Set to
nullfor unbound arrows
Critical: The target shape's boundElements must include { "id": "arrow_a_to_b", "type": "arrow" }.
endArrowhead:"arrow"(default),"triangle","bar","dot",nullstartArrowhead: same options (for bidirectional arrows)strokeStyle:"solid","dashed","dotted"
Use 3+ points with roundness for smooth curves:
{
"type": "arrow",
"id": "curved_arrow",
"x": 300, "y": 240,
"width": 150, "height": 100,
"points": [[0, 0], [75, 50], [150, 100]],
"roundness": { "type": 2 },
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
}The middle point(s) act as control points — the arrow curves smoothly through them.
{
"elbowed": true,
"fixedSegments": [
{ "index": 2, "start": [0, 35], "end": [80, 35] }
]
}Group elements so they're logically associated. All grouped elements share the same group ID in their groupIds array.
// Element A
{ "id": "box_a", "groupIds": ["group_pipeline"], ... }
// Element B
{ "id": "box_b", "groupIds": ["group_pipeline"], ... }
// Arrow between them
{ "id": "arrow_ab", "groupIds": ["group_pipeline"], ... }- Generate group IDs as descriptive strings (e.g.,
"group_pipeline","group_deploy") - An element can belong to multiple groups (nested grouping)
- Groups don't have their own element entry — they exist only via
groupIdsreferences - In excalidraw UI, grouped elements select together on click
Named containers that visually group a section of a diagram. Frames render as a labeled rectangle with a title.
{
"type": "frame",
"id": "frame_initiative1",
"x": 50, "y": 50,
"width": 600, "height": 400,
"name": "Initiative 1: Feature Branches",
"roundness": null,
"boundElements": [],
"version": 1, "versionNonce": 123456789,
"isDeleted": false, "fillStyle": "solid",
"strokeWidth": 2, "strokeStyle": "solid",
"roughness": 0, "opacity": 100, "angle": 0,
"strokeColor": "#bbb", "backgroundColor": "transparent",
"seed": 987654321, "groupIds": [], "frameId": null,
"updated": 1770865000000, "link": null, "locked": false,
"index": "a0"
}- Child elements point to the frame via
"frameId": "frame_initiative1" - Frame
namerenders as a label above the frame - Use
roughness: 0for frames (clean lines look better) - Size the frame to contain all child elements with ~20px padding
Make elements clickable by setting the link property:
{
"id": "my_box",
"link": "https://example.com/docs",
...
}- Works on any element type
- In excalidraw UI, linked elements show a link icon on hover
- Useful for linking diagram nodes to docs, Jira tickets, etc.
Control transparency with opacity (0-100):
100= fully opaque (default)50= semi-transparent (good for background/context elements)25= ghost/watermark effect
Useful for "before/after" diagrams — show the old flow at low opacity with the new flow at full opacity on top.
Prevent accidental edits on finalized elements:
{ "id": "my_box", "locked": true, ... }- Build the elements array with all shapes, text labels, and arrows
- Ensure all bindings are bidirectional (shape references arrow AND arrow references shape)
- Assign sequential index values starting from
"a0" - Wrap in the file structure and write with the Write tool
- Read the existing file
- Find the highest index value:
jq '[.elements[] | .index] | sort | last' - Create new elements with indices after the highest
- Add new elements to the elements array
- Update any existing elements'
boundElementsif new arrows connect to them - Write back with jq via Bash for surgical edits:
jq '<mutations>' file.excalidraw > _tmp.excalidraw && mv _tmp.excalidraw file.excalidraw
Use the Edit tool to find and replace specific JSON properties in the file.
Set "isDeleted": true on the element. Don't remove it from the array — excalidraw uses tombstones.
Find and mark as deleted:
- Elements with
nullx/y coordinates - Text elements with
containerIdpointing to a deleted element - Arrows with bindings pointing to deleted elements
- Use:
jq '[.elements[] | select(.x == null) | select(.isDeleted == false or .isDeleted == null) | .id]'
List all active elements with positions:
jq '[.elements[] | select(.isDeleted == false or .isDeleted == null) | {id, type, x, y, width, height, text: (if .type == "text" then .text else null end)} | del(.[] | nulls)]' file.excalidraw- Standard box size: 180-220w x 70-80h
- Diamond size: 130-150w x 90-110h (larger than boxes due to rotated shape)
- Arrow gap between boxes: 40-60px
- Row spacing: 120-150px
- Keep main flow horizontal, branches vertical
- Total width ~1200px max for readability
- Frame padding: ~20px around contained elements
Box A (x=50) --arrow(50px gap)--> Box B (x=300) --arrow--> Box C (x=550)
Formula: next_x = prev_x + prev_width + arrow_gap(50)
--> Box B (y - 70)
Box A --+
--> Box C (y + 70)
Use two arrows from Box A with different fixedPoints: [1, 0.3] and [1, 0.7]
Box A --> ◇ Decision --> Box B (yes)
|
v
Box C (no)
Diamond with arrows from right (yes) and bottom (no). Add standalone text labels "yes"/"no" near each arrow.
Show old flow at opacity: 30 with new flow at opacity: 100 on top. Use strokeStyle: "dashed" on removed elements.
- Every shape with a label has a matching text element with
containerId - Every shape with a label has
boundElementsincluding the text element - Every arrow binding target has
boundElementsincluding the arrow - All
originalTextmatchestext - All indices are sequential and unique
- No
nullcoordinates on any active element - Groups: all elements in a group share the same
groupIdsentry - Frames: all child elements have
frameIdpointing to the frame
When asked to create or edit an excalidraw diagram:
- If editing, READ the existing file first
- Plan the layout with coordinates before writing
- Build complete, valid JSON — don't skip properties
- Double-check all bindings are bidirectional
- Write the file directly using Write or Edit tools