Busabase

Template format

What a Busabase template directory contains — the root SKILL.md, the template object in busabase.json, .busabaseignore, the validator's rules, and the ownership stamp that lets two installers recognise the same app.

A template packages working context for an agent: the business purpose, initial resources, and the manual for operating them. After installation, the user continues to give the agent tasks through conversation and uses Busabase to inspect results, review changes, and look back at the work.

The format below describes how to save and share that starting point. In the manual, explain how the agent should work with the resources over time; use screenshots to show the results and review surface people will see. For the user workflow, start with Templates.

A template is the busabase-package@1 format plus a small increment that lives entirely outside content/:

busa-email/
├── SKILL.md                  ← the manual an agent reads
├── references/               ← carried into the Skill node
├── agents/
├── scripts/
├── assets/screenshots/       ← Template Center card art (never installed)
├── busabase.json             ← manifest + a `template` object
└── content/                  ← byte-identical to a plain package
    ├── settings/base.json
    ├── settings/records.ndjson
    └── busa-email-app/      ← an AirApp, with a .busabaseignore

Nothing under content/ changes — the package format is unchanged, which is what makes this a multiplexed format rather than a second one. The same directory is a valid Agent Skill (an agent can install it with npx skills add) and a valid Busabase package (busabase-cli install can install it), and neither view has to know about the other.


The root SKILL.md

The entry file is named SKILL.md on purpose: it becomes exactly a Skill node on install, inside the target folder, so the manual travels with the resources instead of staying on the publisher's disk.

Three sidecar directories are carried into that node alongside it:

DirectoryWhy
references/Field references, schemas, examples — the material SKILL.md links to
agents/Sub-agent definitions the skill ships
scripts/The skill's own scripts, e.g. setup.mjs

scripts/ is stored, never executed by Busabase. It is carried so that an agent pulling the skill back out of a space gets a working copy — dropping it would make the round trip lossy. The safety property comes from nothing in Busabase running it, not from refusing to store it.

Frontmatter

---
name: busa-email
description: An inbox triage desk — drafts replies, you approve them.
metadata:
  busabase:
    template: true
    resources:
      - settings
      - reviews
---
KeyMeaning
nameRequired, and must equal busabase.json's name — they are the same app
descriptionShown wherever the skill is listed
metadata.busabase.templateThe opt-in. true means "publish this as a template"
metadata.busabase.resourcesBase slugs the manual talks about. Each must exist under content/
metadata.busabase.folderSlug, riskDeclarative — for readers and future tooling; nothing in install reads them today

template: true is an explicit opt-in, not an inference from directory shape. Publishing a template means accepting that installers will run its AirApp code and feed its SKILL.md to their agent; that deserves a deliberate flag.

resources is validated rather than trusted: an agent told about a table that does not exist will write to the wrong one.


The template object in busabase.json

{
  "format": "busabase-package@1",
  "name": "busa-email",
  "description": "An inbox triage desk.",
  "version": "1.2.0",
  "template": {
    "category": "email",
    "tags": ["inbox", "triage"],
    "screenshots": ["assets/screenshots/cover.webp", "assets/screenshots/overview.webp"],
    "video": "assets/recordings/busa-email.mp4",
    "locales": {
      "zh-CN": {
        "screenshots": ["assets/screenshots/cover-zh-CN.webp", "assets/screenshots/overview-zh-CN.webp"],
        "video": "assets/recordings/busa-email-zh-CN.mp4"
      }
    },
    "agentPrompts": [
      "Triage today's inbox and draft replies for the ones that need one."
    ],
    "airapp": "busa-email-app",
    "schemaVersion": 1,
    "secrets": [
      { "key": "BUSA_EMAIL_IMAP_PASSWORD_PRIMARY", "description": "Mailbox password", "required": true }
    ],
    "requires": { "airapp": true }
  }
}
FieldDefaultWhat it does
categoryrequiredThe Template Center category shown on the card
tags[]Searchable in the gallery
screenshots[]Package-relative paths, English/default set. [0] is the cover. Resolved to URLs server-side, so a card can never point at a different ref than the one it installs
video—Package-relative path to the English/default demo clip, shown beside the gallery on the detail page
videos—Up to three demo clips, [{ "src": "…", "title": "…" }], shown in order right after the cover. Wins over video. See Several demo clips
locales—Per-locale screenshots / video / videos, keyed by locale ("zh-CN", "ja"). See Localized screenshots and video
agentPrompts[]Requests people can give their agent after installation, including recurring work and follow-up adjustments
airapp—Single-AirApp shorthand. Mutually exclusive with airapps
airapps—{ slug, role, label }[]. Roles: primary, admin, public, tool; exactly one must be primary
schemaVersion1Bumped when the declared resource shape changes. Part of the ownership stamp, so both installers must agree on it
vaultNamespace—Which Vault namespace the app's secrets live in
secrets[]{ key, description, required }[] — declared, never created
requires.airapp—true makes "no AirApp in content/" an error rather than a warning

