Writing the tour
The story the room tells a viewer when you are not in the room.
The room's tour is story in pitch.json: an ordered list of steps. A viewer
reads a step's words, the room puts its app on the stage at the page the step
opens, in the frame the step asks for, runs its calls on that viewer's own
copy of the demo, and rings the part of the screen it points at. "Explore
freely" leaves the tour at any point, and the outline lets a viewer start at
any step.
The shape
{
"$schema": "https://getroomi.com/schema/pitch.json",
"name": "Columbus",
"kind": "static",
"output": ".next-build",
"apps": [
{ "id": "columbus", "name": "Columbus", "path": "/", "device": "laptop" }
],
"story": [
{
"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/"
}
]
}| Field | What it is |
|---|---|
id |
Lower-case letters, digits and hyphens; unique in the story |
title |
What happens, in a few words (60 characters at most) |
say |
What the viewer should notice, in one or two sentences (300 at most) |
app |
The id of an app in apps: the one the step is seen in |
path |
Optional. The page of that app the step opens, on the build's own origin, starting with /. Without it, the app is shown where the viewer left it |
device |
Optional. The frame the room draws the app in for this step only: phone, tablet, laptop, screen or none. The app's own frame comes back when the viewer moves on |
run |
Optional. Calls made in order on the build's own origin: "METHOD /path", or { "call": "METHOD /path", "body": { … } } when the route needs a JSON body. A container build only |
point |
Optional. A CSS selector in that app's page to ring. Best-effort: nothing breaks if it is not there |
A static build can have a story; its steps open pages, show and point, and
have no run.
How to write one
- Find what the demo can be told to do. On a container build, read its
launcher, its demo or scenario routes, its seed and its clock: the calls in
runare those routes. Read each handler: what it needs to be true first, and what it answers. A JSON reply'smessageis what the room shows as "Done: …", anderroras "Didn't work: …", so a route that answers in words reads well. On a static build there is nothing to call, so read its screens instead: which ones make the case, and the path each is at. - Tell it in three to six steps. One idea a step, in the order a person
would pitch it: who it is for, the moment the product matters, what the
business sees. A step that only shows an app (no
run) is fine and often right. - Make every step stand on its own. A viewer can jump to any step from
the outline, so a step sets up what it needs itself. If a scenario only
works at a certain demo time or after certain data exists, put that call
first in the same step (Container demos
has a worked example). A step that shows a particular screen names it
with
path, so it opens there whatever the viewer clicked before. - Show it where it lands.
appis where the effect is seen, which is often not where it was caused: a check-in made on the phone is seen on the coach's iPad.pathis the page of that app the step is about: the room opens the app there, and the viewer can click on from it as usual. Leavepathout to show the app wherever the viewer left it. - One app per product, not per screen. An app in
appsis a separate surface with its own tab in the room: Bullring's client app on a phone, the coach's iPad, the HQ console, the arena's wall screen. The screens of one web app are one app, and each step reaches its screen withpath. A different frame is not a different app either: to show the same page on a phone, give the step"device": "phone". - Point at something stable. Prefer an id, a
data-attribute or a class the demo's own code names, on the page the step opens. Never a generated class name. Leavepointout rather than guess. - Write
sayfor the viewer, not the builder. What to notice and why it matters, in the product's words. No instructions about the room.
A worked example: Columbus
Columbus is an interactive prototype of a private-markets introduction
service: a register of family offices (it calls them houses) on one side, a
catalogue of transactions on the other, and a suitability score between them
with its reasons written out. Every house, deal and figure in it is invented.
It is a Next.js app exported as files, so it is a static build: nothing runs
on a server, and no step has run.
It is one app, a web app with a side bar. The story stops at four of its
pages, each named by the step's path, and then shows the first page again
in a phone's frame:
"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"
}
]Why it is written the way it is:
- One app, five pages. Columbus is one product, so the room shows one
app, with no switcher. Each step opens the page it talks about, with the
trailing slash the export writes its folders with
(
/transactions/kestrel/overview/), and the viewer can click around from there as they would in Columbus itself. - Every step stands on its own. There is nothing to set up on a static build, and every step names its page, so a viewer who jumps to step 3 from the outline sees the Anvil table as the step describes it, wherever they had clicked to before.
- The phone is a frame, not an app. The last step is the first page
again with
"device": "phone": the same build, drawn at a phone's size. When the viewer moves on or leaves the tour, the laptop comes back. - The order is the pitch. What the house sees first, the one idea that sets the product apart (a score with its reasons), the mechanic that makes money (a table of houses), the claim only this product makes (old scores kept and audited), and the proof it works away from a desk.
sayis in the product's words, about Arundel House and its deals, not about the room or the buttons. Every figure in it is one the page shows.- No
point. Columbus's own code names no ids ordata-attributes on these pages, and its classes are Tailwind's, so there is nothing stable to ring. The step leaves it out rather than guess.
A container build tells its story the same way, and its steps can also run
calls on the viewer's copy first: Container demos
has Bullring's four-step story, which sets its demo's clock before each
scenario. Bullring really is four apps (the client's phone app, the coach's
iPad, the HQ console and the arena's wall), each at its own path on one
server, so its apps has four and its steps move between them.
Checking it
pitch validate checks the story without building anything, and pitch deploy checks it again first: each refuses a story whose step names an app
that is not in apps, repeats an id, has a malformed call, a path that
does not start with / or a device the room has no frame for, a misspelt
field, or runs calls on a static build, and points at the docs for each.
A step's page is then checked the way the room will serve it. On a static
build, each path must be a file in the output: the path itself, or the
index.html inside it (/transactions/kestrel/overview/ is
.next-build/transactions/kestrel/overview/index.html). pitch validate
looks in the output as it is on disk, when there is one, and pitch deploy
looks again in what it has just built, and stops before uploading if a page
is missing. A route a single-page app draws in the browser has no file of its
own, so it cannot be a step's page; the app's own path can, and the viewer
clicks on from there.
On a container build the rehearsal runs every step's calls, in order, on its
local copy, then asks the copy for the step's page, and prints ok or FAIL
per step with what the demo said. A page must answer with a 2xx, or with a
3xx that sends the viewer on. A FAIL is a step a viewer would see fail: fix
the story (usually a missing first call, or a mistyped path), not the demo,
and rehearse again. A static build's steps have nothing to run, so its
rehearsal lists them under what would go live, with the page and frame each
opens. point selectors, and whether a page shows what its step says, are
not checked by either.
Look at them in the room itself before you publish: pitch dev, beside the
demo's own dev server, serves the real room on your machine and opens it.
Take the tour there: each step opens its page in its frame, and its point
is rung or it is not. A step's page the dev server answers with a 404, and a
call the demo refuses, are listed on the room in the rehearsal's words, and
the room redraws each time you save pitch.json. Nothing in it is recorded
or sent. Preview locally has the details.