Fixtureframedocs
Concepts

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:

HowWhat you get
ff record --local-onlyA 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 agentAnything 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:

FieldWhat it is
nameThe step's title, shown in the storyboard and over the scene.
narration.textWhat the voice says. A script, so a heading or an emoji in it is read out loud.
card.titleOne line above the card body. Plain text.
card.bodyA 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.png means the same thing wherever the command is typed. --assets-dir moves 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 publish send it. An absolute path is also not portable, which is the plainer half of the reason.
  • kind is checked against the file. It is what the renderer reads to choose between looping a still and playing footage, so a .mp4 declared image would 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 flow

Without --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.

On this page