From whiteboard
Draw and annotate visuals with your AI agent on a shared Excalidraw canvas. Use it when screen layout, structure, flow, or comparison still feels too ambiguous in text alone.
How this skill is triggered — by the user, by Claude, or both
Slash command
/whiteboard:drawing-visualsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Like a whiteboard on the wall of a meeting room, this is a tool for AI and humans to **align by drawing on the same Excalidraw workspace**.
references/cloud-and-network-zones.mdreferences/comparison-splits.mdreferences/dark-mode-techniques.mdreferences/decomposition-and-trees.mdreferences/flow-and-architecture.mdreferences/infrastructure-diagrams.mdreferences/layered-architectures.mdreferences/library-first-workflow.mdreferences/library-research-prompt-template.mdreferences/pipelines-and-roadmaps.mdreferences/reading-map.mdreferences/review-and-comparison.mdreferences/sequence-and-decisions.mdreferences/trust-boundary-and-security.mdreferences/workflows-and-swimlanes.mdstyle-reference.mdvisual-vocabulary.mdLike a whiteboard on the wall of a meeting room, this is a tool for AI and humans to align by drawing on the same Excalidraw workspace. Use it when drawing and pointing is faster than iterating in prose. What you draw stays on the canvas and can be revisited and refined later.
Use the whiteboard MCP tools (canvas_create / canvas_list / canvas_open / template_list / template_insert / library_catalog_list / library_list_items / library_insert_item / library_insert_batch / user_library_save / user_library_list / user_library_remove / user_library_metadata_get / user_library_metadata_set / user_library_metadata_delete / annotate / annotate_batch / palette_get / palette_set / palette_delete / load_image / export_png / canvas_export_json / canvas_inspect / update_element / delete_element / delete_elements / move_elements / align_elements / distribute_elements / canvas_clear / assign_to_group / delete_group / list_groups / create_frame / update_frame_members / viewport_set / version_save / version_restore / version_list) to create a canvas, draw the diagram, and export it as PNG or standard .excalidraw JSON.
Open exported PNGs with the Read tool and inspect them visually.
Open references/reading-map.md first and read only the note you need.
references/reading-map.md: the routing note that tells you which guidance to open for which kind of diagramreferences/library-first-workflow.md: how to work library-first for icon-heavy diagrams; open only when neededstyle-reference.md: the deep reference for coordinates, color, frames, and layout recipes; open only when neededvisual-vocabulary.md: the deep reference for labeling, diagram choice, and anti-patterns; open only when neededDo not read style-reference.md and visual-vocabulary.md end-to-end every time.
Choose the diagram type through the reading map, then open only 1-2 relevant notes.
If you need the collaborative workflow for tightening the visual together while talking with the user, also open ../coauthoring-visuals/SKILL.md.
This drawing-visuals skill covers canvas operations, diagram vocabulary, and drawing mechanics.
coauthoring-visuals covers context gathering, frame-by-frame refinement, and fresh-viewer testing.
Do not wait for the user to ask explicitly. The moment it feels like "drawing would be faster than prose," propose it and use it.
When in doubt, draw.
The cost of drawing is low; the cost of proceeding under false alignment is high.
If the diagram turns out unnecessary, a quick canvas_create can be discarded.
/drawing-visuals - export the current canvas and consider the next adjustment/drawing-visuals <canvasId> - work in the specified canvasThis is the working loop after you pick up the whiteboard. Repeat draw -> inspect -> fix until the board converges.
If the domain depends heavily on icons (AWS / GCP / K8s / network / UML / flowchart and similar), search the official catalog and saved libraries before hand-drawing rectangles. Using established icons improves fidelity and removes needless work around shape and color design.
Open references/library-first-workflow.md for the full workflow.
At minimum:
user_library_list and library_catalog_listuser_library_save and inspect contents with library_list_itemstrial insert rather than inserting directly into productionuser_library_metadata_setYou can skip this step for generic rectangle-and-arrow flows, comparison matrices, before/after diffs, and screenshot annotation.
If you cannot rely on a dedicated Claude Code subagent, hand references/library-research-prompt-template.md to a General Subagent for library research.
If a comparison axis is already known before drawing - for example plans A/B/C, main path vs exception path, or stable color families - define workspace color keys first with palette_set({ workspaceId, entries }), then start annotate_batch.
Hardcoded hex is easy to drift and hard for future readers or another AI to inherit correctly.
palette_set({ workspaceId, entries: {
"plan.a.bg": "#dbeafe", "plan.a.fg": "#1971c2",
"plan.b.bg": "#f3e8ff", "plan.b.fg": "#9333ea",
"plan.c.bg": "#d1fae5", "plan.c.fg": "#047857",
"recommend.bg": "#fffbeb",
"system.bg": "#f1f5f9", "system.fg": "#475569"
}})
Then use keys such as "plan.a.bg" in annotate / annotate_batch for color, backgroundColor, and strokeColor.
Shortcut reminder: the moment you decide the board uses named options, families, or priority colors, define the palette before drawing.
Describe the purpose of the diagram in 1-2 lines. Examples:
If the intent is fuzzy, open visual-vocabulary.md, look at "Choose the diagram from the question" and the "intent -> diagram" mapping, and narrow it down to one question this diagram answers.
Do not try to solve multiple questions in one frame or canvas at once.
Examples of questions that should be separated:
The choice of separate canvases vs multiple frames on one canvas should follow the guidance in visual-vocabulary.md.
If the diagrams belong to the same discussion, prefer 1 canvas + multiple frames.
Split into separate canvases only when independent export / sharing / audience separation is needed.
Once the intent is fixed, the drawing tool choice usually becomes obvious:
| Intent | Primary Tools |
|---|---|
| domain-specific icon diagram (AWS/GCP/K8s/UML) | library_catalog_list -> user_library_save -> library_insert_item |
| reusable structural components | template_list -> template_insert |
| flow with rectangles and arrows | annotate_batch with box_with_label + arrow |
| comparison matrix (NxM, same-size cells) | annotate_batch with layout grid + box_with_label |
| comparison table plus extra banner / footer | use absolute coordinates, not layout grid |
| grouping existing related elements | annotate_batch with group |
| emphasis / filled background | annotate with rectangle / highlight |
| annotate an existing screenshot | load_image -> annotate with coords: relative |
| one-off label or emphasis | annotate |
Once the diagram type is chosen, open the relevant recipe in style-reference.md.
It contains the shell, must-have elements, and common failure points for architecture, sequence, decision tree, directory tree, and comparison matrix patterns.
For vocabulary and visual distinction of main path / exceptions / uncertainty, prefer the guidance in visual-vocabulary.md.
library_insert_item. This often communicates more with less effort than a rectangle-and-text substitutetemplate_list, place with template_insert, then adjust the inserted elements with update_element / move_elementsviewport_set({ mode: "fit", padding: 40 }) so the whole composition is visible instead of leaving the browser on the default origin/zoomannotate_batch for multiple elements: this is cheaper and cleaner than many individual annotate callsautoFit: true): box_with_label wraps long lines on whitespace and expands height as needed. Even if you pass string[], long lines inside each entry can still wrap further. Use autoFit: false only when you need strict line controlprimary / success / danger / warning / neutral / info rather than hardcoded hexaccent.target or plan.a with palette_set, then use those keys in annotate / annotate_batchtext with width when you want wrapped headings or notesbox_with_label content into title / text / subText: use title for emphasis, push supporting detail into subText, and use subTextPosition: "top" only when the caption truly belongs outside the rectlayout.colWidths, layout.rowHeights, rowSpan, colSpan, and inspect dryRun: true before committingannotate_batch warnings: overflow, autoExpandedBy, and overlap warnings are early signals that spacing is too tightstartBoxId / endBoxId or the equivalent name-based attachment so the arrow snaps to box edgesupdate_element({
elementId: arrowId,
patch: {
points: [[0, 0], [midX, delta], [endX, 0]],
width: endX,
height: Math.abs(delta),
roundness: { type: 2 }
}
})
delta = -boxH for upper-band arrows and delta = +boxH for lower-band arrowscreate_frame and iterate with export_png({ frameId })template_insert({ variables: { service: "Billing API" } }) to substitute placeholders such as {{service}}create_frame. This also enables section-level export_png({ frameId })current / problem / proposal, think in frames first: on an infinite Excalidraw canvas, related frames are easier to compare than separate canvasesCurrent / Problem / Proposal, do not repeat the same heading inside it. Use the large text inside the frame for the conclusion or claimcreate_embed: embed the URL, then layer arrows / highlights with annotatereorder_elements for z-order fixes: push annotations to front when labels disappear behind embeds or other elementsexport_png({ canvasId })
// For large diagrams where small text gets crushed:
export_png({ canvasId, scale: 2, minFontPx: 16, padding: 32 })
// For inspecting only one section on a large canvas:
export_png({ canvasId, frameId: "<frame-id>", padding: 24 })
export_png waits briefly for the browser to settle after opening, but it does not automatically open the canvas.
If you need reliable export for a disconnected canvas, call canvas_open({ waitForClient: true }) first.
Options:
padding: whitespace around elements in px, default 10scale: export DPI multiplier, default 1minFontPx: boosts font size only in the export cloneframeId: exports only the frame plus its child elementsoutputPath: absolute path to write the PNG to. When omitted, write to the workspace exports diroverwrite: replace an existing file at outputPath. Default false; without it an existing file rejects with output_existstheme: "light" or "dark". Forces the rendered scene into the chosen theme without mutating persisted state. Pair both runs (outputPath: ".../foo-light.png" + theme: "light" and .../foo-dark.png + theme: "dark") when reviewing dark-mode contrast or shipping diagrams that may live in mixed themesInspect the exported PNG visually:
If the PNG is too large and the Read tool would burn too much context, optimize in this order:
frameIdscale to 0.5 or 0.7 if text remains legibleTo use frameId, set up frames in Step 2 with create_frame({ canvasId, memberIds, name }) so section-level export stays available across iterations.
canvas_inspect To Check StructureIf the diagram looks wrong but the reason is unclear:
canvas_inspect({ canvasId })
This returns each element's id / type / x,y / width,height / containerId. Use it to inspect things like broken box-label binding or unintended z-order.
The goal is not to minimize edit count. The goal is to restore correct shared understanding. If the issue is small, fine-tuning is enough. If the structure, layout, or intent itself is suspect, create a new canvas and redraw without hesitation.
| Situation | Best Action |
|---|---|
| change text, color, or font size | update_element({ elementId, patch: { ... } }) |
| move one or many elements | move_elements({ elementIds, dx, dy }) |
| align a few sibling boxes to a shared edge or centre | align_elements({ elementIds, alignment }) — left / right / center for x; top / bottom / middle for y; orthogonal axis untouched |
| even out the spacing of three or more elements along a row or column | distribute_elements({ elementIds, direction }) — horizontal along x, vertical along y; first and last stay fixed |
| delete one unnecessary element | delete_element({ elementId }) |
| delete many unnecessary elements together | delete_elements({ elementIds }) |
| delete and rebuild a whole section | pre-group with assign_to_group, then remove with delete_group |
| clear the entire canvas | canvas_clear({ canvasId }) |
| overall layout is wrong | rebuild using a corrected annotate_batch layout |
| structure or intent is wrong | redraw from scratch on a new canvas, or recreate intentionally with canvas_create({ ..., overwrite: true }) when replacing a known target |
| the real comparison axis changed mid-drawing | redraw on a new canvas instead of force-mutating the old one |
In long design canvases with 10+ sections, it is common to rewrite only one section.
Calling delete_element 20-30 times is slow and error-prone.
Use a logical group instead:
// 1. After the first draw, assign the returned elementIds to a logical group
assign_to_group({
canvasId,
groupId: "sec-11-after",
elementIds: [/* ids */]
})
// 2. Later, delete the whole section in one shot
delete_group({ canvasId, groupId: "sec-11-after" })
// 3. Redraw the new section and reassign the same groupId
const { elementIds } = await annotate_batch({ ... })
assign_to_group({ canvasId, groupId: "sec-11-after", elementIds })
list_groups({ canvasId })groupId a meaningful kebab-case name such as sec-11-before, sec-11-after, or merge-dialog-wireframecreate_frame({ memberIds }) auto-fits only at creation time. If you add new elements later, pull them in with update_frame_members({ frameId, add: [...] }) so the frameId stays stable for future export_png({ frameId })After each fix, go back to Step 3 and export again. Redrawing is normal whiteboard behavior, not failure.
library_catalog_list / user_library_list first?visual-vocabulary.md?annotate_batch?string[] when needed?export_png and inspect structure with canvas_inspect?update_element / move_elements / delete_element for local fixes?version_save({ canvasId, label }) before the risky edit and call version_restore({ canvasId, versionId }) (or version_restore with targetSlug to fork into a new canvas instead of reconciling in place)~/.whiteboard/ are outside git. If you need a PNG in a PR or other artifact, copy the exported file explicitlytext wraps automatically when width is provided. box_with_label auto-fits by default and keeps explicit string[] line breaks. Use autoFit: false only when you truly need rigid line controlannotate coordinates can occasionally be based on an older snapshot. In local single-user practice the timing window is short and usually harmlessnpx claudepluginhub kamiazya/whiteboard --plugin whiteboardCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.