
Canvas
- 329 installs
- 10.4k repo stars
- Updated August 1, 2026
- agricidaniel/claude-obsidian
canvas is a claude-obsidian agent skill that creates and edits Obsidian JSON Canvas files with auto-positioned image, text, PDF, and wiki-note nodes inside zoned visual boards.
About
canvas is the visual knowledge layer skill in agricidaniel/claude-obsidian, complementing /save text synthesis and /autoresearch structured notes. It reads and writes wiki/canvases/*.canvas JSON aligned with the JSON Canvas open standard at jsoncanvas.org, supporting operations like /canvas new, add image, add text, add pdf, add note, zone, list, and from banana for recent image capture. An auto-positioning algorithm places nodes inside labeled group zones without ID collisions, storing images under _attachments/images/canvas/. The default board is wiki/canvases/main.canvas with six supported group colors. Developers reach for canvas when building visual roadmaps, architecture maps, or research graphs inside an Obsidian vault rather than prose-only wiki pages.
- Obsidian Canvas node creation
- Linked note and group layout
- Visual system and roadmap maps
- Vault-native markdown integration
- Iterative board updates
Canvas by the numbers
- 329 all-time installs (skills.sh)
- Ranked #846 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/agricidaniel/claude-obsidian --skill canvasAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 329 |
|---|---|
| repo stars | ★ 10.4k |
| Last updated | August 1, 2026 |
| Repository | agricidaniel/claude-obsidian ↗ |
How do you build Obsidian canvas boards programmatically?
Create and extend Obsidian Canvas boards—nodes, links, groups, and layouts—for visual roadmaps, system maps, or research graphs inside your vault.
Who is it for?
Developers maintaining Obsidian wiki vaults who want agent-driven JSON Canvas creation with zone layout and banana image integration.
Skip if: Non-Obsidian documentation workflows, standalone slide decks, or teams that only need linear markdown notes without visual boards.
When should I use this skill?
A user triggers /canvas, asks to add images or notes to an Obsidian canvas, or wants a visual research or system map inside wiki/canvases/.
What you get
Updated wiki/canvases/*.canvas JSON files, zoned node layouts, _attachments/images/canvas/ assets, and wiki/overview.md canvas index entries.
- wiki/canvases/*.canvas board files
- _attachments/images/canvas/ media assets
- Zone-positioned node layouts
By the numbers
- Supports 6 group node color values from 1 through 6
- Default canvas path is wiki/canvases/main.canvas
- References canvas-spec.md aligned with jsoncanvas.org
Files
canvas: Visual Reference Layer
The three knowledge capture layers:
/save→ text synthesis (wiki/questions/, wiki/concepts/)/autoresearch→ structured knowledge (wiki/sources/, wiki/concepts/)/canvas→ visual references (wiki/canvases/)
A canvas is a JSON file Obsidian renders as an infinite visual board. This skill reads and writes canvas JSON directly. Read references/canvas-spec.md for the full format reference before making any edits. This spec aligns with the JSON Canvas open standard.
Substrate preference (v1.7+): This skill is a self-contained fallback. Prefer `kepano/obsidian-skills` as the authoritative substrate — its json-canvas skill is the canonical spec reference. If you see a json-canvas skill available without the claude-obsidian: namespace, that is kepano's version: use it for spec questions. Continue to use this canvas skill for the wiki-scoped workflows (positioning into wiki/canvases/, /banana integration, zone layout) — those are unique to claude-obsidian and live above kepano's primitive. Install kepano: claude plugin marketplace add kepano/obsidian-skills.
---
Default Canvas
wiki/canvases/main.canvas
If it does not exist, create it:
{
"nodes": [
{
"id": "title",
"type": "text",
"text": "# Visual Reference\n\nDrop images, PDFs, and notes here.",
"x": -400, "y": -300, "width": 400, "height": 120, "color": "6"
},
{
"id": "zone-default",
"type": "group",
"label": "General",
"x": -400, "y": -140, "width": 800, "height": 400, "color": "4"
}
],
"edges": []
}---
Operations
open / status (/canvas with no args)
1. Check if wiki/canvases/main.canvas exists. 2. If yes: read it, count nodes by type, list all group node labels (zone names). Report: "Canvas has N nodes: X images, Y text cards, Z wiki pages. Zones: [list]" 3. If no: create it with the starter structure above. Report: "Created main.canvas with a General zone." 4. Tell user: "Open wiki/canvases/main.canvas in Obsidian to view."
---
new (/canvas new [name])
1. Slugify the name: lowercase, spaces → hyphens, strip special chars. 2. Create wiki/canvases/[slug].canvas with the starter structure, title updated to # [Name]. 3. Add entry to wiki/overview.md under a "## Canvases" subsection (append after the Current State section). Do not modify wiki/index.md. It uses a fixed section schema (Domains, Entities, Concepts, Sources, Questions, Comparisons). 4. Report: "Created wiki/canvases/[slug].canvas"
---
add image (/canvas add image [path or url])
Resolve the image:
- If URL (starts with
http): download withcurl -sL [url] -o _attachments/images/canvas/[filename]
Derive filename from URL path, or use img-[timestamp].jpg if unclear.
- If local path outside vault:
cp [path] _attachments/images/canvas/ - If already vault-relative: use as-is.
Create _attachments/images/canvas/ if it doesn't exist.
Detect aspect ratio: Use python3 -c "from PIL import Image; img=Image.open('[path]'); print(img.width, img.height)" or identify -format '%w %h' [path]. See references/canvas-spec.md for the full aspect ratio → canvas size table (7 ratios including 4:3, 3:4, ultra-wide). Do not use an inline table here. The spec is the single source of truth for sizing.
Position using auto-layout (see Auto-Positioning section below).
Append node to canvas JSON and write.
Report: "Added [filename] to [zone] zone at position ([x], [y])."
---
add text (/canvas add text [content])
Create a text node:
{
"id": "text-[timestamp]",
"type": "text",
"text": "[content]",
"x": [auto], "y": [auto],
"width": 300, "height": 120,
"color": "4"
}Position using auto-layout. Write and report.
---
add pdf (/canvas add pdf [path])
Same as add image. Obsidian renders PDFs natively as file nodes.
- Copy to
_attachments/pdfs/canvas/if outside vault. - Fixed size: width=400, height=520.
- Report page count if you can determine it.
---
add note (/canvas add note [wiki-page])
1. Search wiki/ for a file matching the page name (case-insensitive, partial match ok). 2. Use the vault-relative path as the file field.
- Use
"type": "file"(not"type": "link"):.mdfiles use file nodes, not link nodes. "type": "link"takes aurl: "https://...": it is for web URLs only.
3. Create a file node: width=300, height=100. 4. Position using auto-layout.
{
"id": "note-[timestamp]",
"type": "file",
"file": "wiki/concepts/LLM Wiki Pattern.md",
"x": [auto], "y": [auto],
"width": 300, "height": 100
}---
zone (/canvas zone [name] [color])
1. Read canvas JSON. 2. Find max_y: max(node.y + node.height for all nodes) + 60. Use 280 if no nodes (leaves room above the starter title node). 3. Create a group node:
{
"id": "zone-[slug]",
"type": "group",
"label": "[name]",
"x": -400,
"y": [max_y],
"width": 1000,
"height": 400,
"color": "[color or '3']"
}Valid colors: "1"=red "2"=orange "3"=yellow "4"=green "5"=cyan "6"=purple
Write and report.
---
list (/canvas list)
1. glob wiki/canvases/*.canvas 2. For each canvas: read JSON, count nodes by type. 3. Report:
wiki/canvases/main.canvas . 14 nodes (8 images, 3 text, 2 file, 1 group)
wiki/canvases/design-ideas.canvas. 42 nodes (30 images, 4 text, 8 groups)---
from banana (/canvas from banana) (if the banana-claude plugin is installed)
1. Check wiki/canvases/.recent-images.txt first (session log of newly written images). 2. If not found or empty: use find with correct precedence (parentheses required. Without them -newer only binds to the last -name clause):
python3 -c "import time,os; open('/tmp/ten-min-ago','w').close(); os.utime('/tmp/ten-min-ago',(time.time()-600,time.time()-600))"
find _attachments/images -newer /tmp/ten-min-ago \( -name "*.png" -o -name "*.jpg" \)Note: /banana is an optional external skill not shipped in this plugin. If the user has it installed, the .recent-images.txt log will be populated. If not, the find command above is the fallback. 3. If still none: show the 5 most recently modified images. 4. Present list: "Found N recent images: [list]. Add to canvas? Which zone? (zone name / 'new [name]' / 'skip')" 5. On confirmation: add each using the add image logic.
---
Auto-Positioning Algorithm
Read references/canvas-spec.md for the full coordinate system.
def next_position(canvas_nodes, target_zone_label, new_w, new_h):
# Find zone group node
zone = next((n for n in canvas_nodes
if n.get('type') == 'group'
and n.get('label') == target_zone_label), None)
if zone is None:
# No zone: place below all content
max_y = max((n['y'] + n.get('height', 0) for n in canvas_nodes), default=-140)
return -400, max_y + 60
zx, zy = zone['x'], zone['y']
zw, zh = zone['width'], zone['height']
# Nodes inside this zone
inside = [n for n in canvas_nodes
if n.get('type') != 'group'
and zx <= n['x'] < zx + zw
and zy <= n['y'] < zy + zh]
if not inside:
return zx + 20, zy + 20
rightmost_x = max(n['x'] + n.get('width', 0) for n in inside)
next_x = rightmost_x + 40
if next_x + new_w > zx + zw:
# New row
max_row_y = max(n['y'] + n.get('height', 0) for n in inside)
return zx + 20, max_row_y + 20
# Same row: align to the top of all existing nodes in the zone
current_row_y = min(n['y'] for n in inside)
return next_x, current_row_y---
ID Generation
Read the canvas, collect all existing IDs. Never reuse one.
Safe ID pattern: [type]-[content-slug]-[full-unix-timestamp]
Use the full Unix timestamp (10 digits) to avoid collisions in batch operations.
Examples: img-cover-1744032823, text-note-1744032845, zone-branding-1744032901
If a collision is detected (ID already exists in the canvas), append -2, -3, etc.
---
Session Log (optional hook)
If wiki/canvases/.recent-images.txt exists, append any new image path written to _attachments/images/ during this session (one path per line, keep last 20).
/canvas from banana reads this file first, making it instant without filesystem search.
---
Banana Integration (if the banana-claude plugin is installed)
After any /banana run in the same session, if the user says "add to canvas" or "put on canvas", treat it as /canvas from banana.
When /banana finishes generating images, suggest:
"Add generated images to canvas? Run /canvas from banana"---
Summary
1. Read canvas-spec.md before editing any canvas JSON. 2. Always read the canvas file before writing. Parse existing nodes to avoid ID collisions and calculate auto-positions. 3. Create _attachments/images/canvas/ for downloaded/copied images. 4. Update wiki/index.md when creating new canvases. 5. Report position and zone after every add operation.
See Also
For standalone visual production (12 templates, 6 layout algorithms, AI generation, presentations), see claude-canvas. This skill handles wiki-scoped visual boards. claude-canvas handles full-featured canvas orchestration for any project.
---
How to think (10-principle mapping)
When working on this skill, apply the 10-principle loop. See `skills/think/SKILL.md` for the canonical framework.
| # | Principle | Application here |
|---|---|---|
| 1 | OBSERVE (ext) | Which images, PDFs, notes belong on this canvas? Read each before adding. |
| 2 | OBSERVE (int) | Am I aestheticizing or actually communicating? Pretty canvases that don't inform are noise. |
| 3 | LISTEN | The user's mental model of how these items relate. The canvas should mirror that, not impose another. |
| 4 | THINK | Layout, group hierarchy, edge structure. Spatial reasoning matters; arbitrary positions confuse. |
| 5 | CONNECT (lat) | Edges between canvas nodes reveal hidden structure not visible in the linear wiki. |
| 6 | CONNECT (sys) | JSON Canvas 1.0 spec + Obsidian-native rendering + banana skill for AI image gen. |
| 7 | FEEL | A canvas should be readable at first glance, not a maze of arrows. |
| 8 | ACCEPT | Not every project needs a canvas. Admit when prose is enough. |
| 9 | CREATE | Write the .canvas JSON with stable IDs and sensible positions. |
| 10 | GROW | Which canvases get reopened? Which are abandoned? That signal informs canvas-worthiness over time. |
Obsidian Canvas JSON Specification
Canvas files are JSON with two top-level keys: nodes (array) and edges (array). Obsidian reads and writes them as UTF-8 JSON files with .canvas extension.
This reference aligns with the JSON Canvas 1.0 open specification. All structures support arbitrary additional fields ([key: string]: any) for forward compatibility. Obsidian will preserve unknown fields when reading and writing canvas files.
ID format: The JSON Canvas 1.0 spec recommends 16-character lowercase hexadecimal IDs (e.g., "a1b2c3d4e5f67890"). Obsidian itself generates IDs in this format. The descriptive ID examples in this reference ("text-title-4821", "img-cover-7823") are an alternative naming convention that this plugin uses for human readability. Both are valid JSON Canvas. Use whichever fits your workflow.
---
Coordinate System
x increases →
┌─────────────────────────────────
│ (-920, -2400) (0, -2400)
│
y │ (-920, 0) (0, 0) ← origin
↓ │
│ (-920, 540) (500, 540)- Origin (0, 0) is the center of the canvas viewport.
- x increases rightward. Negative x = left of center.
- y increases downward. Negative y = above center.
- Node
xandyare the top-left corner of the node, not the center. - Obsidian pans to fit all nodes on first open.
---
Node Types
Text node
Renders markdown content as a styled card.
{
"id": "text-title-4821",
"type": "text",
"text": "# Heading\n\nParagraph with **bold** and `code`.",
"x": -400,
"y": -300,
"width": 400,
"height": 120,
"color": "6"
}text: markdown string. Use\nfor newlines.- Minimum readable size: width ≥ 200, height ≥ 60.
coloris optional. Omit for default (no color).
---
File node
Renders an image, PDF, markdown note, or other vault file inline.
{
"id": "img-cover-7823",
"type": "file",
"file": "_attachments/images/example.png",
"x": -900,
"y": -100,
"width": 420,
"height": 236
}file: vault-relative path (not absolute, not~/).- Supported:
.png.jpg.webp.gif.pdf.md.canvas - For
.mdfiles: renders as a preview card. - For
.pdffiles: renders the first page as preview. - No
colorfield for file nodes: color is ignored.
---
Group node (Zone)
A labeled rectangular region. Does not clip or contain nodes. It's a visual guide. Nodes placed "inside" a group are just positioned within its bounding box.
{
"id": "zone-branding-3391",
"type": "group",
"label": "Brand Identity",
"x": -920,
"y": -880,
"width": 1060,
"height": 290,
"color": "6",
"background": "_attachments/images/grid-bg.png",
"backgroundStyle": "cover"
}label: shown at the top of the group box.color: colors the group border and label.background(optional): vault-relative path to a background image for the group.backgroundStyle(optional): how the background is rendered."cover": fills the group, cropping if needed (default-ish behavior)"ratio": preserves aspect ratio, fits inside the group"repeat": tiles the image- Groups do not affect auto-layout: they are purely visual containers.
---
Link node
Renders a web URL as an embedded preview card.
{
"id": "link-karpathy-2233",
"type": "link",
"url": "https://github.com/karpathy",
"x": 200,
"y": -300,
"width": 400,
"height": 120
}url: must be a validhttps://URL.- Obsidian fetches the Open Graph preview (title, description, thumbnail).
---
Edges
Connections between nodes. Usually empty for mood boards.
{
"id": "e-hub-cidx",
"fromNode": "hub",
"fromSide": "right",
"fromEnd": "none",
"toNode": "c-idx",
"toSide": "left",
"toEnd": "arrow",
"label": "concepts",
"color": "5"
}Required fields: id, fromNode, toNode. Everything else is optional.
fromNode/toNode: IDs of the source and target nodes.fromSide/toSide(optional):"top""bottom""left""right". If omitted, Obsidian auto-calculates the best side based on relative node positions.fromEnd(optional): end-cap on the source side. Defaults to"none". Values:"none"|"arrow".toEnd(optional): end-cap on the target side. Defaults to `"arrow"`: note the asymmetric default vsfromEnd. Values:"none"|"arrow".label(optional): text shown on the edge.color(optional): same color palette as nodes ("1"–"6"or hex).
Most edges represent directed relationships, so the asymmetric defaults (fromEnd: "none", toEnd: "arrow") produce a single arrow pointing from source to target without specifying anything explicitly.
---
Color Reference
| Code | Color | Hex (approx) | Use case |
|---|---|---|---|
"1" | Red / Tomato | #e03e3e | Warnings, archive |
"2" | Orange | #d09035 | Active work |
"3" | Yellow / Gold | #d0a023 | WIP, notes |
"4" | Green / Teal | #448361 | Content, sources |
"5" | Blue / Cyan | #3ea7d3 | Navigation, info |
"6" | Purple / Violet | #9063d2 | Title, identity |
Omit color entirely for the default (no border color, transparent label).
---
Image Sizing Guidelines
Calculate from actual image dimensions using PIL or identify:
python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)"
# or
identify -format '%w %h' path.png| Aspect ratio | Condition | Canvas width | Canvas height |
|---|---|---|---|
| 16:9 (wide) | ratio 1.6–2.0 | 420 | 236 |
| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 |
| 4:3 | ratio 1.2–1.6 | 380 | 285 |
| 1:1 (square) | ratio 0.9–1.1 | 280 | 280 |
| 3:4 | ratio 0.6–0.9 | 240 | 320 |
| 9:16 (portrait) | ratio < 0.6 | 200 | 356 |
| any | 400 | 520 | |
| Unknown | fallback | 320 | 240 |
---
Auto-Positioning Pseudocode
function place_node(canvas, zone_label, new_w, new_h):
zone = find group node where label == zone_label
padding = 20
if zone not found:
max_y = max(n.y + n.height for n in canvas.nodes) + 60
return (-400, max_y)
# Nodes visually inside zone
inside = [n for n in canvas.nodes
if n.type != 'group'
and zone.x <= n.x < zone.x + zone.width
and zone.y <= n.y < zone.y + zone.height]
if inside is empty:
return (zone.x + padding, zone.y + padding)
# Rightmost point in zone
rightmost = max(n.x + n.width for n in inside)
next_x = rightmost + 40
if next_x + new_w > zone.x + zone.width - padding:
# Overflow → new row
bottom_of_row = max(n.y + n.height for n in inside)
return (zone.x + padding, bottom_of_row + padding)
# Same row
row_y = min(n.y for n in inside) # align to top of existing row
return (next_x, row_y)---
Full Example: Two-Zone Canvas
{
"nodes": [
{
"id": "title-0001",
"type": "text",
"text": "# Brand Reference\n\n**AI Marketing Hub** visual assets",
"x": -920, "y": -2440, "width": 560, "height": 180, "color": "6"
},
{
"id": "zone-logos",
"type": "group",
"label": "Logos & Icons",
"x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6"
},
{
"id": "img-logo-pro",
"type": "file",
"file": "_attachments/images/example.png",
"x": -900, "y": -2180, "width": 420, "height": 236
},
{
"id": "img-icon-free",
"type": "file",
"file": "_attachments/images/example-icon.png",
"x": -440, "y": -2180, "width": 280, "height": 280
},
{
"id": "zone-covers",
"type": "group",
"label": "Skill Covers",
"x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3"
},
{
"id": "img-seo",
"type": "file",
"file": "_attachments/images/example-cover.png",
"x": -900, "y": -1800, "width": 420, "height": 236
}
],
"edges": []
}---
Common Mistakes
- Wrong path format: use
_attachments/images/file.pngnot/home/user/...or~/... - ID collision: always read existing IDs before generating a new one
- Negative y confusion:
y: -2400is ABOVEy: -1000(more negative = higher up) - Group does not clip: placing a node "inside" a group is just positioning it within the group's bounding box: there is no parent-child relationship in the JSON
- Missing height on text nodes: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24.
Related skills
How it compares
Use canvas for wiki-scoped Obsidian board workflows; prefer claude-canvas when you need standalone visual production with 12 templates and presentation layouts.
FAQ
What file format does canvas write?
canvas writes Obsidian-compatible JSON Canvas .canvas files under wiki/canvases/, following the JSON Canvas open standard documented at jsoncanvas.org. Nodes include text, image, file, link, and group types with x/y positioning.
Where does canvas store downloaded images?
canvas downloads or copies images into _attachments/images/canvas/ inside the vault, detects aspect ratio for sizing, and appends nodes to the target canvas JSON with zone-aware auto-positioning reported back to the user.
What is the default canvas in canvas?
canvas defaults to wiki/canvases/main.canvas, creating a starter General zone with a title text node if the file does not exist, then reports node counts by type and available zone labels.