Flow files
The canonical JSON is the authoring format — local pictures, narration and card text, saved with one command.
A flow is a file before it is a row. The canonical JSON is schema-checked, diffable, and can be
checked into the repository beside the product it describes — which is only useful if the file can
also be the thing that ships. ff publish is what makes that true.
There are three ways to get one:
| How | What you get |
|---|---|
ff record --local-only | A recorded browser journey, written to .fixtureframe/recordings. |
ff media <file...> | Pictures and clips you already have, one step per file. |
| By hand, or by an agent | Anything the schema allows, written directly against it. |
What a step carries
Four fields do the authoring work, and the last two are deliberately not the same text:
| Field | What it is |
|---|---|
name | The step's title, shown in the storyboard and over the scene. |
narration.text | What the voice says. A script, so a heading or an emoji in it is read out loud. |
card.title | One line above the card body. Plain text. |
card.body | A small markdown subset: **bold**, *italic*, `code`, links, paragraphs. |
Omit the card and the card shows the narration, which is what a step with one line to say wants.
Card links must be absolute http(s) or mailto — a published demo is embedded cross-origin on
someone else's page, so a relative link would resolve against the host rather than your product.
Assets can be local files
An asset is normally addressed by URL, because everything that renders one is somewhere else: the composer, the video worker, a browser opening a published demo. While you are authoring, that is backwards — the pictures are sitting next to the file you are writing.
So an asset may name a path instead:
{
"assets": [
{
"id": "overview",
"kind": "image",
"source": "runner_file",
"path": "shots/01-overview.png",
"alt_text": "The overview"
}
],
"steps": [
{
"id": "overview",
"name": "Where the balance lives",
"action": { "type": "show_asset", "asset_ref": "overview" },
"capture": { "mode": "none" },
"narration": {
"text": "Every wallet opens on the balance, so the number you came for is the first thing you see."
},
"card": { "title": "One balance, always first", "body": "The **available** balance, not the ledger one." }
}
]
}ff publish uploads each local file to your organisation's library and saves the asset as
studio_library with a URL. The file on disk keeps its paths — that is what makes it
re-publishable and reviewable — while the flow the control plane holds depends on nothing local.
Supported: .png, .jpg, .webp, .svg, .mp4, .webm, and .mp3 / .wav for music beds and
recorded takes.
Three rules, and why
- Paths resolve against the flow file, not the working directory, so
shots/01.pngmeans the same thing wherever the command is typed.--assets-dirmoves the base for a repository that keeps pictures somewhere central. - A path may not leave that folder, and absolute paths are refused. Publishing reads a file and
uploads it, so a flow file arriving in a pull request must not be able to name a private key and
have a reviewer's
publishsend it. An absolute path is also not portable, which is the plainer half of the reason. kindis checked against the file. It is what the renderer reads to choose between looping a still and playing footage, so a.mp4declaredimagewould render its first frame for the whole scene and throw the rest away.
Saving, and saving again
ff publish flow.json # creates the flow and version 1
ff publish flow.json --flow "Wallet at a glance" # saves the next version of an existing flowWithout --flow a new flow is created. With it, the file becomes the next version of one that
already exists — which is what editing a checked-in definition and saving it again means, and what
creating a second flow called "Wallet at a glance (2)" plainly does not. The version number is read
from the control plane rather than assumed.
Nothing invalid is ever sent: the file is checked against the canonical schema first, and the errors are printed in full.
When you want the paths kept
--keep-paths saves local paths as written, without uploading. That is for a flow deliberately
staged per run — ff run --assets-dir copies runner_file assets into the run and uploads
them as its artefacts, so the file on the runner is the source of truth. Any other flow saved this
way will render blank everywhere but the machine holding the pictures.