CUBSTER Join the waitlist
DOCS / ARTIFACTS

Artifacts

An artifact is a JSON document built from a fixed registry of 15 typed components — never HTML, CSS, or a script. An agent composes the structure; Cubster renders it as a real interactive page (sortable tables, tabs, collapsible detail, charts with hover) and prints it as a complete document with everything expanded.

There is no escape hatch — no html block, no raw markup, no scripted iframe anywhere in the registry. The bet is that the component set makes the skimmable structure cheaper to write than prose, so when a document doesn't fit, hitting the wall is a signal to fix the composition or grow the library — never a reason to reach around it.

Install

The CLI, then the cubster-artifacts skill so an agent knows the component catalog and the validate-then-create 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 artifacts (see Authentication). A key created before this shipped did not gain it automatically — create a new one with the box checked.

The loop

Validating before publishing is the whole point of the service — a rejected document leaves no row behind, and updating means sending the whole document again, never a patch.

  1. Compose { title, blocks: [{ type, props }, ...] } from the components below.
  2. cubster artifact validate it — writes nothing on the server, valid or not.
  3. Fix whatever errors names — each carries a path to the exact field.
  4. cubster artifact create once it validates — always starts private.
  5. Hand out the url. To revise, cubster artifact update <slug> with the full document again.
$ cubster artifact validate ./report.json
Valid: Where the nightly export should run (3 blocks)
$ cubster artifact create --file ./report.json --tag ops
Components

15 component types, in the order every surface presents them: orientation first, then content, then data, then the interactive container. Every one exports its own JSON Schema and an example; cubster artifact components (or GET /api/v1/artifacts/components) prints the full catalog with required/defaulted props for composing a document — the catalog below is names and intent, not the prop reference.

summary The lede: a one-sentence verdict plus 2–5 supporting points, rendered as a distinct card.
key-points A scannable list of short statements, each optionally toned positive, negative, or warning.
detail A titled section that is collapsed on the web and expanded in print. Holds any blocks except another `detail` or a `tabs`.
callout A single boxed message in one of four tones — info, warning, success, or danger — with an optional title.
prose A block of Markdown, rendered sanitized — no raw HTML, no scripts, links restricted to http(s) and mailto.
code A syntax-highlighted code block with an optional filename and highlighted lines. Copyable on the web, ink-on-paper in print.
image A single image with required alt text and an optional caption. Any http(s) URL, including a Cubster asset URL.
diagram Embeds a published Cubster diagram by slug, resolved and rendered inline.
divider A horizontal rule (`line`) or a run of blank space (`space`) between sections.
stat-row A row of 1–6 headline numbers, each with a label, a value, and an optional change indicator.
data-table A table of keyed rows with typed columns — sortable, and optionally searchable and filterable on the web. Prints whole and unfiltered.
comparison A criteria-by-options grid with an optional winner marked per row — the 'which should I pick' component.
chart A bar, line, area, pie, or heatmap chart over one to six series (heatmap: rows × columns of values), drawn on the validated Cubster palette with a 'Show data' table beside it.
chart-embed Embeds a published Cubster chart by slug, resolved and rendered inline.
tabs Two to five labelled panels over the same subject. Prints as stacked sections, each label becoming a heading.

detail and tabs are the two container types — they hold any of the other 13 ("leaf") blocks, never each other and never themselves. The cubster-artifacts skill teaches the full composition rules (when each block earns its place, the "15-second test", the two-screen budget) and ships four complete example documents.

Full reference — props, guidance, and a worked example for every component: /docs/artifacts/components/.

REST reference

Every route below accepts a session or an API key with the artifacts 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/artifacts Publish a new artifact (title, blocks, tags?) → 201 Artifact
GET /api/v1/artifacts List artifacts (tag, q, includeArchived) → { artifacts: ArtifactSummary[] }
GET /api/v1/artifacts/:slug Fetch an artifact — ?version=N for an older one → Artifact
PUT /api/v1/artifacts/:slug Publish a new version → Artifact
POST /api/v1/artifacts/:slug/archive Reversibly hide an artifact → Artifact
POST /api/v1/artifacts/:slug/unarchive Restore an archived artifact → Artifact
POST /api/v1/artifacts/validate Validate a document without publishing → { valid, errors }
GET /api/v1/artifacts/components The 15-component catalog → { components }
POST /api/v1/artifacts/:slug/visibility Session only — change visibility. 403 for an API key.

Every route takes Authorization: Bearer $CUBSTER_API_KEY. validate is the one to reach for first — it always responds 200 and writes nothing:

$ curl -X POST https://app.cubster.dev/api/v1/artifacts/validate \
  -H "Authorization: Bearer $CUBSTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d @report.json
200 response
{ "valid": true, "errors": [] }

