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/reset

The 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 id appears once, and at most one app is the default;
  • a scenario's id appears once;
  • every step of the story happens in an app apps declares, and its id appears once in the story;
  • a static build has no controls, and its story's steps have no run: 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 pages is there, inside the project, and no longer than a page can be;
  • a container build has a Dockerfile at the root;
  • a static build's output is inside the project;
  • once a static build is built, every path its story opens is a file in output: the path itself, or the index.html inside it. pitch deploy checks 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

The fields of pitch.json
FieldTypeWhat it is
$schemastringThe JSON Schema this file follows, so an editor can complete and check it. pitch init writes it; the platform reads nothing from it.optional
namestringThe 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
outputstringThe folder the build writes, relative to pitch.json, with an index.html in it. A static build only.required · not empty · static builds only
healthstringA path that answers 200 once the server is up. A container build only.optional · default "/healthz" · container builds only
runobjectHow 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.startlist of stringsThe 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.envobject of stringsEnvironment variables for the build. Mock settings only: never a secret. The runner sets PORT and HOST.optional · container builds only
appslist of objectsEvery 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[].idstringNames the app everywhere else in pitch.json: lower-case letters, digits and hyphens, unique in apps.required
apps[].namestringWhat the room calls the app, on its tab in the switcher.required · 1–60 characters
apps[].pathstringWhere 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[].defaultbooleantrue on the app the room opens first. Without it, the first app opens first.optional
controlsobjectThe launcher’s buttons, lifted into the room. A container build only.optional
controls.resetstringThe call that puts the viewer’s copy back to its seeded start: "POST /api/demo/reset".optional
controls.scenarioslist of objectsButtons 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[].idstringNames the scenario: lower-case letters, digits and hyphens, unique in scenarios.required
controls.scenarios[].labelstringThe words on its button in the room.required · 1–60 characters
controls.scenarios[].descriptionstringOne line under the button, saying what it does.optional · at most 200 characters
controls.scenarios[].callstringThe call the button makes on the viewer’s copy of the demo: "METHOD /path".required
controls.scenarios[].bodyJSON objectA JSON body sent with the call, when the route needs one.optional
storylist of objectsThe 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[].idstringNames the step: lower-case letters, digits and hyphens, unique in the story.required
story[].titlestringWhat happens in the step, in a few words.required · 1–60 characters
story[].saystringWhat the viewer should notice, and why it matters, in one or two sentences.required · 1–300 characters
story[].appstringThe id of the app in apps the step is seen in: where the effect lands.required
story[].pathstringThe 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[].runlist of string or objectCalls 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[].callstring"METHOD /path" on the build’s own origin.required
story[].run[].bodyJSON objectThe JSON body the route needs.optional
story[].pointstringA 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
pageslist of stringsMarkdown 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