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 .busabaseignoreNothing 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:
| Directory | Why |
|---|---|
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
---| Key | Meaning |
|---|---|
name | Required, and must equal busabase.json's name — they are the same app |
description | Shown wherever the skill is listed |
metadata.busabase.template | The opt-in. true means "publish this as a template" |
metadata.busabase.resources | Base slugs the manual talks about. Each must exist under content/ |
metadata.busabase.folderSlug, risk | Declarative — 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 }
}
}| Field | Default | What it does |
|---|---|---|
category | required | The 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 |
schemaVersion | 1 | Bumped 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:
template.airappif declared — it must match an AirApp undercontent/.- Otherwise
template.airapps, which must declare exactly oneprimaryand reference only AirApps that exist. - Otherwise, if
content/holds exactly one AirApp, that's it. - 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:
| Pattern | Meaning |
|---|---|
# comment, blank line | Ignored |
test/ | That directory and everything under it |
coverage | A file or directory with this name, at any depth |
/build | Anchored to the AirApp root only |
*.log | * matches within one path segment |
docs/**/*.tmp | ** matches across segments |
!keep.log | Negation — 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
screenshotsreplaces 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
videoand keeps the English screenshots. - A localized cover is optional. If the cover has no text worth translating, list the default
assets/screenshots/cover.webpfirst 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 秒看懂" }] }
}videois still the one-clip shorthand and is unchanged. When both are declared,videoswins;busabase-cli indexwritesvideoas the first clip, so servers older thanvideoskeep 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 singlevideo) 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 checkfails 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 notitle, or whenvideodisagrees with the firstvideosentry. - 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"
| Check | Why it's fatal |
|---|---|
No root SKILL.md | It's simply a plain package |
| Frontmatter isn't a valid skill declaration | Nothing downstream can read it |
metadata.busabase.template isn't true | No opt-in, no publication |
busabase.json has no template object | At minimum category is required |
SKILL.md's name ≠ manifest name | They 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 resolved | Install must not guess which app to open |
An AirApp's package.json is invalid JSON, or has no dev script | It 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 records | A template seeds a demo; it does not ship a dataset |
Warnings — it still publishes, the card is just honest
| Warning | Consequence |
|---|---|
| No AirApp | Data-only template: users see tables, not an app |
| No sample records | The app is empty on first open |
| No screenshots | The card uses a placeholder |
No agentPrompts | The 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.
| Node | Stamp |
|---|---|
| Every resource (Base, Drive, AirApp, …) | appId, resourceKey, schemaVersion |
| The app's root Folder | The same triple with resourceKey: "app-root", plus version, source (repo / ref / subdir) and installedAt |
| The Skill node lifted from the package root | appId and isTemplateSkill: true |
Two details carry weight:
resourceKeyis the package's own slug, not the installed one. A Base the author calledsettingsinstalls asbusa-email-settings, and the stamp still sayssettings. 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_CONFLICTrather 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".
Related
- What to build — package, Skill, template, or AirApp: which one this is
- Templates — installing and using them
- Publish a template —
export --templateand the catalog - Install & Export (packages) — the base format
- Node types — Skill and AirApp nodes
Templates
Give your agent business context, data, and instructions, then keep working through conversation and see the results in Busabase.
Publish a template
Share a starting context another agent can work with — export the resources, write its operating manual, and show how people can ask for the work and check the result.