CUBSTER Join the waitlist
DOCS / DIAGRAMS

Diagrams

An agent that draws an SVG has never once seen the picture it made — it wrote markup and moved on. Cubster's diagrams service closes that loop: draw a bare SVG fragment against a fixed class vocabulary, rasterize it to a PNG before anything is public, and only publish once the image actually looks right.

A published diagram is a versioned, workspace-scoped document — private by default, archived instead of deleted, and shareable by flipping its visibility from the dashboard. The vocabulary fixes the colors, typography, and grid so every diagram looks like it came from the same hand; an agent composes structure, never paint.

Install

The CLI, then the cubster-diagrams skill so an agent knows the vocabulary and the preview loop:

$ npm i -g @cubster/cli
$ cubster login
// in Claude Code:
/plugin marketplace add seancdavis/cubster
/plugin install cubster@cubster

Also needs an API key whose services include diagrams (see Authentication). A key created before this shipped did not gain it automatically — create a new one with the box checked.

The preview loop

This is the whole point of the service, and it's mandatory, not optional: an agent cannot see an SVG by reading its source, so it draws, rasterizes, looks at the PNG, and revises before anything is public.

  1. Write a cub-canvas SVG fragment to a file.
  2. cubster diagram preview it — writes a PNG next to the input, publishes nothing.
  3. Look at the PNG.
  4. Fix whatever the errors or the image show.
  5. cubster diagram create once it holds up.

A minimal, known-valid fragment — two nodes and an edge:

flow.svg
<svg class="cub-canvas" viewBox="0 0 640 320">
  <g class="cub-node" transform="translate(32,32)">
    <rect width="176" height="64" rx="8"/>
    <text class="cub-node-label" x="88" y="32">Upload</text>
  </g>
  <g class="cub-node" transform="translate(432,192)">
    <rect width="176" height="64" rx="8"/>
    <text class="cub-node-label" x="88" y="32">Transcode</text>
  </g>
  <path class="cub-edge" d="M208,64 H320 V224 H432" marker-end="url(#cub-arrow)"/>
  <text class="cub-edge-label" x="320" y="152">queued</text>
</svg>
$ cubster diagram preview ./flow.svg
Wrote flow.preview.png (1280×640)
// 2× the viewBox — the preview renders at retina scale
// look at flow.preview.png, then:
$ cubster diagram create --file ./flow.svg --title "Upload pipeline" --kind flow
Vocabulary

A diagram is a bare <svg class="cub-canvas" viewBox="0 0 W H"> fragment (W, H ≤ 4000) built only from a fixed set of cub-* classes. Cubster injects the colors, fonts, and arrowheads server-side — there is no fill, stroke, style, or font attribute in the vocabulary at all. Every class must be a known base class or a modifier valid for it; anything outside the element/attribute allowlist is rejected by name, never silently stripped. A label that won't fit its shape is rejected too — the validator's message names the exact pixel width to widen the shape to.

GET /api/v1/diagrams/vocabulary returns the full class, modifier, typography, and tone list as JSON — the same table the cubster-diagrams skill teaches and the validator enforces, so none of the three can drift from the others.

REST reference

Every route below accepts a session or an API key with the diagrams service — except visibility, which is session only. There is no DELETE anywhere in the service — archive/unarchive are the only reversible hide/restore.

POST /api/v1/diagrams Publish a new diagram (title, svg, kind?, tags?) → 201 Diagram
GET /api/v1/diagrams List diagrams (kind, tag, q, includeArchived) → { diagrams: DiagramSummary[] }
GET /api/v1/diagrams/:slug Fetch a diagram — ?version=N for an older one → Diagram
PUT /api/v1/diagrams/:slug Publish a new version → Diagram
POST /api/v1/diagrams/:slug/archive Reversibly hide a diagram → Diagram
POST /api/v1/diagrams/:slug/unarchive Restore an archived diagram → Diagram
POST /api/v1/diagrams/preview Validate + rasterize without publishing → { valid, errors, warnings, png, width, height }
POST /api/v1/diagrams/layout Dagre coordinates for a node/edge graph → { canvas, nodes, edges }
GET /api/v1/diagrams/vocabulary The class/modifier/typography/tone vocabulary as JSON
POST /api/v1/diagrams/:slug/visibility Session only — change visibility. 403 for an API key.

Every route takes Authorization: Bearer $CUBSTER_API_KEY. The preview route is the one to reach for first — it validates and rasterizes without storing anything:

$ curl -X POST https://app.cubster.dev/api/v1/diagrams/preview \
  -H "Authorization: Bearer $CUBSTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"svg": "<svg class=\"cub-canvas\" viewBox=\"0 0 240 96\"><g class=\"cub-node\"><rect width=\"176\" height=\"64\" rx=\"8\"/><text class=\"cub-node-label\" x=\"88\" y=\"32\">Ingest</text></g></svg>"}'