The package format has no slot for secret values and must not grow one — the same "you cannot leak what the format cannot express" rule the whole format rests on. secrets tells a user which keys to put in the Vault themselves; install neither creates them nor currently prompts for them.

Which AirApp is "the app"

Only primary has mechanical meaning; the other roles are labels for humans and for an agent's benefit ("start the admin panel"). Resolution order:

  1. template.airapp if declared — it must match an AirApp under content/.
  2. Otherwise template.airapps, which must declare exactly one primary and reference only AirApps that exist.
  3. Otherwise, if content/ holds exactly one AirApp, that's it.
  4. Otherwise: an error. Ambiguity is never resolved by taking the first alphabetically — a template that silently opens the wrong app is worse than one that refuses to publish.

.busabaseignore

An AirApp node is the project — Busabase starts it with npm run dev against exactly what the node contains — so content/<airapp>/ has to be both the deployable artefact and the directory a developer works in. content/<airapp>/.busabaseignore is what lets one directory be both: the repo keeps test/, lockfiles and coverage; the node receives only what has to run.

It is a deliberately small subset of gitignore syntax, stated so authors are never surprised by a silent near-miss:

PatternMeaning
# comment, blank lineIgnored
test/That directory and everything under it
coverageA file or directory with this name, at any depth
/buildAnchored to the AirApp root only
*.log* matches within one path segment
docs/**/*.tmp** matches across segments
!keep.logNegation — re-includes a path an earlier rule excluded

