Building an Applet
The Manifest
On this page
Introduction
manifest.json sits at the root of the zip and is your applet's entire declaration of intent. It names the applet, states which capabilities it calls, and describes the shape of everything the Rack will need to validate, store, index, report on and render.
Declaration is mandatory, not advisory. A call to a capability the manifest did not declare is refused at runtime with undeclared_capability. The manifest is also what a reviewer reads when a new version widens what your applet can reach; see capability-diff review.
Core Fields
Every manifest starts with the same handful of core fields:
{
"schema": "ziprack/1",
"slug": "my-applet",
"name": "My Applet",
"version": "1.0.0",
"entry": "index.html",
"requires": { "capabilities": [], "profile": [] },
"media": []
}| Field | Rules |
|---|---|
schema |
Required. Exactly ziprack/1. |
slug |
Required. ^[a-z0-9]+(-[a-z0-9]+)*$, max 100. Identifies the applet on the Rack. Immutable. |
name |
Required, max 255. Shown to participants and administrators. |
version |
Required. MAJOR.MINOR.PATCH with optional -prerelease, max 50. Unique per applet. |
entry |
Optional, default index.html. Relative, no .., ends in .html. |
requires |
Required, even if empty. See The Requires Block. |
media |
Optional. See Media. |
roles |
Optional. The applet's own roles. See Roles. |
Everything else at the top level belongs to a capability. A section whose owning capability is not declared is a validation error, and so is a section that no capability the Rack implements owns.
The Requires Block
The requires block lists the capabilities your applet calls and the profile attributes it would like released:
"requires": {
"capabilities": ["auth@1", "attempt@1", "activity@1", "ai@1"],
"profile": ["display_name", "business_unit", "locale"]
}- Each capability reference matches
^[a-z][a-z0-9_]*@\d+$. Declaring a capability twice is an error, and so is declaring one the Rack does not implement. The error names what it does implement. profilelists the attribute keys you want released, and requiresauth@1. Keys match^[a-z][a-z0-9_]*$. A key the deployment does not release is simply absent at launch, so degrade gracefully.- Declaring
attempt@1has a side effect: it pins every participant to the version they started on. See Versioning.
Roles
You may declare your applet's own roles in a roles block:
"roles": [
{ "name": "dungeon-master", "label": "Dungeon Master", "description": "Runs the session." },
{ "name": "spectator", "grant": "guests" }
]roles is a core key, so it needs no capability. Your applet names its roles; the Rack decides who holds them, and every launch tells your applet which ones the participant holds. See Roles for how they resolve.
| Field | Rules |
|---|---|
name |
Required. Lowercase words joined by hyphens, ^[a-z][a-z0-9]*(-[a-z0-9]+)*$, max 40, unique. Not one of the built-in roles: rack-administrator, applet-author, member, guest. |
label |
Optional, max 60. Shown to whoever assigns the role. Defaults to name. |
description |
Optional, max 255. |
grant |
Optional. Who holds it before anyone is assigned: everyone, members, guests or assigned (the default, meaning nobody). |
A manifest may declare at most 20 roles.
Capability Sections
Each capability owns one top-level section of the manifest:
| Section | Capability | Shape |
|---|---|---|
segments[] |
attempt@1 |
{key, label, values[{key,label}]} — dimensions reports filter by |
activities[] |
activity@1 |
{id, type, label, scale?, segment?, sequence?} — required, at least one |
reporting.metrics[] |
activity@1 (+ attempt@1) |
{key, label, pre, post} — pairs two declared activities |
collections[] |
db@1 |
{name, scope, schema, indexed?, quota?, permissions?, seed?} |
uploads[] |
storage@1 |
{name, scope, mime[], max_bytes?, max_files?} |
prompts[] |
ai@1 |
{id, mode, template, vars?, output_schema?, budget?, model?} |
broadcast |
broadcast@1 |
{channels?, facilitator_events?} |
Each section is specified in full on its capability's page. Two cross-cutting rules are worth stating here.
Scopes are declarations, not runtime choices. db@1 collections and storage@1 uploads each declare a scope: attempt, user, room or applet. An attempt scope requires attempt@1, while a room scope requires broadcast@1. The scope answers, at declaration time, what happens to the data when an attempt completes.
Quotas and budgets are ceilings you may lower, not raise. Each Rack sets hard maxima. A manifest asking for more fails validation, and a manifest asking for nothing gets the deployment's defaults.
A Complete Example
Let's take a look at an applet exercising every capability at once. Of course, most applets need only three or four:
{
"schema": "ziprack/1",
"slug": "team-summit",
"name": "Team Summit",
"version": "2.1.0",
"entry": "index.html",
"requires": {
"capabilities": ["auth@1", "attempt@1", "activity@1", "db@1", "storage@1", "ai@1", "broadcast@1"],
"profile": ["display_name", "business_unit", "locale"]
},
"media": [
{ "id": "intro.welcome", "provider": "vimeo", "key": "912345678" },
{ "id": "debrief.clip", "provider": "youtube", "key": "dQw4w9WgXcQ" }
],
"roles": [
{ "name": "judge", "label": "Judge", "description": "Scores the final pitches." }
],
"segments": [
{
"key": "track",
"label": "Track",
"values": [
{ "key": "rd", "label": "R&D" },
{ "key": "commercial", "label": "Commercial" }
]
}
],
"activities": [
{ "id": "intro.confidence", "type": "rating", "label": "How confident do you feel about the new operating model?", "scale": { "min": 1, "max": 6 }, "sequence": 1 },
{ "id": "team.pitch", "type": "text", "label": "In one sentence, what is your team's pitch?", "segment": "track", "sequence": 2 },
{ "id": "outro.confidence", "type": "rating", "label": "How confident do you feel about the new operating model?", "scale": { "min": 1, "max": 6 }, "sequence": 3 }
],
"reporting": {
"metrics": [
{ "key": "confidence", "label": "Confidence", "pre": "intro.confidence", "post": "outro.confidence" }
]
},
"collections": [
{
"name": "ideas",
"scope": "room",
"schema": {
"type": "object",
"properties": {
"title": { "type": "string", "maxLength": 120 },
"body": { "type": "string", "maxLength": 2000 },
"status": { "type": "string", "enum": ["draft", "posted", "picked"] },
"votes": { "type": "integer", "minimum": 0 }
},
"required": ["title", "status"]
},
"indexed": ["status", "votes"],
"quota": { "max_records": 500, "max_bytes": 16384 },
"permissions": { "update": "owner", "delete": "facilitator" }
}
],
"uploads": [
{ "name": "artifacts", "scope": "room", "mime": ["image/png", "image/jpeg"], "max_bytes": 5242880, "max_files": 10 }
],
"prompts": [
{
"id": "score_pitch",
"mode": "analysis",
"template": "Score this team pitch for clarity and specificity.\n\nPitch: {{pitch}}\nTrack: {{track}}",
"vars": {
"type": "object",
"properties": { "pitch": { "type": "string" }, "track": { "type": "string" } },
"required": ["pitch"]
},
"output_schema": {
"type": "object",
"properties": {
"clarity": { "type": "integer", "minimum": 1, "maximum": 5 },
"feedback": { "type": "string" }
},
"required": ["clarity", "feedback"]
},
"budget": { "max_calls_per_attempt": 3, "max_output_tokens": 400 }
}
],
"broadcast": {
"channels": ["main", "board"],
"facilitator_events": ["advance", "lock", "reveal"]
}
}Validation Errors
When you upload, every validation error is reported at once, keyed by manifest path, in a single message.
These are the checks that most often catch people out:
| Error | Cause |
|---|---|
Unknown manifest section [x] |
No capability the Rack implements owns that top-level key. |
Manifest section [collections] requires the [db] capability |
The section is there; the declaration is not. |
This Rack does not implement [db@1]. Implemented: … |
Wrong capability or wrong version for this deployment. |
Activity references unknown segment [track] |
activities[].segment must name a declared segment key. |
Metric references unknown activity [outro.x] |
pre and post must be declared activity ids. |
reporting.metrics needs the attempt capability |
Metrics are computed over completed attempts. |
A room-scoped collection requires the [broadcast] capability |
Scope implies capability. |
Indexed path [votes] must be a string, integer, number or boolean property declared in the schema |
You can only index what you declared, and only scalars. |
Only applet-scoped collections can be seeded |
A seed is shared reference data; see Seeding. |
Role names are lowercase words joined by hyphens, like "dungeon-master". |
roles[].name has capitals, underscores or spaces. |
Role [guest] is one every Rack provides …; it cannot be declared. |
rack-administrator, applet-author, member and guest are built in. |
Duplicate role [judge]. |
Role names are unique. |
A role's grant is one of everyone, members, guests or assigned. |
Any other grant. |
Placeholder {{pitch}} is not declared in vars.properties |
Prompt templates and their vars schema must agree. |
Analysis prompts need an output_schema of type object |
mode: "analysis" is structured output by definition. |
The $room channel is reserved for the Rack |
Pick another channel name. |