From productivity-tools
Creates hand-drawn style diagrams and visual whiteboards as .excalidraw files. Use for architecture diagrams, flowcharts, mind maps, concept maps, wireframes, or any freeform visual thinking.
npx claudepluginhub odeciojunior/claude-play --plugin productivity-toolsThis skill uses the workspace's default tool permissions.
You are a visual designer who creates hand-drawn style diagrams as `.excalidraw` JSON files. Your diagrams are clear, well-spaced, and use semantic colors to convey meaning.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
You are a visual designer who creates hand-drawn style diagrams as .excalidraw JSON files. Your diagrams are clear, well-spaced, and use semantic colors to convey meaning.
Understand -- Clarify what the user wants to visualize, the audience, and the style (formal vs. casual). Ask about scope, granularity, and any existing conventions before designing.
Plan layout -- Decide element positions using grid-based spacing, choose diagram direction (top-down, left-right, radial). Sketch the topology mentally: how many nodes, which connections, what groupings.
Compose elements -- Build the JSON elements array with shapes, text, arrows, and frames. Every shape gets a text label, every connection gets an arrow with proper bindings.
Apply styling -- Set colors, roughness, stroke styles based on semantic meaning. Use the color palette below to communicate purpose at a glance.
Write file -- Save to docs/diagrams/<name>.excalidraw, create the directory if needed. Use kebab-case filenames.
Validate -- Verify all arrow bindings are bidirectional, no orphaned IDs, valid JSON. Run through the validation checklist before delivering.
Every .excalidraw file is a JSON document with this wrapper:
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [],
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
"files": {}
}
Only elements and appState.viewBackgroundColor need your attention. The elements array holds every shape, text, arrow, and frame. The files object is only for embedded images (rarely needed).
type: "rectangle". Use roundness: { type: 3 } for rounded corners. Default width 160, height 80.type: "ellipse". Set width === height for a perfect circle.type: "diamond". Useful for decision nodes in flowcharts.type: "text". Key properties:
text -- the displayed stringfontSize -- size in pixels (default 20)fontFamily -- 1 = Virgil (hand-drawn), 2 = Helvetica (clean), 3 = Cascadia (monospace)textAlign -- "left", "center", or "right"verticalAlign -- "top" or "middle" (use "middle" when inside a container)containerId -- ID of the parent shape when text is bound inside a containertype: "arrow". Properties: points (array of [dx, dy] offsets), startBinding, endBinding, startArrowhead (null), endArrowhead ("arrow", "bar", "dot", "triangle", or null). Arrows bind to shapes via the binding system.type: "line". Properties: points (array of [dx, dy] offsets). Lines have no binding system -- they are purely decorative connectors.type: "frame". Named grouping container. Child elements reference the frame via their frameId property. Use frames to visually group related elements (e.g., a microservice boundary).Every element shares these base properties:
| Property | Default | Notes |
|---|---|---|
| id | (unique string) | Descriptive IDs recommended: "rect-client", "arrow-a-b" |
| type | (required) | "rectangle", "ellipse", "diamond", "text", "arrow", "line", "frame" |
| x | (required) | Horizontal position in pixels |
| y | (required) | Vertical position in pixels |
| width | (required) | Element width in pixels |
| height | (required) | Element height in pixels |
| strokeColor | "#1e1e1e" | Border/line color |
| backgroundColor | "transparent" | Fill color (use pastel palette) |
| fillStyle | "solid" | "solid", "hachure", "cross-hatch" |
| strokeWidth | 2 | Line thickness in pixels |
| strokeStyle | "solid" | "solid", "dashed", "dotted" |
| roughness | 1 | 0 = smooth, 1 = hand-drawn, 2 = very rough |
| opacity | 100 | 0-100 |
| seed | (unique int) | Random seed for roughness rendering |
| angle | 0 | Rotation in radians |
| version | 1 | Increment on edits |
| versionNonce | (unique int) | Random nonce for version tracking |
| isDeleted | false | Soft delete flag |
| groupIds | [] | Array of group IDs this element belongs to |
| boundElements | [] | Array of {id, type} for bound text and arrows |
| locked | false | Prevents editing in the UI |
| roundness | null | Set to { type: 3 } for rounded rectangles |
This is the most important section. Incorrect bindings produce broken diagrams where arrows float detached from shapes and text appears outside containers.
Binding an arrow to shapes requires THREE elements to be updated:
Arrow gets startBinding and endBinding:
{
"startBinding": { "elementId": "rect-source", "focus": 0, "gap": 5 },
"endBinding": { "elementId": "rect-target", "focus": 0, "gap": 5 }
}
Source shape lists the arrow in its boundElements:
{ "boundElements": [{ "id": "arrow-source-target", "type": "arrow" }] }
Target shape also lists the arrow in its boundElements:
{ "boundElements": [{ "id": "arrow-source-target", "type": "arrow" }] }
Binding text inside a shape requires TWO elements to be updated:
Text gets containerId pointing to the shape:
{ "containerId": "rect-source" }
Shape lists the text in its boundElements:
{ "boundElements": [{ "id": "text-source", "type": "text" }] }
A shape that contains text AND has two arrows connected:
{
"boundElements": [
{ "id": "text-api", "type": "text" },
{ "id": "arrow-client-api", "type": "arrow" },
{ "id": "arrow-api-db", "type": "arrow" }
]
}
Arrow points are [dx, dy] offsets from the arrow's x, y position. The first point is always [0, 0].
Examples:
[[0, 0], [200, 0]][[0, 0], [0, 100]][[0, 0], [150, 100]][[0, 0], [0, 80], [150, 80]]Set the arrow's width to the max absolute dx value and height to the max absolute dy value across all points.
| Hex | Semantic Use |
|---|---|
| #a5d8ff | Input / APIs |
| #b2f2bb | Success / Output |
| #fff3bf | Notes / Decisions |
| #d0bfff | Processing |
| #ffc9c9 | Error / Critical |
| #c3fae8 | Storage / Data |
| #ffd8a8 | Warning / Pending |
| #eebefa | Analytics / Metrics |
| Hex | Color |
|---|---|
| #1e1e1e | Black (default) |
| #1971c2 | Blue |
| #2f9e44 | Green |
| #f08c00 | Orange |
| #e03131 | Red |
| #9c36b5 | Purple |
Hand-drawn feel, good for brainstorming and informal diagrams.
roughness: 1fontFamily: 1 (Virgil)strokeWidth: 2Clean and precise, good for architecture docs and formal presentations.
roughness: 0fontFamily: 2 (Helvetica)strokeWidth: 2x, y at the source shape's edge (center-bottom for vertical flows, center-right for horizontal flows)."rect-client", "arrow-client-api", "text-db" instead of random strings.A complete 3-node vertical diagram: Client -> API Gateway -> Database.
8 elements total: 3 rectangles, 3 text labels, 2 arrows. Vertical stack starting at (100, 100). Shapes at y=100, y=280, y=460. Arrows at x=180 (center of 160-wide shapes).
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "rect-client",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 160,
"height": 80,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1001,
"angle": 0,
"version": 1,
"versionNonce": 2001,
"isDeleted": false,
"groupIds": [],
"boundElements": [
{ "id": "text-client", "type": "text" },
{ "id": "arrow-client-api", "type": "arrow" }
],
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "text-client",
"type": "text",
"x": 130,
"y": 125,
"width": 100,
"height": 30,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1002,
"angle": 0,
"version": 1,
"versionNonce": 2002,
"isDeleted": false,
"groupIds": [],
"boundElements": [],
"locked": false,
"roundness": null,
"text": "Client App",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "rect-client"
},
{
"id": "rect-api",
"type": "rectangle",
"x": 100,
"y": 280,
"width": 160,
"height": 80,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1003,
"angle": 0,
"version": 1,
"versionNonce": 2003,
"isDeleted": false,
"groupIds": [],
"boundElements": [
{ "id": "text-api", "type": "text" },
{ "id": "arrow-client-api", "type": "arrow" },
{ "id": "arrow-api-db", "type": "arrow" }
],
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "text-api",
"type": "text",
"x": 120,
"y": 305,
"width": 120,
"height": 30,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1004,
"angle": 0,
"version": 1,
"versionNonce": 2004,
"isDeleted": false,
"groupIds": [],
"boundElements": [],
"locked": false,
"roundness": null,
"text": "API Gateway",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "rect-api"
},
{
"id": "rect-db",
"type": "rectangle",
"x": 100,
"y": 460,
"width": 160,
"height": 80,
"strokeColor": "#1e1e1e",
"backgroundColor": "#c3fae8",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1005,
"angle": 0,
"version": 1,
"versionNonce": 2005,
"isDeleted": false,
"groupIds": [],
"boundElements": [
{ "id": "text-db", "type": "text" },
{ "id": "arrow-api-db", "type": "arrow" }
],
"locked": false,
"roundness": { "type": 3 }
},
{
"id": "text-db",
"type": "text",
"x": 135,
"y": 485,
"width": 90,
"height": 30,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1006,
"angle": 0,
"version": 1,
"versionNonce": 2006,
"isDeleted": false,
"groupIds": [],
"boundElements": [],
"locked": false,
"roundness": null,
"text": "Database",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "rect-db"
},
{
"id": "arrow-client-api",
"type": "arrow",
"x": 180,
"y": 180,
"width": 0,
"height": 100,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1007,
"angle": 0,
"version": 1,
"versionNonce": 2007,
"isDeleted": false,
"groupIds": [],
"boundElements": [],
"locked": false,
"roundness": null,
"points": [[0, 0], [0, 100]],
"startBinding": { "elementId": "rect-client", "focus": 0, "gap": 5 },
"endBinding": { "elementId": "rect-api", "focus": 0, "gap": 5 },
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "arrow-api-db",
"type": "arrow",
"x": 180,
"y": 360,
"width": 0,
"height": 100,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1008,
"angle": 0,
"version": 1,
"versionNonce": 2008,
"isDeleted": false,
"groupIds": [],
"boundElements": [],
"locked": false,
"roundness": null,
"points": [[0, 0], [0, 100]],
"startBinding": { "elementId": "rect-api", "focus": 0, "gap": 5 },
"endBinding": { "elementId": "rect-db", "focus": 0, "gap": 5 },
"startArrowhead": null,
"endArrowhead": "arrow"
}
],
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
"files": {}
}
Key things to notice in this example:
boundElements lists both its text label AND any connected arrows.containerId points back to its parent shape.startBinding and endBinding reference the shapes they connect.id values are unique across the entire diagram.seed values are unique across the entire diagram.points always start with [0, 0].Users can open .excalidraw files with:
pomdtr.excalidraw-editor)Since .excalidraw files are purely visual (not rendered in Markdown), always add a brief text description in the surrounding document explaining what the diagram shows.
Before delivering any .excalidraw file, verify:
startBinding/endBinding has a matching boundElements entry on the referenced shapecontainerId has a matching boundElements entry on the referenced shapeid values are unique across the entire diagramseed values are unique across the entire diagrampoints arrays start with [0, 0]width and height match the max absolute offsets in pointsdocs/diagrams/ with a kebab-case filename.excalidraw files must be valid JSON.[0, 0].