200 response
{
  "valid": true, "errors": [], "warnings": [],
  "png": "iVBORw0KGgoAAAANSUhEUg…",
  "width": 480, "height": 192
}

png is base64 — decode it to a file and look at it (base64 -d > preview.png), or let cubster diagram preview write it for you. It's null when valid is false, and the dimensions are 2× the viewBox.

A create/update body carrying a visibility key at all — even set to the value it already has — is 400 "error": "visibility is not accepted here; change it in the dashboard". Nothing an API key sends can ever change a diagram's visibility.

Diagram JSON
{
  "id": "uuid", "slug": "aB3xQ9zK7mN2", "title": "Upload pipeline",
  "kind": "flow", "tags": ["auth"], "visibility": "private",
  "archived": false, "latestVersion": 2,
  "version": { "number": 2, "svg": "<svg class="cub-canvas" ...>...</svg>",
    "width": 640, "height": 320, "createdAt": "ISO" },
  "url": "https://app.cubster.dev/d/aB3xQ9zK7mN2",
  "svgUrl": "https://app.cubster.dev/d/aB3xQ9zK7mN2.svg",
  "pngUrl": "https://app.cubster.dev/d/aB3xQ9zK7mN2.png",
  "createdAt": "ISO", "updatedAt": "ISO"
}

DiagramSummary (the list endpoint) is the same shape minus version. The three URLs are always present, even while private — they 404 until visibility changes.

Serving & visibility

GET /d/:slug (an HTML page), .svg, and .png all take an optional ?v=N for an older version. A private diagram 404s with the exact same body as an unknown slug — its existence is never confirmed to anyone but its owner.

Visibility Owner's session Anyone else
private (default) 200 — owner only 404
unlisted 200 200
public 200 200
archived (any visibility) 200 — owner only 404

An owner-only response (private, or archived) is never cached (Cache-Control: private, no-store). An unlisted or public one caches for 5 minutes (public, max-age=300, never immutable) so flipping a diagram back to private takes effect within minutes, not forever. The .svg response embeds the brand fonts as base64 so it renders correctly hotlinked anywhere — a GitHub README included.

SDK

client.diagrams.* mirrors the REST surface one-for-one. preview() has no publishing side effect, so call it first every time.

const check = await cubster.diagrams.preview({ svg: mySvgFragment })
// { valid, errors, warnings, png, width, height } — writes nothing
const diagram = await cubster.diagrams.create({
  title: 'Upload pipeline', svg: mySvgFragment, kind: 'flow',
})
// diagram.visibility === 'private'; diagram.url / .svgUrl / .pngUrl 404 until a human shares it
const diagrams = await cubster.diagrams.list({ kind: 'flow' })
await cubster.diagrams.update(diagram.slug, { svg: revisedSvgFragment })
await cubster.diagrams.archive(diagram.slug)

There's no visibility parameter anywhere on the client — not on create, not on update, not as its own method. A diagram always starts private; sharing it is a dashboard action.

CLI

cubster diagram preview|create|update|get|ls|archive|unarchive, all over client.diagrams.*. A validation failure from preview or a server-side rejection on create/update prints each problem as its own line on stderr and exits 1 — nothing is written or published.

$ cubster diagram preview ./flow.svg -o /tmp/flow.png
$ cubster diagram create --file ./flow.svg --title "Upload pipeline" --kind flow --tag auth
$ cubster diagram update abc123abc123 --file ./flow-v2.svg
$ cubster diagram get abc123abc123 -o ./flow.svg
$ cubster diagram ls --kind flow
$ cubster diagram archive abc123abc123

No cubster diagram delete and no --visibility flag anywhere — same two rules as the REST API, enforced again at this layer.

What you can't do — and why
  • No colors, fonts, or paint attributes — the vocabulary is what makes every diagram look like it came from the same hand, and it's also what keeps the published PNG pixel-identical to what a browser renders from the same SVG. Letting an agent set its own fill or font-family would break both guarantees at once.
  • No delete — only reversible archive/unarchive. A diagram hotlinked into a README or a doc must never 404 because an agent tidied up its list.
  • No visibility from the API, SDK, or CLI — publishing something to the open web is a decision for a human, not an agent. The one route that changes it requires a signed-in session and 403s an API key.
  • Layout labels cap at 80 characters — a node or edge label longer than that cannot fit any node the 8px grid can produce, so POST /api/v1/diagrams/layout returns a 400 naming the limit rather than laying out a box the text spills out of. Shorten it, or move the detail to a cub-annotation.