canvakit.sh

CANVAKIT.md

source ↗

CANVAKIT.md — template + data-source specification (v1, alpha)

Canvakit is an open standard for templates bound to data sources. A canvakit template is a single file with YAML frontmatter that declares named data sources and a body written in a pluggable template engine (Mustache by default). A canvakit runtime resolves the sources in parallel, merges the results into a rendering context, and emits the body — typically HTML, but the spec doesn't require a specific output format.

Canvakit's companion standard is DESIGN.md (aka designkit). DESIGN.md covers style — tokens and atomic components. CANVAKIT covers structure + binding — composable templates that agents fill in, update, and bind to tool outputs. The two are orthogonal and often used together: canvakit templates consume designkit tokens through the optional @canvakit/designkit bridge.

The reference implementation is @canvakit/core (Node/TypeScript). Canvakit templates are portable: any runtime that can resolve tool names, read files, and call a template engine can render them.

Why canvakit

An agent that needs to update a rendered UI with new data has two usual options: re-emit the whole HTML (expensive in tokens, fragile), or hand-wire a component library that knows how to refresh itself (heavy, framework-specific, non-portable). Canvakit gives a third option: render a template once, bind it to live data sources, re-render when the data changes. The template body is the same between updates; only the resolved data differs. Agents don't re-author HTML on every turn.

A canvakit template declares its tool dependencies. A runtime with those tools available (Mastra, MCP client, Vercel AI SDK, a CI job with fixtures) can render the template. The template is portable because the binding layer — kind: tool references — is a normalized concept that every agent runtime speaks.

Document structure

A canvakit template has three parts:

  1. YAML frontmatter (required) — machine-readable declarations: identity, data sources, variables.
  2. Body (required) — the content, interpreted by the chosen template engine. In v1 the default engine is Mustache.
  3. Output — whatever the engine produces. HTML is the soft default (what v1 tooling assumes); non-HTML outputs are spec-legal.

Canonical filename

Canvakit templates use a double extension: <name>.canvakit.<engine-ext>.

  • q2-dashboard.canvakit.html — Mustache-templated HTML
  • brief.canvakit.md — Mustache-templated markdown
  • report.canvakit.mdx — MDX-templated (Phase 3)

find . -name '*.canvakit.*' is the canonical glob. Editors autodetect the template file type by the infix. The body engine is self-describing via the final extension. The frontmatter template: true field is a redundant safety check — tooling that loads files by glob can still catch "renamed but not a template" mistakes.

The gallery + CLI resolve templates by the identity tuple (author, name, version) from the frontmatter. The filename on disk is a convention for humans and editors, not load-bearing for the registry.

YAML frontmatter

The frontmatter block must begin and end with lines containing exactly ---. Everything between is parsed as YAML.

Schema

# ── Identity (registry + tooling) ─────────────────────────────
template:    true                  # required marker
name:        <string>              # required; slug-safe
version:     <semver>              # required
description: <string>              # optional; one-liner for gallery
author:      <string>              # optional; registry populates from publish identity
# ── Rendering ─────────────────────────────────────────────────
renderer:    mustache              # optional; default "mustache"
design:      dk:heritage           # optional; designkit hint — see below
# ── Refresh cadence (declarative; honored opportunistically) ──
refreshEvery: manual               # manual | <duration> | on-tool-change
# ── Data ──────────────────────────────────────────────────────
variables:                         # authored defaults; overridable by caller
  <string>: <any>
dataSources:                       # the binding layer
  <name>:
    kind: tool | static | file | query
    # kind-specific fields — see §Data source kinds

design

Optional hint declaring the template's preferred designkit theme. Accepted shapes:

  • dk:<preset-id> — a preset shipped with @agstudio/design-kit (e.g. dk:heritage, dk:modern-minimal)
  • ./brand.md / brand.md / any path ending in .md — resolved through the canvakit filesystem adapter

