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.
The CLI, then the cubster-diagrams skill so an agent knows the vocabulary and the preview loop:
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.
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.
- Write a
cub-canvasSVG fragment to a file. cubster diagram previewit — writes a PNG next to the input, publishes nothing.- Look at the PNG.
- Fix whatever the errors or the image show.
cubster diagram createonce it holds up.
A minimal, known-valid fragment — two nodes and an edge:
<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>
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.
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:
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.
{
"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.
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.
client.diagrams.* mirrors the REST surface one-for-one. preview() has no publishing side effect, so call it first every time.
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.
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.
No cubster diagram delete and no --visibility flag anywhere — same two rules as the REST API, enforced again at this layer.
- 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
fillorfont-familywould 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/layoutreturns a400naming the limit rather than laying out a box the text spills out of. Shorten it, or move the detail to acub-annotation.