pitch.json
Every field of the manifest, drawn from the schema pitch checks it with.
On this page
pitch.json sits at the root of the demo and says what the build is: a server
or a folder of files, the apps in it and the device each is for, the story the
room tells, and the pages beside it. The room is drawn from it, and nothing
the room shows a viewer is anything the file did not declare.
pitch init writes a first one by reading the repository. Its first line
points at the schema. This is the one it wrote for Columbus, the
Quickstart's demo, a Next app exported as files:
{
"$schema": "https://getroomi.com/schema/pitch.json",
"name": "Columbus",
"kind": "static",
"output": ".next-build",
"apps": [
{
"id": "app",
"name": "Columbus",
"path": "/",
"device": "laptop"
}
]
}Two kinds of build
Every pitch.json names its kind, and the
kind decides which other fields it may have.
container |
static |
|
|---|---|---|
| What it is | A server, built from the Dockerfile at the project's root. Each viewer gets their own running copy. |
A built folder of HTML, CSS and JavaScript, served as files. No server. |
| Its own fields | health, the path that answers once the server is up (default /healthz); run, how it starts, which pitch deploy reads from the image |
output, the folder the build writes, with an index.html in it |
controls |
Yes: a reset and scenario buttons | No: there is no server to call |
A story's run |
Yes: calls made on the viewer's copy before a step shows | No: a step shows an app and points, and runs nothing |
| Read more | Container demos | Static demos |
Both have a name, apps, and may have a story and pages.
A complete example
Columbus as it is published: one app, and a five-step story that opens four
of its pages and then shows the first again on a phone. A static build has no
controls, and its steps have no run.
{
"$schema": "https://getroomi.com/schema/pitch.json",
"name": "Columbus",
"kind": "static",
"output": ".next-build",
"apps": [{ "id": "columbus", "name": "Columbus", "path": "/", "device": "laptop" }],
"story": [
{
"id": "briefing",
"title": "What needs the house today",
"say": "Arundel House, an invented family office, opens Columbus on a concierge rather than a feed: two things are waiting on them, one closes in four days, and nothing else needs them today.",
"app": "columbus",
"path": "/"
},
{
"id": "score",
"title": "A score with its reasons",
"say": "Project Kestrel scores 74 against the house's own mandate. Each of the eight axes gives its reason, and the binding one is named: a 10–12% target sits below the house's 12–18% floor.",
"app": "columbus",
"path": "/transactions/kestrel/overview/"
},
{
"id": "table",
"title": "A seat at the table",
"say": "Four family offices take Project Anvil's £40m round between them. Arundel House's seat is £12.0m, and the cap table shows what it would hold: 7.4% fully diluted.",
"app": "columbus",
"path": "/transactions/anvil/structure/"
},
{
"id": "audited",
"title": "The score, held to account",
"say": "Columbus keeps the score it gave on the day the house committed, and sets it beside what happened: Thornbury scored 91 at entry and has returned 2.00× since. Old scores are never revised.",
"app": "columbus",
"path": "/portfolio/thornbury/overview/"
},
{
"id": "phone",
"title": "The same briefing on a phone",
"say": "On a phone the navigation folds into a drawer and tables become cards, so a principal can read what is waiting on them between meetings.",
"app": "columbus",
"path": "/",
"device": "phone"
}
]
}What each part does:
| Field | In this example |
|---|---|
$schema |
Points an editor at the schema, so it completes and checks the file. The platform reads nothing from it. |
name |
What the room calls the demo. The project's address is made from it: columbus. |
kind |
A built folder of files, served as they are. |
output |
The folder the build writes, with an index.html in it: Columbus's build writes .next-build. |
apps |
One app, Columbus, at /, in a laptop's frame. With one app, the room shows no switcher. |
story |
The guided tour. Each step opens the page it is about with path; the last shows the first page again with "device": "phone", and the laptop comes back after it. No step runs anything. |
A container example
A server has more to say: the path that answers once it is up, a reset, the launcher's buttons, and calls a step makes on the viewer's copy before it shows. Bullring, a gym's coaching product with four apps on one server, a reset, a scenario button, a three-step story and two pages:
{
"$schema": "https://getroomi.com/schema/pitch.json",
"name": "Corner for Bullring",
"kind": "container",
"health": "/healthz",
"apps": [
{ "id": "client", "name": "Bullring", "path": "/app", "device": "phone", "default": true },
{ "id": "coach", "name": "Bullring Coach", "path": "/coach/", "device": "tablet" },
{ "id": "hq", "name": "Corner HQ", "path": "/hq/", "device": "laptop" },
{ "id": "arena", "name": "Arena", "path": "/arena/", "device": "screen" }
],
"controls": {
"reset": "POST /api/demo/reset",
"scenarios": [
{
"id": "full-session",
"label": "Run a full session",
"description": "Tom books, trains and pays, start to finish.",
"call": "POST /api/demo/scenario",
"body": { "preset": "full-session" }
}
]
},
"story": [
{
"id": "book",
"title": "Tom books a session",
"say": "A member books from their phone in two taps. Watch the coach's iPad the moment it lands.",
"app": "client",
"run": ["POST /api/demo/reset"],
"point": "#book-button"
},
{
"id": "coach-sees-it",
"title": "The coach sees it at once",
"say": "No refresh and no phone call: the booking is on the coach's day before Tom has locked his screen.",
"app": "coach",
"run": [{ "call": "POST /api/demo/clock", "body": { "preset": "session" } }],
"point": "[data-booking]"
},
{
"id": "on-the-wall",
"title": "The gym sees the session",
"say": "The wall screen shows who is training now, so the floor staff know without asking.",
"app": "arena"
}
],
"pages": ["docs/why-corner.md", "docs/corner-architecture.html"]
}What its parts add:
| Field | In this example |
|---|---|
name |
The project's address is made from it: corner-for-bullring. |
kind |
A server, built from the Dockerfile. |
health |
The path the platform waits on before a viewer's copy is ready, and that pitch deploy requires to answer. |
apps |
Four tabs in the room's app switcher, each drawn in its own device's frame. client opens first. |
controls.reset |
A button that puts the viewer's copy back to its seeded start. |
controls.scenarios |
One button that drives the demo by hand, with a JSON body for its call. |
story |
The guided tour. The first step resets the copy so it can be started from; the second sends a body with its call; the third only shows an app. |
pages |
Two files listed under Read more, uploaded with every deploy. |
Apps and devices
An app is one surface of the build the room can show: a member's phone app,
a coach's tablet, a wall screen. Every app is served by the same build, on
the same origin, under its own path. The
room frames each at the real size of its
device:
device |
The frame |
|---|---|
phone |
A phone |
tablet |
A tablet |
laptop |
A laptop |
screen |
A wall or a TV |
none |
No frame: the app fills the stage |
The viewer can change the frame in the room. The app marked
default opens first; without one,
the first app does. An app's id is how the rest of the file names it: a
story step's app, and the room's record of what a viewer looked at.
Declare an app for each separate surface a person would pick up: Bullring's
client app, the coach's iPad, the HQ console, the arena's wall. The pages of
one web app are one app, however many the tour visits: a story step opens a
page of its app with path, and shows it
in another frame, for that step only, with
device.
Calls
A reset, a scenario and a story step each make a call on the viewer's own copy of the demo, server to server. A call is written as a method and a path on the build's own origin:
POST /api/demo/resetThe method is GET, POST, PUT, PATCH or DELETE. The room can make
only the calls pitch.json declares; a viewer's browser can never ask it for
another. What the demo answers is shown to the viewer: the message of a JSON
reply, or its error when it refused, as plain text.
A story step's call may carry a JSON body, as an object:
{ "call": "POST /api/demo/clock", "body": { "preset": "session" } }The story
The story is the tour the room offers each
viewer: up to 20 steps, in order. Each says what to notice
(say), puts an app on the stage
(app), may open it at one of its pages
(path) and in another frame
(device), may make calls first on a
container build (run), and may ring
something on the app's page (point, a
CSS selector, best effort). A viewer can start at any step from the outline,
so a step with calls sets up what it needs itself, and a step about a page
names it.
pitch deploy checks every page a step opens and runs every step's calls on
its own copy before anything is uploaded, and stops if a page is missing or
the demo refuses a call. Writing the tour is the guide.
Pages
pages lists Markdown and HTML files,
relative to pitch.json, up to 30. Each is uploaded before the build goes
live and listed in the room under Read more. Pages says how
each kind is shown.
How a container starts
run says how the platform starts a container
build: the command, as a list of words, and its environment. pitch deploy
reads both from the image your Dockerfile makes, its CMD and ENV, so
you rarely write run yourself. When you do, it is used as written. The
platform gives the build PORT and HOST itself.
Your editor checks it
The schema at /schema/pitch.json is generated from the
same definition the CLI and the platform check pitch.json with, so an editor
that reads $schema (VS Code, Zed, any JetBrains IDE) completes each field,
shows what it is for on hover, and marks a wrong value before you run anything.
The table below is drawn from the same schema.
The schema refuses any field it does not name, at every level. The platform
would ignore such a field, so a misspelt stroy is a tour that silently never
appears; the schema, and pitch validate, say so instead.
pitch validate checks the rest
Some rules are about the file as a whole, which a schema cannot say.
pitch validate checks them, pitch deploy runs it first, and the platform
checks them again when a build is registered:
- an app's
idappears once, and at most one app is thedefault; - a scenario's
idappears once; - every step of the story happens in an app
appsdeclares, and itsidappears once in the story; - a static build has no
controls, and its story's steps have norun: there is no server to call.
And some are about what the file names on disk, which only pitch validate
can see:
- every file in
pagesis there, inside the project, and no longer than a page can be; - a container build has a
Dockerfileat the root; - a static build's
outputis inside the project; - once a static build is built, every
pathits story opens is a file inoutput: the path itself, or theindex.htmlinside it.pitch deploychecks again in what it has just built.
Each problem names where it is and links to its field below. Errors
lists every one, and the notes pitch validate adds about a Dockerfile.
Every field
| Field | Type | What it is |
|---|---|---|
$schema | string | The JSON Schema this file follows, so an editor can complete and check it. pitch init writes it; the platform reads nothing from it.optional |
name | string | The demo’s name, shown in the room. The project’s address is made from it.required · 1–80 characters |
kind | "container" | "static" | container: a server, built from the Dockerfile at the root. static: a built folder of files.required |
output | string | The folder the build writes, relative to pitch.json, with an index.html in it. A static build only.required · not empty · static builds only |
health | string | A path that answers 200 once the server is up. A container build only.optional · default "/healthz" · container builds only |
run | object | How the runner starts the build. pitch deploy reads it from the image’s CMD and ENV, so it is rarely written by hand. A container build only.optional · container builds only |
run.start | list of strings | The command that starts the build in /app, as a list of words, like a Dockerfile’s exec-form CMD.required · 1–64 items · container builds only |
run.env | object of strings | Environment variables for the build. Mock settings only: never a secret. The runner sets PORT and HOST.optional · container builds only |
apps | list of objects | Every surface of the build the room can show, one tab each: the client app, the coach’s iPad, the wall.required · 1–12 items |
apps[].id | string | Names the app everywhere else in pitch.json: lower-case letters, digits and hyphens, unique in apps.required |
apps[].name | string | What the room calls the app, on its tab in the switcher.required · 1–60 characters |
apps[].path | string | Where the app is served on the build's own origin, starting with /.required |
apps[].device | "phone" | "tablet" | "laptop" | "screen" | "none" | The frame the room draws the app in, at that device’s real size: phone, tablet, laptop, screen (a wall or a TV), or none.required |
apps[].default | boolean | true on the app the room opens first. Without it, the first app opens first.optional |
controls | object | The launcher’s buttons, lifted into the room. A container build only.optional |
controls.reset | string | The call that puts the viewer’s copy back to its seeded start: "POST /api/demo/reset".optional |
controls.scenarios | list of objects | Buttons that drive the demo by hand, one call each. Still honoured; a story is the better way to tell it.optional · at most 20 items |
controls.scenarios[].id | string | Names the scenario: lower-case letters, digits and hyphens, unique in scenarios.required |
controls.scenarios[].label | string | The words on its button in the room.required · 1–60 characters |
controls.scenarios[].description | string | One line under the button, saying what it does.optional · at most 200 characters |
controls.scenarios[].call | string | The call the button makes on the viewer’s copy of the demo: "METHOD /path".required |
controls.scenarios[].body | JSON object | A JSON body sent with the call, when the route needs one.optional |
story | list of objects | The guided tour the room offers a viewer, in order: what to notice, which app to show and at which page, and what to run first.optional · 1–20 items |
story[].id | string | Names the step: lower-case letters, digits and hyphens, unique in the story.required |
story[].title | string | What happens in the step, in a few words.required · 1–60 characters |
story[].say | string | What the viewer should notice, and why it matters, in one or two sentences.required · 1–300 characters |
story[].app | string | The id of the app in apps the step is seen in: where the effect lands.required |
story[].path | string | The page of that app the step opens: a path on the build’s own origin, starting with /, such as /transactions/kestrel/overview/. Without it, the step shows the app where the viewer left it. A static build’s path must be a file in its output: the path itself, or index.html inside it.optional · at most 500 characters |
story[].device | "phone" | "tablet" | "laptop" | "screen" | "none" | The frame the room draws the app in for this step only: phone, tablet, laptop, screen or none. The app’s own device comes back when the viewer moves on or leaves the tour.optional |
story[].run | list of string or object | Calls made in order on the viewer’s own copy before the step shows: "METHOD /path", or { "call", "body" }. A container build only. A step sets up what it needs itself, so a viewer can start at any step.optional · 1–8 items |
story[].run[].call | string | "METHOD /path" on the build’s own origin.required |
story[].run[].body | JSON object | The JSON body the route needs.optional |
story[].point | string | A CSS selector on the app’s page for the room to ring. Best-effort: nothing breaks if it is not there. Prefer an id, a data- attribute or a class the demo’s own code names.optional · 1–200 characters |
pages | list of strings | Markdown or HTML files, relative to pitch.json, uploaded with every deploy and listed in the room’s rail under Read more.optional · at most 30 items |