
Excalidraw
- 12 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
Generates architecture diagrams as .excalidraw files from codebase analysis, with optional PNG and SVG export.
About
Generates architecture diagrams as .excalidraw files directly from codebase analysis, with optional PNG and SVG export. A developer uses it to visualize system structure, components, and data flows.
- Generates .excalidraw JSON from codebase analysis
- Optional PNG/SVG export via Playwright
Excalidraw by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,115 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill excalidrawAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Generates architecture diagrams as .excalidraw files from codebase analysis, with optional PNG and SVG export.
Files
Excalidraw Diagram Generator
Generate architecture diagrams as .excalidraw files directly from codebase analysis, with optional export to PNG and SVG.
---
Quick Start
User just asks:
"Generate an architecture diagram for this project"
"Create an excalidraw diagram of the system"
"Visualize this codebase as an excalidraw file"Claude Code will: 1. Analyze the codebase (any language/framework) 2. Identify components, services, databases, APIs 3. Map relationships and data flows 4. Generate valid .excalidraw JSON with dynamic IDs and labels 5. Optionally export to PNG and/or SVG using Playwright
No prerequisites: Works without existing diagrams, Terraform, or specific file types.
---
Critical Rules
1. NEVER Use Diamond Shapes
Diamond arrow connections are broken in raw Excalidraw JSON. Use styled rectangles instead:
| Semantic Meaning | Rectangle Style |
|---|---|
| Orchestrator/Hub | Coral (#ffa8a8/#c92a2a) + strokeWidth: 3 |
| Decision Point | Orange (#ffd8a8/#e8590c) + dashed stroke |
2. Labels Require TWO Elements
The label property does NOT work in raw JSON. Every labeled shape needs:
// 1. Shape with boundElements reference
{
"id": "my-box",
"type": "rectangle",
"boundElements": [{ "type": "text", "id": "my-box-text" }]
}
// 2. Separate text element with containerId
{
"id": "my-box-text",
"type": "text",
"containerId": "my-box",
"text": "My Label"
}3. Elbow Arrows Need Three Properties
For 90-degree corners (not curved):
{
"type": "arrow",
"roughness": 0, // Clean lines
"roundness": null, // Sharp corners
"elbowed": true // 90-degree mode
}4. Arrow Edge Calculations
Arrows must start/end at shape edges, not centers:
| Edge | Formula |
|---|---|
| Top | (x + width/2, y) |
| Bottom | (x + width/2, y + height) |
| Left | (x, y + height/2) |
| Right | (x + width, y + height/2) |
Detailed arrow routing: See references/arrows.md
---
Element Types
| Type | Use For |
|---|---|
rectangle | Services, databases, containers, orchestrators |
ellipse | Users, external systems, start/end points |
text | Labels inside shapes, titles, annotations |
arrow | Data flow, connections, dependencies |
line | Grouping boundaries, separators |
Full JSON format: See references/json-format.md
---
Workflow
Step 1: Analyze Codebase
Discover components by looking for:
| Codebase Type | What to Look For |
|---|---|
| Monorepo | packages/*/package.json, workspace configs |
| Microservices | docker-compose.yml, k8s manifests |
| IaC | Terraform/Pulumi resource definitions |
| Backend API | Route definitions, controllers, DB models |
| Frontend | Component hierarchy, API calls |
Use tools:
Glob→**/package.json,**/Dockerfile,**/*.tfGrep→app.get,@Controller,CREATE TABLERead→ README, config files, entry points
Step 2: Plan Layout
Vertical flow (most common):
Row 1: Users/Entry points (y: 100)
Row 2: Frontend/Gateway (y: 230)
Row 3: Orchestration (y: 380)
Row 4: Services (y: 530)
Row 5: Data layer (y: 680)
Columns: x = 100, 300, 500, 700, 900
Element size: 160-200px x 80-90pxOther patterns: See references/examples.md
Step 3: Generate Elements
For each component: 1. Create shape with unique id 2. Add boundElements referencing text 3. Create text with containerId 4. Choose color based on type
Color palettes: See references/colors.md
Step 4: Add Connections
For each relationship: 1. Calculate source edge point 2. Plan elbow route (avoid overlaps) 3. Create arrow with points array 4. Match stroke color to destination type
Arrow patterns: See references/arrows.md
Step 5: Add Grouping (Optional)
For logical groupings:
- Large transparent rectangle with
strokeStyle: "dashed" - Standalone text label at top-left
Step 6: Validate and Write
Run validation before writing. Save to docs/ or user-specified path.
Validation checklist: See references/validation.md
Step 7: Export to PNG/SVG (Optional)
After writing the .excalidraw file, ask the user if they want PNG, SVG, or both exports.
Uses Playwright MCP tools and @excalidraw/utils to programmatically render the diagram — no manual upload to excalidraw.com needed.
Requires: Playwright MCP tools (browser_navigate, browser_run_code, browser_close).
Full export procedure: See references/export.md
---
Quick Arrow Reference
Straight down:
{ "points": [[0, 0], [0, 110]], "x": 590, "y": 290 }L-shape (left then down):
{ "points": [[0, 0], [-325, 0], [-325, 125]], "x": 525, "y": 420 }U-turn (callback):
{ "points": [[0, 0], [50, 0], [50, -125], [20, -125]], "x": 710, "y": 440 }Arrow width/height = bounding box of points:
points [[0,0], [-440,0], [-440,70]] → width=440, height=70Multiple arrows from same edge - stagger positions:
5 arrows: 20%, 35%, 50%, 65%, 80% across edge width---
Default Color Palette
| Component | Background | Stroke |
|---|---|---|
| Frontend | #a5d8ff | #1971c2 |
| Backend/API | #d0bfff | #7048e8 |
| Database | #b2f2bb | #2f9e44 |
| Storage | #ffec99 | #f08c00 |
| AI/ML | #e599f7 | #9c36b5 |
| External APIs | #ffc9c9 | #e03131 |
| Orchestration | #ffa8a8 | #c92a2a |
| Message Queue | #fff3bf | #fab005 |
| Cache | #ffe8cc | #fd7e14 |
| Users | #e7f5ff | #1971c2 |
Cloud-specific palettes: See references/colors.md
---
Quick Validation Checklist
Before writing file:
- [ ] Every shape with label has boundElements + text element
- [ ] Text elements have containerId matching shape
- [ ] Multi-point arrows have
elbowed: true,roundness: null - [ ] Arrow x,y = source shape edge point
- [ ] Arrow final point offset reaches target edge
- [ ] No diamond shapes
- [ ] No duplicate IDs
Full validation algorithm: See references/validation.md
---
Common Issues
| Issue | Fix |
|---|---|
| Labels don't appear | Use TWO elements (shape + text), not label property |
| Arrows curved | Add elbowed: true, roundness: null, roughness: 0 |
| Arrows floating | Calculate x,y from shape edge, not center |
| Arrows overlapping | Stagger start positions across edge |
Detailed bug fixes: See references/validation.md
---
Reference Files
| File | Contents |
|---|---|
references/json-format.md | Element types, required properties, text bindings |
references/arrows.md | Routing algorithm, patterns, bindings, staggering |
references/colors.md | Default, AWS, Azure, GCP, K8s palettes |
references/examples.md | Complete JSON examples, layout patterns |
references/validation.md | Checklists, validation algorithm, bug fixes |
references/export.md | PNG/SVG export procedure via Playwright |
---
Output
- Location:
docs/architecture/or user-specified - Filename: Descriptive, e.g.,
system-architecture.excalidraw - Exports (optional):
system-architecture.svgand/orsystem-architecture.pngin same directory - Testing: Open
.excalidrawin https://excalidraw.com or VS Code extension;.svgand.pngcan be viewed directly or embedded in documentation
---
Credits & Attribution
This skill is based on the excellent work by [ooiyeefei](https://github.com/ooiyeefei/ccc/tree/main/skills/excalidraw).
Original repository: https://github.com/ooiyeefei/ccc/tree/main/skills/excalidraw
Special thanks to ooiyeefei for their generous open-source contributions, which helped shape this skill collection. Adapted by webconsulting.at for this skill collection
Excalidraw - Architecture Diagram Generator
Generate architecture diagrams as .excalidraw files from codebase analysis, with optional PNG and SVG export.
---
Installation
# Add the ccc marketplace (if not already added)
/plugin marketplace add ooiyeefei/ccc
# Install the skills collection
/plugin install ccc-skills@cccQuick Start
After installing, just ask Claude Code:
"Generate an architecture diagram for this project"
"Create an excalidraw diagram of the system"
"Visualize this codebase as an excalidraw file"Claude Code will analyze any codebase (Node.js, Python, Java, Go, etc.), identify components, map relationships, and generate a valid .excalidraw JSON file.
Features
- Any codebase: Discovers services, databases, APIs, and infrastructure from source code
- No prerequisites: Works without existing diagrams, Terraform, or specific file types
- Proper arrows: 90-degree elbow arrows with correct edge binding (not curved)
- Color-coded: Components styled by type (database, API, storage, AI/ML, etc.)
- Cloud palettes: AWS, Azure, GCP, and Kubernetes color schemes
- Multiple layouts: Vertical flow, horizontal pipeline, hub-and-spoke patterns
- PNG/SVG export: Optionally export to PNG and/or SVG via Playwright
PNG/SVG Export
After generating a diagram, Claude Code will ask if you want to export to PNG, SVG, or both.
The export uses @excalidraw/utils loaded in a Playwright browser — fully programmatic, no manual upload to excalidraw.com needed.
Requirements: Playwright MCP tools must be available.
Output: Exported files are saved alongside the .excalidraw file:
docs/architecture/
├── system-architecture.excalidraw # Editable diagram
├── system-architecture.svg # Vector export
└── system-architecture.png # Raster exportOutput
- Location:
docs/architecture/or user-specified path - Format:
.excalidrawJSON (editable in excalidraw.com or VS Code extension) - Exports:
.svgand.pngviewable directly or embeddable in documentation
Reference Files
The skill includes detailed reference documentation:
| File | Contents |
|---|---|
references/json-format.md | Element types, required properties, text bindings |
references/arrows.md | Routing algorithm, patterns, bindings, staggering |
references/colors.md | Default, AWS, Azure, GCP, K8s palettes |
references/examples.md | Complete JSON examples, layout patterns |
references/validation.md | Checklists, validation algorithm, bug fixes |
references/export.md | PNG/SVG export procedure via Playwright |
License
MIT
Arrow Routing Reference
Complete guide for creating elbow arrows with proper connections.
---
Critical: Elbow Arrow Properties
Three required properties for 90-degree corners:
{
"type": "arrow",
"roughness": 0, // Clean lines
"roundness": null, // Sharp corners (not curved)
"elbowed": true // Enables elbow mode
}Without these, arrows will be curved, not 90-degree elbows.
---
Edge Calculation Formulas
| Shape Type | Edge | Formula |
|---|---|---|
| Rectangle | Top | (x + width/2, y) |
| Rectangle | Bottom | (x + width/2, y + height) |
| Rectangle | Left | (x, y + height/2) |
| Rectangle | Right | (x + width, y + height/2) |
| Ellipse | Top | (x + width/2, y) |
| Ellipse | Bottom | (x + width/2, y + height) |
---
Universal Arrow Routing Algorithm
FUNCTION createArrow(source, target, sourceEdge, targetEdge):
// Step 1: Get source edge point
sourcePoint = getEdgePoint(source, sourceEdge)
// Step 2: Get target edge point
targetPoint = getEdgePoint(target, targetEdge)
// Step 3: Calculate offsets
dx = targetPoint.x - sourcePoint.x
dy = targetPoint.y - sourcePoint.y
// Step 4: Determine routing pattern
IF sourceEdge == "bottom" AND targetEdge == "top":
IF abs(dx) < 10: // Nearly aligned
points = [[0, 0], [0, dy]]
ELSE: // Need L-shape
points = [[0, 0], [dx, 0], [dx, dy]]
ELSE IF sourceEdge == "right" AND targetEdge == "left":
IF abs(dy) < 10:
points = [[0, 0], [dx, 0]]
ELSE:
points = [[0, 0], [0, dy], [dx, dy]]
ELSE IF sourceEdge == targetEdge: // U-turn
clearance = 50
IF sourceEdge == "right":
points = [[0, 0], [clearance, 0], [clearance, dy], [dx, dy]]
ELSE IF sourceEdge == "bottom":
points = [[0, 0], [0, clearance], [dx, clearance], [dx, dy]]
// Step 5: Calculate bounding box
width = max(abs(p[0]) for p in points)
height = max(abs(p[1]) for p in points)
RETURN {x: sourcePoint.x, y: sourcePoint.y, points, width, height}
FUNCTION getEdgePoint(shape, edge):
SWITCH edge:
"top": RETURN (shape.x + shape.width/2, shape.y)
"bottom": RETURN (shape.x + shape.width/2, shape.y + shape.height)
"left": RETURN (shape.x, shape.y + shape.height/2)
"right": RETURN (shape.x + shape.width, shape.y + shape.height/2)---
Arrow Patterns Reference
| Pattern | Points | Use Case |
|---|---|---|
| Down | [[0,0], [0,h]] | Vertical connection |
| Right | [[0,0], [w,0]] | Horizontal connection |
| L-left-down | [[0,0], [-w,0], [-w,h]] | Go left, then down |
| L-right-down | [[0,0], [w,0], [w,h]] | Go right, then down |
| L-down-left | [[0,0], [0,h], [-w,h]] | Go down, then left |
| L-down-right | [[0,0], [0,h], [w,h]] | Go down, then right |
| S-shape | [[0,0], [0,h1], [w,h1], [w,h2]] | Navigate around obstacles |
| U-turn | [[0,0], [w,0], [w,-h], [0,-h]] | Callback/return arrows |
---
Worked Examples
Vertical Connection (Bottom to Top)
Source: x=500, y=200, width=180, height=90
Target: x=500, y=400, width=180, height=90
source_bottom = (500 + 180/2, 200 + 90) = (590, 290)
target_top = (500 + 180/2, 400) = (590, 400)
Arrow x = 590, y = 290
Distance = 400 - 290 = 110
Points = [[0, 0], [0, 110]]Fan-out (One to Many)
Orchestrator: x=570, y=400, width=140, height=80
Target: x=120, y=550, width=160, height=80
orchestrator_bottom = (570 + 140/2, 400 + 80) = (640, 480)
target_top = (120 + 160/2, 550) = (200, 550)
Arrow x = 640, y = 480
Horizontal offset = 200 - 640 = -440
Vertical offset = 550 - 480 = 70
Points = [[0, 0], [-440, 0], [-440, 70]] // Left first, then downU-turn (Callback)
Source: x=570, y=400, width=140, height=80
Target: x=550, y=270, width=180, height=90
Connection: Right of source -> Right of target
source_right = (570 + 140, 400 + 80/2) = (710, 440)
target_right = (550 + 180, 270 + 90/2) = (730, 315)
Arrow x = 710, y = 440
Vertical distance = 315 - 440 = -125
Final x offset = 730 - 710 = 20
Points = [[0, 0], [50, 0], [50, -125], [20, -125]]
// Right 50px (clearance), up 125px, left 30px---
Staggering Multiple Arrows
When N arrows leave from same edge, spread evenly:
FUNCTION getStaggeredPositions(shape, edge, numArrows):
positions = []
FOR i FROM 0 TO numArrows-1:
percentage = 0.2 + (0.6 * i / (numArrows - 1))
IF edge == "bottom" OR edge == "top":
x = shape.x + shape.width * percentage
y = (edge == "bottom") ? shape.y + shape.height : shape.y
ELSE:
x = (edge == "right") ? shape.x + shape.width : shape.x
y = shape.y + shape.height * percentage
positions.append({x, y})
RETURN positions
// Examples:
// 2 arrows: 20%, 80%
// 3 arrows: 20%, 50%, 80%
// 5 arrows: 20%, 35%, 50%, 65%, 80%---
Arrow Bindings
For better visual attachment, use startBinding and endBinding:
{
"id": "arrow-workflow-convert",
"type": "arrow",
"x": 525,
"y": 420,
"width": 325,
"height": 125,
"points": [[0, 0], [-325, 0], [-325, 125]],
"roughness": 0,
"roundness": null,
"elbowed": true,
"startBinding": {
"elementId": "cloud-workflows",
"focus": 0,
"gap": 1,
"fixedPoint": [0.5, 1]
},
"endBinding": {
"elementId": "convert-pdf-service",
"focus": 0,
"gap": 1,
"fixedPoint": [0.5, 0]
},
"startArrowhead": null,
"endArrowhead": "arrow"
}fixedPoint Values
- Top center:
[0.5, 0] - Bottom center:
[0.5, 1] - Left center:
[0, 0.5] - Right center:
[1, 0.5]
Update Shape boundElements
{
"id": "cloud-workflows",
"boundElements": [
{ "type": "text", "id": "cloud-workflows-text" },
{ "type": "arrow", "id": "arrow-workflow-convert" }
]
}---
Bidirectional Arrows
For two-way data flows:
{
"type": "arrow",
"startArrowhead": "arrow",
"endArrowhead": "arrow"
}Arrowhead options: null, "arrow", "bar", "dot", "triangle"
---
Arrow Labels
Position standalone text near arrow midpoint:
{
"id": "arrow-api-db-label",
"type": "text",
"x": 305, // Arrow x + offset
"y": 245, // Arrow midpoint
"text": "SQL",
"fontSize": 12,
"containerId": null,
"backgroundColor": "#ffffff"
}Positioning formula:
- Vertical:
label.y = arrow.y + (total_height / 2) - Horizontal:
label.x = arrow.x + (total_width / 2) - L-shaped: Position at corner or longest segment midpoint
---
Width/Height Calculation
Arrow width and height = bounding box of path:
points = [[0, 0], [-440, 0], [-440, 70]]
width = abs(-440) = 440
height = abs(70) = 70
points = [[0, 0], [50, 0], [50, -125], [20, -125]]
width = max(abs(50), abs(20)) = 50
height = abs(-125) = 125Color Palettes Reference
Color schemes for different platforms and component types.
---
Default Palette (Platform-Agnostic)
| Component Type | Background | Stroke | Example |
|---|---|---|---|
| Frontend/UI | #a5d8ff | #1971c2 | Next.js, React apps |
| Backend/API | #d0bfff | #7048e8 | API servers, processors |
| Database | #b2f2bb | #2f9e44 | PostgreSQL, MySQL, MongoDB |
| Storage | #ffec99 | #f08c00 | Object storage, file systems |
| AI/ML Services | #e599f7 | #9c36b5 | ML models, AI APIs |
| External APIs | #ffc9c9 | #e03131 | Third-party services |
| Orchestration | #ffa8a8 | #c92a2a | Workflows, schedulers |
| Validation | #ffd8a8 | #e8590c | Validators, checkers |
| Network/Security | #dee2e6 | #495057 | VPC, IAM, firewalls |
| Classification | #99e9f2 | #0c8599 | Routers, classifiers |
| Users/Actors | #e7f5ff | #1971c2 | User ellipses |
| Message Queue | #fff3bf | #fab005 | Kafka, RabbitMQ, SQS |
| Cache | #ffe8cc | #fd7e14 | Redis, Memcached |
| Monitoring | #d3f9d8 | #40c057 | Prometheus, Grafana |
---
AWS Palette
| Service Category | Background | Stroke |
|---|---|---|
| Compute (EC2, Lambda, ECS) | #ff9900 | #cc7a00 |
| Storage (S3, EBS) | #3f8624 | #2d6119 |
| Database (RDS, DynamoDB) | #3b48cc | #2d3899 |
| Networking (VPC, Route53) | #8c4fff | #6b3dcc |
| Security (IAM, KMS) | #dd344c | #b12a3d |
| Analytics (Kinesis, Athena) | #8c4fff | #6b3dcc |
| ML (SageMaker, Bedrock) | #01a88d | #017d69 |
---
Azure Palette
| Service Category | Background | Stroke |
|---|---|---|
| Compute | #0078d4 | #005a9e |
| Storage | #50e6ff | #3cb5cc |
| Database | #0078d4 | #005a9e |
| Networking | #773adc | #5a2ca8 |
| Security | #ff8c00 | #cc7000 |
| AI/ML | #50e6ff | #3cb5cc |
---
GCP Palette
| Service Category | Background | Stroke |
|---|---|---|
| Compute (GCE, Cloud Run) | #4285f4 | #3367d6 |
| Storage (GCS) | #34a853 | #2d8e47 |
| Database (Cloud SQL, Firestore) | #ea4335 | #c53929 |
| Networking | #fbbc04 | #d99e04 |
| AI/ML (Vertex AI) | #9334e6 | #7627b8 |
---
Kubernetes Palette
| Component | Background | Stroke |
|---|---|---|
| Pod | #326ce5 | #2756b8 |
| Service | #326ce5 | #2756b8 |
| Deployment | #326ce5 | #2756b8 |
| ConfigMap/Secret | #7f8c8d | #626d6e |
| Ingress | #00d4aa | #00a888 |
| Node | #303030 | #1a1a1a |
| Namespace | #f0f0f0 | #c0c0c0 (dashed) |
---
Diagram Type Suggestions
| Diagram Type | Recommended Layout | Key Elements |
|---|---|---|
| Microservices | Vertical flow | Services, databases, queues, API gateway |
| Data Pipeline | Horizontal flow | Sources, transformers, sinks, storage |
| Event-Driven | Hub-and-spoke | Event bus center, producers/consumers |
| Kubernetes | Layered groups | Namespace boxes, pods inside deployments |
| CI/CD | Horizontal flow | Source -> Build -> Test -> Deploy -> Monitor |
| Network | Hierarchical | Internet -> LB -> VPC -> Subnets -> Instances |
| User Flow | Swimlanes | User actions, system responses, external calls |
Complete Examples Reference
Full JSON examples showing proper element structure.
---
3-Tier Architecture Example
This is a REFERENCE showing JSON structure. Replace IDs, labels, positions, and colors based on discovered components.
{
"type": "excalidraw",
"version": 2,
"source": "claude-code-excalidraw-skill",
"elements": [
{
"id": "user",
"type": "ellipse",
"x": 150,
"y": 50,
"width": 100,
"height": 60,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#e7f5ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 2 },
"seed": 1,
"version": 1,
"versionNonce": 1,
"isDeleted": false,
"boundElements": [{ "type": "text", "id": "user-text" }],
"updated": 1,
"link": null,
"locked": false
},
{
"id": "user-text",
"type": "text",
"x": 175,
"y": 67,
"width": 50,
"height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 2,
"version": 1,
"versionNonce": 2,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false,
"text": "User",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"baseline": 14,
"containerId": "user",
"originalText": "User",
"lineHeight": 1.25
},
{
"id": "frontend",
"type": "rectangle",
"x": 100,
"y": 180,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 3 },
"seed": 3,
"version": 1,
"versionNonce": 3,
"isDeleted": false,
"boundElements": [{ "type": "text", "id": "frontend-text" }],
"updated": 1,
"link": null,
"locked": false
},
{
"id": "frontend-text",
"type": "text",
"x": 105,
"y": 195,
"width": 190,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 4,
"version": 1,
"versionNonce": 4,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false,
"text": "Frontend\nNext.js",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"baseline": 14,
"containerId": "frontend",
"originalText": "Frontend\nNext.js",
"lineHeight": 1.25
},
{
"id": "database",
"type": "rectangle",
"x": 100,
"y": 330,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 3 },
"seed": 5,
"version": 1,
"versionNonce": 5,
"isDeleted": false,
"boundElements": [{ "type": "text", "id": "database-text" }],
"updated": 1,
"link": null,
"locked": false
},
{
"id": "database-text",
"type": "text",
"x": 105,
"y": 345,
"width": 190,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 6,
"version": 1,
"versionNonce": 6,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false,
"text": "Database\nPostgreSQL",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"baseline": 14,
"containerId": "database",
"originalText": "Database\nPostgreSQL",
"lineHeight": 1.25
},
{
"id": "arrow-user-frontend",
"type": "arrow",
"x": 200,
"y": 115,
"width": 0,
"height": 60,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 7,
"version": 1,
"versionNonce": 7,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false,
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "arrow-frontend-database",
"type": "arrow",
"x": 200,
"y": 265,
"width": 0,
"height": 60,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"seed": 8,
"version": 1,
"versionNonce": 8,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false,
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
}
],
"appState": {
"gridSize": 20,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}---
Layout Patterns
Vertical Flow (Most Common)
Grid positioning:
- Column width: 200-250px
- Row height: 130-150px
- Element size: 160-200px x 80-90px
- Spacing: 40-50px between elements
Row positions (y):
Row 0: 20 (title)
Row 1: 100 (users/entry points)
Row 2: 230 (frontend/gateway)
Row 3: 380 (orchestration)
Row 4: 530 (services)
Row 5: 680 (data layer)
Row 6: 830 (external services)
Column positions (x):
Col 0: 100
Col 1: 300
Col 2: 500
Col 3: 700
Col 4: 900Horizontal Flow (Pipelines)
Stage positions (x):
Stage 0: 100 (input/source)
Stage 1: 350 (transform 1)
Stage 2: 600 (transform 2)
Stage 3: 850 (transform 3)
Stage 4: 1100 (output/sink)
All stages at same y: 200
Arrows: "right" -> "left" connectionsHub-and-Spoke
Center hub: x=500, y=350
8 positions at 45° increments:
N: (500, 150)
NE: (640, 210)
E: (700, 350)
SE: (640, 490)
S: (500, 550)
SW: (360, 490)
W: (300, 350)
NW: (360, 210)---
Complex Architecture Layout
Row 0: Title/Header (y: 20)
Row 1: Users/Clients (y: 80)
Row 2: Frontend/Gateway (y: 200)
Row 3: Orchestration (y: 350)
Row 4: Processing Services (y: 550)
Row 5: Data Layer (y: 680)
Row 6: External Services (y: 830)
Columns (x):
Col 0: 120
Col 1: 320
Col 2: 520
Col 3: 720
Col 4: 920---
Diagram Complexity Guidelines
| Complexity | Max Elements | Max Arrows | Approach |
|---|---|---|---|
| Simple | 5-10 | 5-10 | Single file, no groups |
| Medium | 10-25 | 15-30 | Use grouping rectangles |
| Complex | 25-50 | 30-60 | Split into multiple diagrams |
| Very Complex | 50+ | 60+ | Multiple focused diagrams |
When to split:
- More than 50 elements
- Create:
architecture-overview.excalidraw,architecture-data-layer.excalidraw
When to use groups:
- 3+ related services
- Same deployment unit
- Logical boundaries (VPC, Security Zone)
Export to PNG/SVG
Export .excalidraw files to PNG and/or SVG using Playwright MCP tools and @excalidraw/utils.
Prerequisites
- Playwright MCP tools available:
browser_navigate,browser_run_code,browser_close - Python 3 installed (for local HTTP server)
Procedure
1. Start a Local HTTP Server
A browser origin is required for dynamic ESM imports. Start a temporary server on any available port:
python3 -m http.server 8765 &
SERVER_PID=$!2. Navigate Playwright to the Server
browser_navigate → http://localhost:8765/The 404 page is fine — we only need the HTTP origin for the dynamic import to work.
3. Read the .excalidraw File
Use the Read tool to get the .excalidraw file contents as a string. This JSON string will be passed into the browser context in the next steps.
4. Export SVG
Use browser_run_code with the following pattern. Replace EXCALIDRAW_JSON_HERE with the actual JSON string from Step 3:
async (page) => {
const excalidrawJson = `EXCALIDRAW_JSON_HERE`;
const svgString = await page.evaluate(async (json) => {
const utils = await import('https://esm.sh/@excalidraw/utils@0.1.2');
const { exportToSvg } = utils.default;
const data = JSON.parse(json);
const svg = await exportToSvg({
elements: data.elements,
appState: { ...data.appState, exportBackground: true },
files: data.files || {}
});
return svg.outerHTML;
}, excalidrawJson);
return svgString;
}Write the returned SVG string directly to <filename>.svg using the Write tool.
5. Export PNG
Use browser_run_code with the following pattern:
async (page) => {
const excalidrawJson = `EXCALIDRAW_JSON_HERE`;
const pngBase64 = await page.evaluate(async (json) => {
const utils = await import('https://esm.sh/@excalidraw/utils@0.1.2');
const { exportToBlob } = utils.default;
const data = JSON.parse(json);
const blob = await exportToBlob({
elements: data.elements,
appState: { ...data.appState, exportBackground: true },
files: data.files || {},
mimeType: 'image/png'
});
const reader = new FileReader();
return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}, excalidrawJson);
return pngBase64;
}The result is a base64 data URL. Decode and write to <filename>.png:
echo "<base64_data_without_prefix>" | base64 -d > <filename>.pngStrip the data:image/png;base64, prefix before decoding.
6. Clean Up
Close the browser and kill the HTTP server:
browser_closekill $SERVER_PIDKey Details
- Import path: Export functions are on
utils.default, not named exports — this is howesm.shwraps the@excalidraw/utilspackage - Console errors:
<text> attribute y: Expected lengthwarnings are cosmetic — exports are valid - Background:
exportBackground: trueincludes the white background in exports - Output location: Save exported files alongside the
.excalidrawfile with matching filename (e.g.,system-architecture.excalidraw→system-architecture.svg,system-architecture.png) - Visual fidelity: Both exports produce the same visual output as opening in excalidraw.com
Troubleshooting
| Issue | Fix |
|---|---|
| Port already in use | Try a different port: python3 -m http.server 9876 & |
| Dynamic import fails | Check network connectivity; esm.sh CDN must be reachable |
| Playwright tools not available | Ensure Playwright MCP server is configured and running |
| PNG is blank/corrupted | Verify the base64 prefix was stripped before decoding |
| SVG missing text | Cosmetic only — text renders correctly when opened in a browser |
Excalidraw JSON Format Reference
Complete reference for Excalidraw JSON structure and element types.
---
File Structure
{
"type": "excalidraw",
"version": 2,
"source": "claude-code-excalidraw-skill",
"elements": [],
"appState": {
"gridSize": 20,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}---
Element Types
| Type | Use For | Arrow Reliability |
|---|---|---|
rectangle | Services, components, databases, containers, orchestrators, decision points | Excellent |
ellipse | Users, external systems, start/end points | Good |
text | Labels inside shapes, titles, annotations | N/A |
arrow | Data flow, connections, dependencies | N/A |
line | Grouping boundaries, separators | N/A |
BANNED: Diamond Shapes
NEVER use `type: "diamond"` in generated diagrams.
Diamond arrow connections are fundamentally broken in raw Excalidraw JSON:
- Excalidraw applies
roundnessto diamond vertices during rendering - Visual edges appear offset from mathematical edge points
- No offset formula reliably compensates
- Arrows appear disconnected/floating
Use styled rectangles instead for visual distinction:
| Semantic Meaning | Rectangle Style |
|---|---|
| Orchestrator/Hub | Coral (#ffa8a8/#c92a2a) + strokeWidth: 3 |
| Decision Point | Orange (#ffd8a8/#e8590c) + dashed stroke |
| Central Router | Larger size + bold color |
---
Required Element Properties
Every element MUST have these properties:
{
"id": "unique-id-string",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 3 },
"seed": 1,
"version": 1,
"versionNonce": 1,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false
}---
Text Inside Shapes (Labels)
Every labeled shape requires TWO elements:
Shape with boundElements
{
"id": "{component-id}",
"type": "rectangle",
"x": 500,
"y": 200,
"width": 200,
"height": 90,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"boundElements": [{ "type": "text", "id": "{component-id}-text" }],
// ... other required properties
}Text with containerId
{
"id": "{component-id}-text",
"type": "text",
"x": 505, // shape.x + 5
"y": 220, // shape.y + (shape.height - text.height) / 2
"width": 190, // shape.width - 10
"height": 50,
"text": "{Component Name}\n{Subtitle}",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "{component-id}",
"originalText": "{Component Name}\n{Subtitle}",
"lineHeight": 1.25,
// ... other required properties
}DO NOT Use the label Property
The label property is for the JavaScript API, NOT raw JSON files:
// WRONG - will show empty boxes
{ "type": "rectangle", "label": { "text": "My Label" } }
// CORRECT - requires TWO elements
// 1. Shape with boundElements reference
// 2. Separate text element with containerIdText Positioning
- Text
x= shapex+ 5 - Text
y= shapey+ (shape.height - text.height) / 2 - Text
width= shapewidth- 10 - Use
\nfor multi-line labels - Always use
textAlign: "center"andverticalAlign: "middle"
ID Naming Convention
Always use pattern: {shape-id}-text for text element IDs.
---
Dynamic ID Generation
IDs and labels are generated from codebase analysis:
| Discovered Component | Generated ID | Generated Label |
|---|---|---|
| Express API server | express-api | "API Server\nExpress.js" |
| PostgreSQL database | postgres-db | "PostgreSQL\nDatabase" |
| Redis cache | redis-cache | "Redis\nCache Layer" |
| S3 bucket for uploads | s3-uploads | "S3 Bucket\nuploads/" |
| Lambda function | lambda-processor | "Lambda\nProcessor" |
| React frontend | react-frontend | "React App\nFrontend" |
---
Grouping with Dashed Rectangles
For logical groupings (namespaces, VPCs, pipelines):
{
"id": "group-ai-pipeline",
"type": "rectangle",
"x": 100,
"y": 500,
"width": 1000,
"height": 280,
"strokeColor": "#9c36b5",
"backgroundColor": "transparent",
"strokeStyle": "dashed",
"roughness": 0,
"roundness": null,
"boundElements": null
}Group labels are standalone text (no containerId) at top-left:
{
"id": "group-ai-pipeline-label",
"type": "text",
"x": 120,
"y": 510,
"text": "AI Processing Pipeline (Cloud Run)",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null
}Validation Reference
Checklists, validation algorithms, and common bug fixes.
---
Pre-Flight Validation Algorithm
Run BEFORE writing the file:
FUNCTION validateDiagram(elements):
errors = []
// 1. Validate shape-text bindings
FOR each shape IN elements WHERE shape.boundElements != null:
FOR each binding IN shape.boundElements:
textElement = findById(elements, binding.id)
IF textElement == null:
errors.append("Shape {shape.id} references missing text {binding.id}")
ELSE IF textElement.containerId != shape.id:
errors.append("Text containerId doesn't match shape")
// 2. Validate arrow connections
FOR each arrow IN elements WHERE arrow.type == "arrow":
sourceShape = findShapeNear(elements, arrow.x, arrow.y)
IF sourceShape == null:
errors.append("Arrow {arrow.id} doesn't start from shape edge")
finalPoint = arrow.points[arrow.points.length - 1]
endX = arrow.x + finalPoint[0]
endY = arrow.y + finalPoint[1]
targetShape = findShapeNear(elements, endX, endY)
IF targetShape == null:
errors.append("Arrow {arrow.id} doesn't end at shape edge")
IF arrow.points.length > 2:
IF arrow.elbowed != true:
errors.append("Arrow {arrow.id} missing elbowed:true")
IF arrow.roundness != null:
errors.append("Arrow {arrow.id} should have roundness:null")
// 3. Validate unique IDs
ids = [el.id for el in elements]
duplicates = findDuplicates(ids)
IF duplicates.length > 0:
errors.append("Duplicate IDs: {duplicates}")
// 4. Validate bounding boxes
FOR each arrow IN elements WHERE arrow.type == "arrow":
maxX = max(abs(p[0]) for p in arrow.points)
maxY = max(abs(p[1]) for p in arrow.points)
IF arrow.width < maxX OR arrow.height < maxY:
errors.append("Arrow {arrow.id} bounding box too small")
RETURN errors
FUNCTION findShapeNear(elements, x, y, tolerance=15):
FOR each shape IN elements WHERE shape.type IN ["rectangle", "ellipse"]:
edges = [
(shape.x + shape.width/2, shape.y), // top
(shape.x + shape.width/2, shape.y + shape.height), // bottom
(shape.x, shape.y + shape.height/2), // left
(shape.x + shape.width, shape.y + shape.height/2) // right
]
FOR each edge IN edges:
IF abs(edge.x - x) < tolerance AND abs(edge.y - y) < tolerance:
RETURN shape
RETURN null---
Checklists
Before Generating
- [ ] Identified all components from codebase
- [ ] Mapped all connections/data flows
- [ ] Chose layout pattern (vertical, horizontal, hub-and-spoke)
- [ ] Selected color palette (default, AWS, Azure, K8s)
- [ ] Planned grid positions
- [ ] Created ID naming scheme
During Generation
- [ ] Every labeled shape has BOTH shape AND text elements
- [ ] Shape has
boundElements: [{ "type": "text", "id": "{id}-text" }] - [ ] Text has
containerId: "{shape-id}" - [ ] Multi-point arrows have
elbowed: true,roundness: null,roughness: 0 - [ ] Arrows have
startBindingandendBinding - [ ] No diamond shapes used
- [ ] Applied staggering formula for multiple arrows
Arrow Validation (Every Arrow)
- [ ] Arrow
x,ycalculated from shape edge - [ ] Final point offset =
targetEdge - sourceEdge - [ ] Arrow
width=max(abs(point[0])) - [ ] Arrow
height=max(abs(point[1])) - [ ] U-turn arrows have 40-60px clearance
After Generation
- [ ] All
boundElementsIDs reference valid text elements - [ ] All
containerIdvalues reference valid shapes - [ ] All arrows start within 15px of shape edge
- [ ] All arrows end within 15px of shape edge
- [ ] No duplicate IDs
- [ ] Arrow bounding boxes match points
- [ ] File is valid JSON
---
Common Bugs and Fixes
Bug: Arrow appears disconnected/floating
Cause: Arrow x,y not calculated from shape edge.
Fix:
Rectangle bottom: arrow_x = shape.x + shape.width/2
arrow_y = shape.y + shape.heightBug: Arrow endpoint doesn't reach target
Cause: Final point offset calculated incorrectly.
Fix:
target_edge = (target.x + target.width/2, target.y)
offset_x = target_edge.x - arrow.x
offset_y = target_edge.y - arrow.y
Final point = [offset_x, offset_y]Bug: Multiple arrows from same source overlap
Cause: All arrows start from identical x,y.
Fix: Stagger start positions:
For 5 arrows from bottom edge:
arrow1.x = shape.x + shape.width * 0.2
arrow2.x = shape.x + shape.width * 0.35
arrow3.x = shape.x + shape.width * 0.5
arrow4.x = shape.x + shape.width * 0.65
arrow5.x = shape.x + shape.width * 0.8Bug: Callback arrow doesn't loop correctly
Cause: U-turn path lacks clearance.
Fix: Use 4-point path:
Points = [[0, 0], [clearance, 0], [clearance, -vert], [final_x, -vert]]
clearance = 40-60pxBug: Labels don't appear inside shapes
Cause: Using label property instead of separate text element.
Fix: Create TWO elements: 1. Shape with boundElements referencing text 2. Text with containerId referencing shape
Bug: Arrows are curved, not 90-degree
Cause: Missing elbow properties.
Fix: Add all three:
{
"roughness": 0,
"roundness": null,
"elbowed": true
}