excalidraw
Excalidraw Diagram Generator
Generate valid .excalidraw JSON files that open directly in Excalidraw with clean, professional layout.
Workflow
- Parse the request — identify diagram type (flowchart, architecture, ER, sequence, mind map, or free-form) and extract all nodes and relationships.
- Read the theme — read
references/theme-default.mdfor colors, fonts, spacing, and layout values. Never hardcode style values. - Read layout strategy — read
references/layout-strategies.mdand apply the matching diagram type strategy. - Build logical graph — list all nodes (id, label, shape type, semantic color) and edges (source, target, label, arrow style).
- Assign positions — apply the layout algorithm to compute
(x, y, width, height)for every node. - Route arrows — compute arrow attachment points, paths, and elbowed bends.
- Generate legend — if the diagram uses multiple semantic colors, shape types, or arrow styles, add a legend row at the bottom of the main frame.
- Wrap in frame — enclose the entire diagram (all nodes, arrows, and legend) inside a single frame element whose
nameis the diagram title. Do NOT add a separate title text element. Compute frame bounds from ALL visual elements — see "Frame Sizing" below. - Assemble and write — build the full JSON and write to
<name>.excalidraw.
Element JSON Quick Reference
For full templates with all fields, read references/element-spec.md. Here's the minimum you need to remember:
Shapes (rectangle, ellipse, diamond)
Key fields: id, type, x, y, width, height, strokeColor, backgroundColor, fillStyle, strokeWidth, strokeStyle, roughness (use 0), opacity (use 100), roundness, boundElements, index.
- Rectangles:
roundness: { "type": 3 } - Ellipses:
roundness: null - Diamonds:
roundness: { "type": 2 }, size needs ~1.6x text dimensions because of the 45deg rotation
Arrows
Key fields: id, type: "arrow", x, y, points array, startBinding, endBinding, startArrowhead, endArrowhead.
x,y= position of first point (source attachment coordinates)points:[[0,0], ..., [dx, dy]]— offsets relative to(x, y)- Bindings:
{ "elementId": "<id>", "focus": 0, "gap": 1, "fixedPoint": null }— always setfixedPointtonullso Excalidraw auto-calculates attachment points - Never set
"elbowed": true— elbowed arrows require internal editor state that raw JSON cannot provide. Always use"elbowed": falseand route multi-point arrows manually via thepointsarray instead.
Text
Key fields: id, type: "text", x, y, width, height, text, originalText, fontSize, fontFamily, textAlign, verticalAlign, containerId, lineHeight (use 1.25), autoResize (use true).
- Container-bound text: set
containerIdto the shape's ID - The shape must list the text in its
boundElements:{ "id": "<text_id>", "type": "text" }
Frames
Key fields: id, type: "frame", x, y, width, height, name.
- Every diagram gets a single main frame (
id: "main_frame") wrapping all elements - The frame's
nameproperty serves as the diagram title — no separate title text element needed - Children must appear before the frame in the
elementsarray - Children set
frameIdto the frame's ID - Size the frame to fit all content with
padX/padYmargin on each side
ID Generation
Use deterministic, readable IDs:
| Element | ID Pattern | Example |
|---|---|---|
| Main frame | main_frame |
main_frame |
| Shape node | node_<n> |
node_1 |
| Node text | text_node_<n> |
text_node_1 |
| Arrow | arrow_<src>_<tgt> |
arrow_1_2 |
| Arrow label | text_arrow_<src>_<tgt> |
text_arrow_1_2 |
| Legend separator | legend_line |
legend_line |
| Legend item shape | legend_shape_<n> |
legend_shape_1 |
| Legend item text | legend_text_<n> |
legend_text_1 |
| Lifeline (seq) | lifeline_<n> |
lifeline_1 |
When multiple arrows connect the same source/target pair, append a suffix: arrow_1_2_a, arrow_1_2_b.
Index Values (z-ordering)
Excalidraw uses fractional indexing for element ordering. The index field must stay within the "a" integer prefix — never use "b" or higher. Valid sequences:
- Up to 36 elements:
"a0","a1", ...,"a9","aA","aB", ...,"aZ"(36 values) - 37-72 elements: interleave with
"a0V","a1V", etc. to double capacity - 73+ elements: use three-char fractions:
"a0G","a0V","a1","a1G","a1V","a2", ...
The key rule: never go past "aZ" by incrementing to "b0", "b1", etc. — those are invalid fractional index keys and will cause Excalidraw to reject the file.
Layout Algorithm
Block Sizing
Calculate block dimensions from text content using the theme's character width multipliers:
charWidth = fontSize * charWidthMultiplier[fontFamily]
textWidth = longestLineCharCount * charWidth
textHeight = lineCount * fontSize * lineHeight
blockWidth = max(cellWidth, textWidth + 2 * padX)
blockHeight = max(cellHeight, textHeight + 2 * padY)
Round both to the nearest multiple of 10 for visual cleanliness.
For diamonds, multiply both dimensions by 1.6 to account for the rotated rendering.
Position Assignment
- Build a directed graph from the logical model.
- Assign each node a
(row, col)using the diagram-type strategy (seereferences/layout-strategies.md). - Compute pixel positions:
Wherex = col * (colWidth + gapX) + offsetX y = row * (rowHeight + gapY) + offsetYcolWidthis the max block width in that column,rowHeightis the max block height in that row. - Center narrower rows:
offsetX = (totalWidth - rowWidth) / 2. - Offset the entire diagram to start at
(100, 100)for canvas margin.
Text Centering
Position container-bound text at the center of its parent shape:
text.x = shape.x + (shape.width - text.width) / 2
text.y = shape.y + (shape.height - text.height) / 2
Arrow Routing
Binding Format
All arrow bindings must use this exact format — fixedPoint must always be null:
"startBinding": {
"elementId": "<source_shape_id>",
"focus": 0,
"gap": 1,
"fixedPoint": null
},
"endBinding": {
"elementId": "<target_shape_id>",
"focus": 0,
"gap": 1,
"fixedPoint": null
}
Excalidraw auto-calculates where the arrow attaches based on the arrow's start/end coordinates and the target shape's geometry. Setting fixedPoint to any non-null value will cause the file to fail to open.
Computing Arrow Coordinates
To control which side an arrow connects to, position the arrow's start/end points near the desired edge of the shape:
- Decide the exit side of the source and entry side of the target based on layout:
- Target below source → exit bottom, enter top
- Target right of source → exit right, enter left
- Set arrow
xandyto the source shape's exit edge center:- Bottom exit:
x = shape.x + shape.width/2,y = shape.y + shape.height - Right exit:
x = shape.x + shape.width,y = shape.y + shape.height/2 - Top exit:
x = shape.x + shape.width/2,y = shape.y - Left exit:
x = shape.x,y = shape.y + shape.height/2
- Bottom exit:
- Calculate the target entry point the same way.
- Set
points:[[0, 0], [targetEntry.x - arrow.x, targetEntry.y - arrow.y]] - Set
widthandheightto the absolute deltas.
Multi-Point Arrows (Bends)
For arrows that need to route around obstacles, add intermediate points. Always use "elbowed": false — Excalidraw handles the visual rendering of multi-point paths automatically.
L-bend (going down then right):
"points": [[0, 0], [0, midY], [dx, midY]]
Z-bend (going right, down, then right again):
"points": [[0, 0], [offsetX, 0], [offsetX, dy], [dx, dy]]
Overlap Prevention
- When multiple arrows leave from the same side of a node, offset their starting
xoryby 20-30px apart so they don't stack on top of each other. - For arrow labels on parallel arrows, stagger them vertically.
Frame Sizing
The main frame must be large enough to contain every visual element — including arrows that route outside the main node area. This is critical for back-edges, loop arrows, and any multi-point arrow that routes around nodes.
How to compute frame bounds
After all nodes, arrows, and legend items are positioned:
- Collect all bounding points — for each element:
- Shapes:
(x, y)to(x + width, y + height) - Arrows: compute the absolute position of every point in the
pointsarray (arrow.x + point[0],arrow.y + point[1]) and include all of them - Text (standalone):
(x, y)to(x + width, y + height)
- Shapes:
- Find the global bounding box —
minX,minY,maxX,maxYacross all collected points - Add generous padding — apply
padX * 2(48px) on left/right andpadY * 2(32px) on top/bottom:frame.x = minX - padX * 2 frame.y = minY - padY * 2 frame.width = (maxX - minX) + padX * 4 frame.height = (maxY - minY) + padY * 4
This ensures arrows that loop left (like a "fail" back-edge) or route around obstacles have breathing room inside the frame. Without this, routed arrows will clip against or extend beyond the frame border.
Legend Generation
When to Generate
Add a legend when the diagram uses 2 or more of:
- Different background colors with semantic meaning
- Different arrow styles (solid, dashed, dotted) representing different relationships
- Different shape types (rectangle, ellipse, diamond) representing different categories
Skip the legend if colors/shapes are purely decorative or the diagram has fewer than 5 elements.
How to Build
The legend is a horizontal row at the bottom of the main diagram frame, spanning the full frame width. It is NOT a separate frame — legend items are direct children of the main frame.
- Position: place legend items in a horizontal row below the last diagram element, with
gapYspacing above. - Layout: arrange entries left-to-right in a single row. Each entry is a pair:
- A small sample shape (width=30, height=20) with the matching style
- A text label to its right (offset 12px),
fontSize_legend, describing what it represents
- Horizontal spacing: distribute entries evenly across the full frame width with
gapXbetween entries. - Separator line: add a thin horizontal line (
strokeWidth: 1,strokeStyle: "solid", legend_border color) across the full frame width just above the legend row, withgapY/2gap above and below. - Element ordering: all legend elements (line, shapes, text) must appear before the main frame in the
elementsarray, and each must setframeIdto the main frame's ID. - Frame sizing: the main frame height must include the legend row. Add
gapY + separator_gap + legend_row_height + padYto the frame height.
Legend Entry Examples
[sage rectangle] "Process" [brown diamond] "Decision" [solid arrow →] "Flow" [dashed arrow →] "Dependency"
Semantic Color Assignment
When the user doesn't specify colors, assign them based on the node's role. The palette uses earthy/sage tones — refer to references/theme-default.md for exact hex values.
| Role / Category | Theme Color |
|---|---|
| Process, service, action | primary (sage green) |
| Data store, database | success (light green) |
| Gateway, router, proxy | secondary (pale green) |
| Decision, condition | accent (warm brown) |
| External system, user | neutral (cream) |
| Error, failure | error (brown) |
| Queue, cache, async | warning (peach) |
Use the stroke/fill/text triplet from the theme for consistency.
Output Format
Write the final file as <diagram-name>.excalidraw with this structure:
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [ ... ],
"appState": {
"gridSize": null,
"viewBackgroundColor": "<theme.background>"
},
"files": {}
}
Element Ordering in the Array
Everything goes inside the main frame. All children must appear before the frame element in the array:
- All shapes (bottom to top, left to right)
- All text elements bound to those shapes (immediately after their container)
- All arrows
- Arrow label text elements
- Legend separator line (if legend present)
- Legend shapes and text (if legend present)
- The main frame element itself — last in the array
Every element except the frame must set "frameId": "main_frame". This ensures correct rendering and clipping.
Validation Checklist
Before writing the file, verify:
- The entire diagram is wrapped in a single
main_frameframe element - The frame's
nameproperty is the diagram title - Every element except the frame has
"frameId": "main_frame" - The frame element is the last element in the array
- Every
containerIdreferences an existing element ID - Every
boundElementsentry references an existing element ID - Every
startBinding.elementIdandendBinding.elementIdreferences an existing shape ID - The bound shape's
boundElementsarray includes the arrow ID with"type": "arrow" - No two elements share the same
id - No shape bounding boxes overlap (except text inside its container)
- All colors come from the theme palette — no hardcoded hex values
- Arrow
x,ymatches the source attachment point coordinates - Arrow
pointslast entry matches the delta to the target attachment point -
textandoriginalTextfields are identical on every text element - All elements have
roughness: 0,opacity: 100,isDeleted: false -
indexvalues are unique, sequential, and never exceed the"a"prefix (see ID Generation) - If legend present: legend items are inside the main frame (not a separate frame), at the bottom, full width
- Frame bounds computed from ALL elements including arrow path points — no element extends beyond the frame border
Tips
- Start by sketching the logical graph on paper (mentally): nodes, edges, groups. Then map to grid positions.
- For complex diagrams, compute all positions first, then generate all JSON elements in a second pass. This avoids having to back-patch binding references.
- When the user asks for changes to an existing diagram, read the
.excalidrawfile first, understand the current layout, and modify in place rather than regenerating from scratch. - If the diagram has more than ~20 nodes, consider splitting into sub-diagrams or using frames to group related sections.
- The theme file is designed to be swappable. If the user asks for a different look (dark mode, pastel, corporate), create a new theme file following the same structure and read that instead.
More from dzmitry-vasileuski/skills
laravel-docs
Search official Laravel ecosystem documentation using semantic vector search — delegated to a sub-agent that returns a compact synthesis so the main context stays lean. Use this skill whenever writing or modifying Laravel application code — search the docs first to discover the current API before writing any implementation. The agent's training data has a knowledge cutoff; newer Laravel versions regularly add cleaner methods that replace familiar patterns. Always search before writing code for any Laravel feature — caching, string helpers, collections, concurrency, request handling, Eloquent, routing, queues, events, mail, and more. Also use this skill when the user references a Laravel version you don't have training data for, when you're unsure whether an API still exists or has changed, or when you want to verify your knowledge is current — the docs API always has the latest information.
9grepai
Semantic code search with grepai CLI — finds code by meaning, not text. Use this skill whenever the user asks to understand how something works, explore unfamiliar code areas, find code that handles a concept, or needs to discover related files across a codebase. Trigger phrases include "how does X work", "find the code for Y", "where is Z handled", "walk me through the flow", "show me how X is implemented". NOT for exact symbol lookups or tracing specific function calls (use grep for those). Assumes grepai is installed and indexed.
1