When the @canvakit/designkit bridge is wired, design resolution priority is (highest first):

  1. Workspace pointer — _design/active.txt_design/<slug>.md
  2. Workspace root DESIGN.md (Stitch drop-in)
  3. The template's design: frontmatter hint

The operator's workspace choice overrides the author's hint — an active DESIGN.md near the render site represents operator intent, not accident. The hint kicks in when the template is rendered outside an operator's workspace (CLI, gallery, playground, CI) or the workspace has no active theme. Templates that want graceful degradation should still reference tokens from CSS via var(--color-foo, <fallback>).

refreshEvery

Declarative cadence hint. A runtime MAY honor it by scheduling re-renders; runtimes that can't (browser playground, one-shot CLI) treat it as manual. Accepted values:

  • manual (default) — never auto-refresh
  • duration string (60s, 5m, 1h) — timer-driven refresh
  • on-tool-change — re-render when any kind: tool source's input invalidates (for agent runtimes with tool tracking)

Identity tuple

(author, name, version) is the canonical registry key. Templates published to a canvakit gallery MUST carry all three. Local / in-workspace templates MAY omit author — the identity is local to the workspace.

Data source kinds

Canvakit ships two primitives and two filesystem shorthands.

tool — primitive

weather: { kind: tool, ref: <tool-id>, params: { ... } }

Invokes a tool by name with JSON params. The portable binding — any runtime that can resolve tool names can satisfy it. ref resolves in three styles, and implementations SHOULD support all three:

StyleExampleRuntime behavior
Flat idsearchFlightsLook up in the host's local tool registry
Namespacedstripe.mrr, notion.pages.getLongest-prefix match against registered wildcard resolvers
MCP-qualifiedmcp://myserver/toolNameProxy through MCP

Resolution precedence: exact flat > longest wildcard > MCP. Params are passed through verbatim to the resolver; the returned value passes through verbatim into the context.

static — primitive

header: { kind: static, value: "Welcome back" }

Literal pass-through. Never fetched, never parsed. Useful for caller-supplied defaults and per-template constants.

file — shorthand for tool + readFile

roadmap: { kind: file, path: /plans/q2.md }

Read a single file from the render filesystem, parse by extension (see §Format contract). A runtime without filesystem sugar MAY emit kind: file as { kind: tool, ref: "readFile", params: { path } } and implement the shape conversion in its readFile tool — the observed value in the context is identical.

query — shorthand for tool + query

tasks:
  kind: query
  include: /tasks/**/*.md
  where:   { status: todo }
  sort:    -updatedAt
  limit:   20
  fields:  [title, priority, assignee]

Fetch many files matching one or more globs; parse each by format; filter / sort / project.

  • include — glob pattern or array of patterns. A bare directory path (no glob char) is interpreted as <path>/**/*.md for back-compat with canvakit's pre-v1 queryFiles kind.
  • where — field predicates on the parsed shape. Operators: equality, _in (array membership), _contains (substring), _before / _after (date or number compare).
  • sortfield (asc) or -field (desc) on a parsed field.
  • limit — max entries (default 50).
  • fields — project into a subset of parsed keys.

Output: [{ path, data }] where data is the parsed content per the format contract.

Equivalent tool form: { kind: tool, ref: "query", params: { include, where, sort, limit, fields } }.

Per-source status

Every resolution reports a status — runtimes MUST surface this so authors can debug "empty render" cases without log-diving.

type DataSourceStatus =
  | { status: "ok", sampleKeys?: string[], count?: number }
  | { status: "missing", reason: string }
  | { status: "error", reason: string }

A failed source degrades its own key (returns null / []); the render completes with the remaining sources intact.

Deprecated kinds

  • kind: integration (pre-v1) — remapped to kind: tool with the same ref. Accepted in v1 with a parse-time warning; removed in v2 (target: 2026-Q4).
  • kind: queryFiles (pre-v1) — remapped to kind: query. Bare directory paths become <path>/**/*.md for continuity. Same deprecation cadence.

