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 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/overview.webp"],
    "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. Resolved to URLs server-side, so a card can never point at a different ref than the one it installs
agentPrompts[]Ready-made prompts shown on the detail page — the difference between "a folder of tables" and something usable
airappSingle-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
vaultNamespaceWhich Vault namespace the app's secrets live in
secrets[]{ key, description, required }[]declared, never created
requires.airapptrue 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.


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