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.
The CLI, then the cubster-artifacts skill so an agent knows the component catalog and the validate-then-create loop:
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.
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.
- Compose
{ title, blocks: [{ type, props }, ...] }from the components below. cubster artifact validateit — writes nothing on the server, valid or not.- Fix whatever
errorsnames — each carries a path to the exact field. cubster artifact createonce it validates — always starts private.- Hand out the
url. To revise,cubster artifact update <slug>with the full document again.
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/.
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:
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:
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.
{
"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.
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.
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.
client.artifacts.* mirrors the REST surface one-for-one. validate() 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. An artifact always starts private; sharing it is a dashboard action.
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.
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.
- 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.
proserenders sanitized Markdown with raw HTML disabled (html: falsein markdown-it and a DOMPurify pass), links restricted tohttp(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 —
detailandtabshold leaf blocks only — every component except those two — so adetailinside adetail, or atabsinside adetail, fails validation with a path pointing at the offending block.