Imports (composable canvases)

A template MAY declare an imports map in its frontmatter. Each entry is a nested canvas that renders in isolation; the rendered body is exposed under {{{imports.<name>}}} in the parent's context.

imports:
  header:
    template: parts/header.canvakit.html       # no variables — pure partial
  mrr:
    template: widgets/mrr-card.canvakit.html   # reusable widget, own dataSources
    variables: { period: "Q2 2026" }

Semantics:

  • Each import is a full nested render — its own frontmatter parse, its own dataSources resolution, its own engine pass, its own _data payload.
  • Imports inherit the parent's filesystem, tool registry, and render extensions (so @canvakit/designkit themes children automatically).
  • Imports do not inherit the parent's variables or resolved sources — scope is deliberately isolated. Pass values down explicitly via each entry's variables: block.
  • Per-source statuses from an imported canvas roll up into the parent's statuses map under <importName>.<sourceName>.
  • Cycle protection: a template path already in the active import chain is rejected with a warning and renders as the empty string.
  • Depth cap: at least 8 levels of nested imports. Runtimes MAY cap at a lower value.
  • Reference the rendered body with triple-brace: {{{imports.mrr}}}. Double-brace would HTML-escape the rendered HTML.

Use cases

  • Shared layout fragments (header, footer, nav) — imports with no variables and no dataSources act as partials.
  • Reusable widgets (a stripe-mrr-card, a flights-card) — self-contained canvases composed into dashboards. Each re-renders against fresh tool data when its ref is re-invoked; the parent doesn't need to know.
  • Base layouts — a parent template with slots; child templates fill the body.

Token economics

A composed dashboard ships with thin frontmatter + a body that references {{{imports.*}}}. The agent authoring the dashboard doesn't re-emit every widget's HTML; the import reference is a few tokens. Re-running a widget's underlying tool re-renders just that widget's output, not the whole page.

Format contract

Both file and query parse a file's content by its extension into a uniform shape. Canvakit runtimes MUST implement these mappings so templates see the same data regardless of source format:

ExtensionParsed shape
.jsonParsed JSON (any shape)
.yaml / .ymlParsed YAML (any shape)
.csv / .tsv{ columns: string[], rows: Record<string, string>[] }
.md / .markdown{ ...frontmatter, $body: string }
.html / .htm / .txt{ $text: string } (escape-safe raw text)
(anything else){ $text: string }

A template rolling up tasks from markdown frontmatter and pricing from a CSV uses the same kind: query with two include patterns — the parsed shapes differ per entry but the iteration pattern is identical.

Context shape

The render pipeline merges resolved sources into a context object and passes it to the engine. Precedence (lowest to highest):

  1. Template-declared variables defaults
  2. Caller-provided variables overrides
  3. Resolved dataSources (wins — the dynamic data is the point)

Plus these well-known roots:

  • $meta{ renderedAt, templatePath, renderedFrom, instanceSlug }
  • $design — flattened designkit tokens when the optional designkit bridge is wired (e.g. {{$design.colors.primary}}). null when no design is active. Authoring convention: reference tokens from CSS via var(--color-foo, <fallback>) rather than emitting a competing :root { ... defaults ... } block in the template body — the bridge's injected :root can't override a later-in-body :root declaration (CSS cascade), and inline var() fallbacks render cleanly whether the bridge is wired or not.
  • _data — entire context serialized as <script>-safe JSON. See §Client-side rehydration.
  • renderedAt — alias for $meta.renderedAt exposed at the top level for older templates.

A caller-provided variables key that shadows a dataSources name is silently replaced by the source (the dynamic value wins). Runtimes SHOULD emit a warning so the collision is visible.

Client-side rehydration

Canvakit produces server-rendered content AND an embedded JSON copy of the same context, so the browser can rehydrate typed values without re-fetching or re-computing, without prescribing a frontend framework:

<script id="canvas-data" type="application/json">{{{_data}}}</script>