An invalid document reports { "valid": false, "errors": [{ "path", "message" }] } — still 200, because "here is what to fix" is the answer either way. path addresses the exact field, e.g. blocks[3].props.stats[0].value. Publish once it's clean:

$ curl -X POST https://app.cubster.dev/api/v1/artifacts \
  -H "Authorization: Bearer $CUBSTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d @report.json

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 an artifact's visibility.

Artifact JSON
{
  "id": "uuid", "slug": "aB3xQ9zK7mN2", "title": "Where the nightly export should run",
  "summary": "Move the nightly export to a scheduled background function.",
  "tags": ["ops"], "visibility": "private", "archived": false, "latestVersion": 1,
  "version": { "number": 1,
    "doc": { "title": "...", "blocks": [ { "type": "summary", "props": { "...": "..." } } ] },
    "createdAt": "ISO" },
  "url": "https://app.cubster.dev/a/aB3xQ9zK7mN2",
  "createdAt": "ISO", "updatedAt": "ISO"
}

ArtifactSummary (the list endpoint) is the same shape minus version. summary is the verdict of the document's first top-level summary block (truncated to 500 characters), or null when the document has none — recomputed on every write. url is always present, even while private — it 404s until visibility changes.

Serving & visibility

GET /a/:slug is an SPA page; it reads its data from GET /a/:slug/doc, both taking an optional ?v=N for an older version. A private artifact 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 an artifact back to private takes effect within minutes, not forever. The page has its own Print button, which opens ?print=1 in a new tab — every tabs panel and detail section expanded, every table row shown, and the print dialog opens automatically.

Embedding diagrams

A diagram block — anywhere in the document, including nested inside detail/tabs — renders inline only when the diagram it names exists, is not archived, lives in the same workspace as the artifact, and is at least as visible as the artifact (private < unlisted < public). Otherwise the reader sees a labeled placeholder — never a leak. This is decided server-side, independent of who is asking, and returned beside the stored document rather than inside it: GET /a/:slug/doc responds { ..., doc, diagrams: Record<slug, EmbeddedDiagram | null> }. So publish the diagram first with cubster diagram create , reference its slug, and if you share the artifact, flip the diagram's visibility to match — or the reader sees the placeholder where the picture should be.

A chart-embed block follows the identical rule — same workspace, not archived, at least as visible — and the same JSON response carries it: { ..., doc, diagrams, charts: Record<slug, EmbeddedChart | null> }. Unlike a diagram, a resolved chart carries its own published JSON rather than an image URL, so it draws through the same chart component an inline chart block uses — hover and "Show data" on the web, a data table in print. Publish it first with cubster chart create, and reuse the slug wherever you want that chart to stay in sync.

SDK

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

const check = await cubster.artifacts.validate({ title, blocks })
// { valid, errors } — writes nothing
const artifact = await cubster.artifacts.create({
  title, blocks, tags: ['ops'],
})
// artifact.visibility === 'private'; artifact.url 404s until a human shares it
const artifacts = await cubster.artifacts.list({ tag: 'ops' })
await cubster.artifacts.update(artifact.slug, { title, blocks: revisedBlocks })
await cubster.artifacts.archive(artifact.slug)

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

CLI

cubster artifact validate|create|update|get|ls|archive|unarchive|components, all over client.artifacts.*. A validation failure or a server-side rejection on create/update prints each problem as its own - <path>: <message> line on stderr and exits 1 — nothing is written or published.

$ cubster artifact validate <file> [--json]
$ cubster artifact create --file <f> [--title <t>] [--tag <t>]... [--json]
$ cubster artifact update <slug> --file <f> [--title <t>] [--tag <t>]... [--json]
$ cubster artifact get <slug> [--version <n>] [-o|--out <path>] [--json]
$ cubster artifact ls [--tag <t>] [--archived] [--json]
$ cubster artifact archive <slug> [--json]
$ cubster artifact unarchive <slug> [--json]
$ cubster artifact components [--type <t>] [--json]

No cubster artifact delete (or rm) and no --visibility flag anywhere — same two rules as the REST API, enforced again at this layer. components works without the skill installed — it prints the same catalog straight from the API, so an agent (or a person) can compose a document from the CLI alone.

What you can't do — and why
  • 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.
  • No delete — only reversible archive/unarchive. An artifact link handed to someone must never 404 because an agent tidied up its list.
  • No HTML, raw markup, scripts, or iframes — no component accepts them. prose renders sanitized Markdown with raw HTML disabled (html: false in markdown-it and a DOMPurify pass), links restricted to http(s)/mailto. There is no plan to add an escape hatch.
  • Charts take at most 6 series — six is the length of the validated palette; a seventh would have to reuse a color, and two series in one color is a chart that lies. Fold the tail into an "Other" series.
  • Containers don't nest — detail and tabs hold leaf blocks only — every component except those two — so a detail inside a detail, or a tabs inside a detail, fails validation with a path pointing at the offending block.