Map syntax (architecture as code)

Map syntax

Every Canopy map is plain JSON - easy to read, diff, and edit by hand or with an agent. This page is the complete syntax / schema reference: every field, its type, and its accepted values. (Canopy calls this "architecture as code".) Open any map and switch the editor to Split or Code to see it; edit the canvas or edit the JSON and they stay in sync.

Plain JSON means your maps are portable - version them in git, generate them from a script, or read and write them through the API and MCP server.

The Code editor has autocomplete (IntelliSense). As you type, it suggests field names, icon slugs (type a technology - stripe, next - and pick the brand mark), billing models, edge line types, and the node ids an edge can point to. Press Ctrl Space to re-open suggestions, to move, Enter or Tab to accept.

Shape

A map is an object with two arrays, nodes and edges (an optional name is also stored):

JSON
{
  "name": "My Stack",
  "nodes": [
    { "id": "web", "label": "Web App", "brand": "nextdotjs", "spend": "$1.2k" },
    { "id": "api", "label": "API", "brand": "nodedotjs", "spend": "$2.1k" }
  ],
  "edges": [{ "source": "web", "target": "api" }]
}

Only id is required on a node; source and target are required on an edge. Every other field is optional.

Canopy also stores an optional canvas object with your per-map view settings (background, edgeStyle, minimap, snapToGrid). It's written automatically when you save and restored on load - you don't need to author it by hand, and older files without it just fall back to the defaults.

Node fields

FieldTypeRequiredAccepted valuesNotes
idstringyesLetters, digits, -, _ only - /^[A-Za-z0-9_-]+$/. Unique within the map.The node's stable key; edges reference it.
labelstringAny text (kept ≤ 80 chars).Display name on the canvas. Defaults to id when omitted.
techstringAny text (kept ≤ 60 chars).Sub-label under the name, e.g. "node · trpc". Also accepted as sub.
brandstringAn icon slug from the Icon library, e.g. "postgresql", "openai".Brand icon. Also accepted as icon. An unknown slug → a neutral box node.
purposestringAny text (kept ≤ 160 chars).What the service does; shown in the inspector and in CLAUDE.md/AGENTS.md exports.
spendstringA currency string, e.g. "$1.2k", "€840", "$0".Monthly infra cost shown on the node. Defaults to "$0".
billingstring[]Any of "seat", "team", "usage", "flat". Unknown values are dropped.Billing-model tags shown on the node and rolled up on the Spend page.
x, ynumberAny number (pixels).Canvas position. Auto-assigned when omitted - see Auto-layout.

Not a node field

There is no per-node color. The map has a single accent colour set from the editor; individual nodes take their hue from their brand icon.

Edge fields

FieldTypeRequiredAccepted valuesNotes
sourcestringyesAn existing node id.The edge's start. (This is source, not from.)
targetstringyesAn existing node id.The edge's end. (This is target, not to.)
linestringOne of "bezier", "smoothstep", "step", "straight".Edge curve style. Defaults to "bezier"; an invalid value falls back to "bezier".
idstringAny string.Optional stable edge id; auto-generated when omitted.

Both ends must reference a node id that exists in the same nodes array - edges with a missing or unknown source/target are silently dropped.

Auto-layout

When x/y are omitted on any node, Canopy assigns positions automatically using a left-to-right layered layout: each depth level from the edge graph gets its own column (spaced 240 px apart), and nodes in the same column are evenly distributed vertically (96 px row height). Once you drag a node, its position is written back to the JSON and no longer auto-managed.

Minimal map

Only id (nodes) and source/target (edges) are required:

JSON
{
  "nodes": [{ "id": "web" }, { "id": "api" }, { "id": "db" }],
  "edges": [
    { "source": "web", "target": "api" },
    { "source": "api", "target": "db" }
  ]
}

Full example

JSON
{
  "name": "AI SaaS",
  "nodes": [
    {
      "id": "web",
      "label": "Web App",
      "tech": "next.js",
      "brand": "nextdotjs",
      "purpose": "Frontend application",
      "spend": "$1.2k",
      "billing": ["flat"],
      "x": 0,
      "y": 96
    },
    {
      "id": "api",
      "label": "API",
      "tech": "node · trpc",
      "brand": "nodedotjs",
      "purpose": "Backend REST API",
      "spend": "$2.1k",
      "billing": ["flat"],
      "x": 240,
      "y": 144
    },
    {
      "id": "db",
      "label": "Postgres",
      "brand": "postgresql",
      "purpose": "Primary database",
      "spend": "$840",
      "billing": ["usage", "team"],
      "x": 480,
      "y": 48
    },
    {
      "id": "llm",
      "label": "LLM",
      "brand": "openai",
      "purpose": "AI completions",
      "spend": "$2.8k",
      "billing": ["usage"],
      "x": 240,
      "y": 240
    }
  ],
  "edges": [
    { "source": "web", "target": "api" },
    { "source": "api", "target": "db", "line": "step" },
    { "source": "api", "target": "llm" }
  ]
}

Editing

  • Add node opens the icon library - search every brand, or pick a generic service box.
  • The inspector edits a node's label, tech sub-label, icon (brand), monthly spend, purpose, and billing tags.
  • Drag between handles to connect; drag a node to move it - its x / y are written back to the JSON.
  • A map colour swatch sets the map's accent, reflected on the dashboard overview.
  • If the JSON is invalid, the offending line is flagged and the canvas keeps your last valid render until you fix it.

Importing

Already have a map written to this syntax? You can paste it straight in via New map → Paste canopy.json - see Import from canopy.json for the full flow and what gets validated.

Next

  • Icon library - every brand slug, searchable.
  • Nodes - the service inspector and metadata.
  • Edges - connections in depth.
  • Exports - JSON, Markdown, CLAUDE.md and more.

to paste into any AI.

Ask ChatGPT about this page.

Ask Claude about this page.

Need help? Sign in to chat with support

Have ideas?

General inquiry? Email support.