<script type="module">
  const ctx = JSON.parse(document.getElementById('canvas-data').textContent)
  // ctx.flights.flights is typed exactly as the tool returned it —
  // draw sparklines, animate counters, filter a table client-side
  // without duplicating logic in SQL or a second fetch.
</script>

The triple-brace {{{_data}}} skips the engine's default HTML escaping (because _data is already safe JSON — < in string values is pre-escaped to < to neutralize stray </script> sequences). React islands, Solid signals, Vue refs, and any other opinionated hydration contract build on this primitive.

Tool registry

A canvakit runtime hosts a tool registry that resolves kind: tool refs. The reference implementation exposes one function:

registerTool(match, resolver)

where match is one of:

  • "searchFlights" — exact flat id
  • "stripe.*" — namespaced wildcard (longest-prefix match)
  • "mcp://myserver/*" — MCP prefix

Resolution precedence is exact flat > longest wildcard > MCP. Registering the same match twice replaces the previous resolver (supports hot reload in dev).

Portability

A canvakit template with only kind: tool + kind: static entries is portable to any runtime that implements the tool registry. The frontmatter becomes a declaration of tool dependencies:

dataSources:
  flights: { kind: tool, ref: searchFlights, params: { ... } }
  mrr:     { kind: tool, ref: stripe.mrr,    params: { ... } }

Render under Mastra → its tool registry resolves searchFlights; a registered stripe.* handler resolves stripe.mrr.

Render under a Vercel AI app → the tools array supplies both.

Render under an MCP-speaking runtime → both proxied through MCP.

Render in CI with fixtures → each ref resolves to a mock.

Same template, different runtime, correct render.

The file / query shorthands exist because simple filesystem-driven templates are the 80% case. A bare-bones renderer (filesystem + Mustache, no tool runtime) handles them natively; a tool-runtime-only renderer maps them through the readFile / query well-known tools.

Streaming — v1 limitation

Tools that stream incremental results (long-running search, multi-step agent runs) are resolved fully before render in v1. Templates expecting progressive rendering SHOULD use refreshEvery or on-tool-change to re-render after each update. True streaming bodies (render-as-the-tool-streams) are out of scope for v1.

Output

Whatever the engine produces. HTML is the soft default — the v1 Mustache engine, the v1 playground, the v1 gallery preview, and all example templates in this spec assume HTML. Non-HTML templates (foo.canvakit.svg, foo.canvakit.xml, foo.canvakit.csv) are spec-legal but outside v1 tooling's polished path; runtimes MAY refuse them or render without preview.

Engine contract

A canvakit engine is a single function: (body, context) => Promise<string>. No class hierarchy, no lifecycle. An MDX engine (Phase 3) and a Handlebars engine both fit this shape. A canvakit core implementation MUST NOT import React, DOM, or any front-end framework; engines that need React import it themselves in a separate package.

Unknown content handling

Per the consumer-behavior convention inherited from DESIGN.md:

ScenarioBehavior
Unknown frontmatter keyPreserve (round-trip); do not error.
Missing template: true markerWarn; render anyway.
Unknown dataSources kindDrop with warning; other sources still render.
Malformed source fieldsDrop with warning; status reports the issue.
Missing / unreadable file for file sourceSource status becomes missing; value is null.
Unregistered tool refSource status becomes missing; value is null.
Unknown refreshEvery valueWarn; fall back to manual.

Runtimes SHOULD NOT crash a render over author / data mistakes — surface per-source statuses and carry on.

Versioning

This spec is v1, alpha in 2026-04. Breaking changes land in v2 (target: 2026-Q4):

  • kind: integration — removed
  • kind: queryFiles — removed
  • Stricter refreshEvery parsing

Additions (new kinds, new format mappings, new context roots) are non-breaking and land as minor updates to v1.


The reference implementation — @canvakit/core + @canvakit/designkit — lives in the agentik-studio monorepo. The public gallery + playground + CLI live at canvakit.sh.