Last matching rule wins, exactly like gitignore. Not supported: character classes ([a-z]) and escaped literals (\#) — a pattern using them is matched literally rather than silently reinterpreted.

package.json, and the entry it declares, can never be ignored. That is a hard error rather than a warning, because the resulting AirApp would install fine and then fail to boot — the failure mode hardest to attribute.


Screenshots

assets/screenshots/ is card art for the Template Center. These files are not nodes and are never installed — they exist so the gallery has something to show. Declare them in template.screenshots with package-relative paths; the server turns them into URLs against the same repo and ref the card installs from.

A missing or renamed file degrades to the placeholder rather than a broken-image icon: the card is still useful without it.

Localized screenshots and video

A template whose app ships in more than one language can show each viewer their own language's screenshots and demo clip. The top-level screenshots and video stay the English/default set; every other language goes under template.locales:

"screenshots": ["assets/screenshots/cover.webp", "assets/screenshots/overview.webp"],
"video": "assets/recordings/busa-rnd.mp4",
"locales": {
  "zh-CN": {
    "screenshots": ["assets/screenshots/cover-zh-CN.webp", "assets/screenshots/overview-zh-CN.webp"],
    "video": "assets/recordings/busa-rnd-zh-CN.mp4"
  }
}

The rule is the one app stores use for localized screenshots, and the same fallback a template's localized description gets:

  • A viewer whose locale has an entry sees that entry's media. The entry's screenshots replaces the default list whole: it is never merged image by image, so [0] of the localized list is that language's cover.
  • Whatever an entry leaves out falls back to the default. A locale that only re-recorded the clip declares just video and keeps the English screenshots.
  • A localized cover is optional. If the cover has no text worth translating, list the default assets/screenshots/cover.webp first in the locale's own list. Because the list replaces the default whole, leaving the cover out means that language has no cover.
  • A viewer whose locale has no entry sees the defaults. Keys must match exactly ("zh-CN", not "zh"), and a key Busabase doesn't recognise still installs. It is simply never shown.

Name localized files <name>-<locale>.<ext> (cover-zh-CN.webp, busa-rnd-ja.mp4). Keep them out of the default list: the defaults are what every language without its own entry sees, so a mixed English-and-Chinese default list shows Chinese screenshots to Japanese viewers. Servers and CLIs older than locales ignore the key and show the defaults, which is why the defaults must be the English set.

busabase-cli check treats every localized path like a default one: a missing file is the same package/screenshot-missing / package/video-missing error. It also warns when a locales key is not a supported locale, when locales.en duplicates the English defaults, or when a file named for one locale (-zh-CN.webp) sits in the default list.

Several demo clips

A template can show up to three demo clips, for example a 30-second tour and a longer walkthrough. Declare them in template.videos; each entry is an object, and title is what tells the tiles apart:

"videos": [
  { "src": "assets/recordings/busa-rnd-tour.mp4", "title": "30-second tour" },
  { "src": "assets/recordings/busa-rnd-full.mp4", "title": "Full walkthrough" }
],
"locales": {
  "zh-CN": { "videos": [{ "src": "assets/recordings/busa-rnd-tour-zh-CN.mp4", "title": "30 秒看懂" }] }
}
  • video is still the one-clip shorthand and is unchanged. When both are declared, videos wins; busabase-cli index writes video as the first clip, so servers older than videos keep showing one clip instead of none.
  • The clips sit in the gallery right after the cover, in the order you list them, before the other screenshots. With more than one, each tile carries its title, so give every clip one.
  • The same whole-list rule as screenshots: a locale's videos (or its single video) replaces the default clips, and a language with no entry sees the default clips.
  • The limit is three, as on the App Store. busabase-cli check fails a longer list, because the extra clips would never be shown. It also reports a missing clip file (package/video-missing, naming the list index) and warns when a clip in a list of several has no title, or when video disagrees with the first videos entry.
  • Entries are objects from the start, not bare paths, so a field can be added later without breaking servers that already read the catalog.

Sample records with required relations

Relation values in records.ndjson are package record keys, resolved within the relation field's targetBaseSlug. The installer creates required-relation targets first and replaces those keys with canonical record ids before it creates each dependent row. Optional relations are normalized after every sample row has an id.

That makes acyclic dependencies such as Company → Contact → Activity installable without weakening the Base schema. It also gives three mistakes a clear preview-time failure, before any workspace resource is created:

  • a required relation is empty;
  • its key does not exist in the declared target Base;
  • required relations form a cycle, so no valid first row exists.

Record keys are scoped to their Base. Two Bases may both contain a key such as default; the relation field's targetBaseSlug is what makes the reference unambiguous.


What the validator checks

One function decides whether a package is a template, and all four consumers share it: the catalog build, busabase-cli install, the dashboard's install preview, and an agent checking its own output. A skill that shows up as a card but installs as a plain package (or the reverse) is a trust bug, not a cosmetic one.

It is purely static — no AirApp is started, no script is run, no server is contacted.

Errors — any one of these means "not a template"

CheckWhy it's fatal
No root SKILL.mdIt's simply a plain package
Frontmatter isn't a valid skill declarationNothing downstream can read it
metadata.busabase.template isn't trueNo opt-in, no publication
busabase.json has no template objectAt minimum category is required
SKILL.md's name ≠ manifest nameThey are the same app and must agree
A resources entry has no matching Base under content/An agent would write to the wrong table
The primary AirApp can't be resolvedInstall must not guess which app to open
An AirApp's package.json is invalid JSON, or has no dev scriptIt would install fine and never boot
requires.airapp is true with no AirApp in content/The manifest contradicts the contents
A Base carries more than 50 sample recordsA template seeds a demo; it does not ship a dataset

Warnings — it still publishes, the card is just honest

WarningConsequence
No AirAppData-only template: users see tables, not an app
No sample recordsThe app is empty on first open
No screenshotsThe card uses a placeholder
No agentPromptsThe detail page has no suggested prompts

Failing to be a template is never a reason to refuse a valid package. A package that fails these checks still installs — as a plain busabase-package@1, exactly as it did before templates existed.


Ownership stamps

This is the mechanism that makes "two doors, one app" work. Two independent code paths create the same resources — the installer in busabase-package, and a skill's own setup.mjs through busabase-sdk — and they recognise each other's work only by the shape of a stamp written into node metadata.

NodeStamp
Every resource (Base, Drive, AirApp, …)appId, resourceKey, schemaVersion
The app's root FolderThe same triple with resourceKey: "app-root", plus version, source (repo / ref / subdir) and installedAt
The Skill node lifted from the package rootappId and isTemplateSkill: true

Two details carry weight:

  • resourceKey is the package's own slug, not the installed one. A Base the author called settings installs as busa-email-settings, and the stamp still says settings. That's how the app finds its own tables by a stable handle regardless of which folder it was installed into.
  • The SDK treats an unstamped — or differently stamped — node as someone else's and raises SETUP_CONFLICT rather than touching it. So if the two writers ever drifted, a user who installed from the Template Center and then ran the same skill in their shell would get a hard conflict on their own data. That's why the stamp is defined once, in the contract package, and neither side may re-declare it locally; the SDK's copy is checked against the contract's at compile time.

The Skill node's isTemplateSkill marker is needed in both directions: export --template lifts exactly that node back out to the package root (rather than writing it under content/, which would create the duplicate the format exists to avoid), and the agent wiring lists exactly these nodes as "app manuals available in this space".


On this page