excalidraw — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited excalidraw (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Generate valid .excalidraw JSON files directly from a text description. No API keys, no external services. Output opens in Excalidraw web app, VS Code extension, or Obsidian plugin.
Gather from the user:
If the user gives a quick description ("draw a flowchart of X"), infer the rest and proceed. Don't over-ask.
Before generating JSON, present a quick element list:
Diagram: [name]
Type: vertical flowchart, 5 nodes
Nodes:
1. "Input Data" (rectangle)
2. "Process" (rectangle)
3. "Decision" (diamond, accent)
4. "Output A" (rectangle)
5. "Output B" (rectangle)
Arrows: 1→2, 2→3, 3→4, 3→5
Annotations: note on node 3
Proceed?If the user approves or doesn't object, generate the JSON.
Build the complete .excalidraw JSON following the schema reference below. Every element must have all required fields — Excalidraw silently breaks on missing fields.
Save to the appropriate location:
drafts/{slug}/diagram-name.excalidrawcontent-pipeline-flow.excalidraw| Property | Value | Notes |
|---|---|---|
| strokeColor | #1F2937 | Charcoal, default for all elements |
| accent strokeColor | #B45309 | Rust, for highlighted nodes |
| accent backgroundColor | #FEF3E7 | Light warm fill on accent nodes |
| annotation strokeColor | #6B7280 | Gray, for side comments |
| annotation line strokeStyle | dashed | Dashed leader lines to annotations |
| viewBackgroundColor | #FAFAF7 | Warm off-white canvas |
| fontFamily | 3 | Monospace (code-on-graph-paper aesthetic) |
| fontSize | 18-20 | Body text in nodes |
| annotation fontSize | 14 | Smaller for side notes |
| roughness | 0 | Clean lines, no hand-drawn wobble |
| strokeWidth | 2 (shapes), 1.5 (arrows), 1 (annotations) | |
| roundness | null | Sharp corners on all shapes |
| fillStyle | solid | When backgroundColor is set |
x: 160, y: 60280 × 56 (single-line), 280 × 72 (two-line), expand for more textfontSize × 1.25lineCount × fontSize × 1.25nodeHeight = textHeight + 20 (10px top/bottom padding)| Edge | X | Y |
|---|---|---|
| Top | x + width/2 | y |
| Bottom | x + width/2 | y + height |
| Left | x | y + height/2 |
| Right | x + width | y + height/2 |
For orchestrator/event-bus diagrams. 8 positions at 45-degree increments:
Center hub: (cx, cy), radius r = 200px
N: (cx, cy - r) NE: (cx + 0.7r, cy - 0.7r)
E: (cx + r, cy) SE: (cx + 0.7r, cy + 0.7r)
S: (cx, cy + r) SW: (cx - 0.7r, cy + 0.7r)
W: (cx - r, cy) NW: (cx - 0.7r, cy - 0.7r)For logical groupings (namespaces, pipeline stages, bounded contexts), use a large transparent dashed rectangle with a standalone label:
// In spec, add as a node with no arrows:
{"ref": "group1", "type": "rectangle", "x": 80, "y": 50, "width": 400, "height": 300,
"strokeStyle": "dashed", "strokeColor": "#9c36b5", "backgroundColor": "transparent"}
// Add a standalone label in freeText:
{"text": "Group Name", "x": 100, "y": 60, "fontSize": 14, "strokeColor": "#9c36b5"}Size the group rectangle to encompass all child elements + 40px padding on each side.
| Complexity | Max Elements | Approach |
|---|---|---|
| Simple | 5-10 | Single file, no groups |
| Medium | 10-25 | Use grouping rectangles |
| Complex | 25-50 | Split into multiple diagrams |
| 50+ | — | Multiple focused diagrams: overview.excalidraw + detail-{subsystem}.excalidraw |
Use the align property on freeText elements for automatic positioning:
"align": "center" — centers the text horizontally over all nodes"align": "left" — positions to the left of all nodes (for row labels like INPUTS, OUTPUTS)When using align, the x value is ignored and computed automatically.
x: nodeRightEdge + 70{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [],
"appState": {
"viewBackgroundColor": "#FAFAF7",
"gridSize": null,
"gridStep": 5,
"gridModeEnabled": false
},
"files": {}
}type string "rectangle" | "text" | "arrow" | "line" | "ellipse" | "diamond"
version number 1
versionNonce number unique integer (1001, 1002, ...)
index string fractional ordering: "a0"..."az" (62 values), then "b00"..."bzz" (3844 values)
isDeleted boolean false
id string short unique: "r1", "r2" (rectangles), "t1" (text), "a1" (arrows), "d1" (diamonds), "ll1" (lines), "at1" (annotation text)
fillStyle string "solid"
strokeWidth number 2 (shapes), 1.5 (arrows), 1 (text/annotations)
strokeStyle string "solid" | "dashed"
roughness number 0
opacity number 100
angle number 0
x number left edge position
y number top edge position
strokeColor string hex color
backgroundColor string hex color or "transparent"
width number element width
height number element height
seed number unique integer (101, 102, ...)
groupIds array []
frameId null null
roundness null null (sharp corners)
boundElements array see binding rules below
updated number timestamp in ms (use 1714400000000)
link null null
locked boolean falseAll common fields plus boundElements linking to contained text and connected arrows.
{
"type": "rectangle",
"boundElements": [
{"id": "t1", "type": "text"},
{"id": "a1", "type": "arrow"}
]
}For accent nodes, add:
{
"strokeColor": "#B45309",
"backgroundColor": "#FEF3E7"
}Diamond arrow connections are unreliable in raw Excalidraw JSON because Excalidraw applies roundness to diamond vertices during rendering, causing visual offset from mathematical edge points. Prefer a styled rectangle (e.g., dashed stroke, accent color) for decision points. The generator emits a warning if diamonds are used.
If you must use a diamond, size it larger (~320 x 180) because the shape clips content more aggressively.
Same as rectangle but "type": "ellipse".
All common fields plus text-specific fields. The containerId binds it to the parent shape.
{
"type": "text",
"strokeWidth": 1,
"text": "NODE LABEL",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "r1",
"originalText": "NODE LABEL",
"autoResize": true,
"lineHeight": 1.25,
"boundElements": []
}Position: x = container.x + 10, y = container.y + (container.height - textHeight) / 2 Size: width = container.width - 20, height = lineCount × fontSize × 1.25
For multi-line text, use \n in both text and originalText.
Same as contained text but:
containerId: nullverticalAlign: "top"textAlign: "left"strokeColor: "#6B7280" (gray for annotations)fontSize: 14All common fields plus arrow-specific binding fields.
{
"type": "arrow",
"strokeWidth": 1.5,
"points": [[0, 0], [0, 68]],
"startBinding": {
"elementId": "r1",
"focus": 0,
"gap": 1
},
"endBinding": {
"elementId": "r2",
"focus": 0,
"gap": 1
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false,
"boundElements": []
}Arrow position: calculated from the fixedPoint on the source shape's edge. Points: [[0, 0], [dx, dy]] where dx/dy is the offset to the target's fixedPoint.
fixedPoint bindings: Arrows use explicit anchor points — [0.5, 0] = top center, [0.5, 1] = bottom center, [0, 0.5] = left center, [1, 0.5] = right center. When multiple arrows share the same edge, the generator aligns each exit/entry point with the connected shape's center for straight vertical/horizontal arrows.
Arrowhead types:
| Value | Appearance |
|---|---|
null | No arrowhead |
"arrow" | Standard triangle (default for endArrowhead) |
"bar" | Flat terminator line |
"dot" | Circle/dot |
"triangle" | Filled triangle |
For bidirectional arrows (two-way data flows), set both: "startArrowhead": "arrow", "endArrowhead": "arrow".
Arrow point patterns:
| Pattern | Points | Use Case |
|---|---|---|
| Straight down | [[0,0], [0,h]] | Vertical connection |
| Straight right | [[0,0], [w,0]] | Horizontal connection |
| L-right-down | [[0,0], [w,0], [w,h]] | Go right then down |
| L-down-right | [[0,0], [0,h], [w,h]] | Go down then right |
| L-down-left | [[0,0], [0,h], [-w,h]] | Go down then left |
| L-left-down | [[0,0], [-w,0], [-w,h]] | Go left then down |
| S-shape | [[0,0], [0,h1], [w,h1], [w,h2]] | Navigate around obstacles |
| U-turn (right) | [[0,0], [50,0], [50,dy], [dx,dy]] | Feedback loops (same-edge) |
| U-turn (bottom) | [[0,0], [0,50], [dx,50], [dx,dy]] | Callback arrows |
Multi-point arrows (3+ points) must set "elbowed": true for clean 90-degree rendering.
Arrow routing decision tree (for manual points overrides):
IF source bottom → target top:
|dx| < 10? → straight: [[0,0], [0, dy]]
otherwise → L-shape: [[0,0], [dx, 0], [dx, dy]]
IF source right → target left:
|dy| < 10? → straight: [[0,0], [dx, 0]]
otherwise → L-shape: [[0,0], [0, dy], [dx, dy]]
IF same edge (U-turn):
clearance = 50px
right: [[0,0], [50, 0], [50, dy], [dx, dy]]
bottom: [[0,0], [0, 50], [dx, 50], [dx, dy]]For accent arrows (leading to/from accent nodes):
{
"strokeColor": "#B45309",
"strokeWidth": 2
}Same as arrow but no bindings and no arrowheads.
{
"type": "line",
"strokeWidth": 1,
"strokeStyle": "dashed",
"strokeColor": "#6B7280",
"points": [[0, 0], [80, 0]],
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
}Bindings are bidirectional. Both sides must reference each other or Excalidraw breaks silently.
Text in a shape:
boundElements includes {"id": "text-id", "type": "text"}containerId = shape's idArrow connecting two shapes:
startBinding.elementId = source shape idendBinding.elementId = target shape idboundElements includes {"id": "arrow-id", "type": "arrow"}boundElements includes {"id": "arrow-id", "type": "arrow"}A shape with text AND arrows:
"boundElements": [
{"id": "t1", "type": "text"},
{"id": "a1", "type": "arrow"},
{"id": "a2", "type": "arrow"}
]Excalidraw uses @excalidraw/fractional-indexing with base-62 digits (0-9, A-Z, a-z). The head character encodes the integer length:
a prefix → 2 chars total: a0 through az (62 values)b prefix → 3 chars total: b00 through bzz (3844 values)c prefix → 4 chars total: c000 through czzzCRITICAL: b0, c0, c4 etc. are INVALID — b requires 2 digits after it, c requires 3. Fractional parts (anything after the integer) must NOT end with 0.
Elements are assigned sequential integer-only indices. The generator handles this automatically. For 62 or fewer elements, all indices stay in a0–az. For larger diagrams, they overflow into b00+.
Pattern: shapes first (with their contained text interleaved), then arrows, then annotations. This keeps z-ordering correct.
| Element type | Prefix | Example |
|---|---|---|
| Rectangle | r | r1, r2, r3 |
| Diamond | d | d1, d2 |
| Ellipse | e | e1, e2 |
| Contained text | t | t1, t2 |
| Arrow | a | a1, a2 |
| Annotation text | at | at1, at2 |
| Leader line | ll | ll1, ll2 |
Keep these in separate ranges to avoid collisions:
VersionNonce follows the same pattern: 1001+ (rectangles), 2001+ (text), 3001+ (arrows), 4001+ (annotation text), 5001+ (lines), 6001+ (diamonds).
The generator runs a validation pass before outputting JSON. It catches:
containerId pointing to a missing shape, or shape boundElements referencing a missing text/arrowboundElements doesn't list the arrow (or vice versa)width/height not matching max(abs(point)) across all path pointselbowed: true and roundness: nullIf validation fails, the generator raises an error with details. Fix the spec and regenerate.
index values are unique, in order, and valid (a0–az, then b00+; never b0/c0/c4)id values are uniqueseed values are uniquepoints match the actual distance between source and targetwidth/height = max(abs(point[0/1])) across ALL points (not just the endpoint)x, y position matches the staggered fixedPoint on the source shape's edgegroupIds: [] or frameId: null — they're all required.<!-- Updated by /reflect. Promote stable patterns to the main skill body. -->
generate_excalidraw.py is the only supported approach. Live MCP collab (excaliclaude) was tested and abandoned due to unreliable rendering. (Session 10, 2026-05-04)a prefix = 2 chars (a0–az), b prefix = 3 chars (b00–bzz). Using b0, c0, c4 etc. makes files uneditable — Excalidraw can't generate new indices relative to malformed ones. The generator now handles this correctly. (Session 11, 2026-05-05)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.