Originally published on the ReelFork blog.
Most interactive-video tooling keeps the story structure locked inside a proprietary editor. If you want to know what your branching drama actually looks like, you open the app and squint at a canvas. You can't diff it. You can't review it. You certainly can't generate it from a script.
We went the other way. In ReelFork, the story is a YAML file — readable, diffable, and perfectly happy in Git. It's called RFD (ReelFork Drama), and this post walks through the whole format.
Why a text format at all
Three reasons, all of them boring and all of them the point:
-
Diffable.
git diffon a story file shows you exactly which choice changed. Try that with a binary project file. - Generatable. An LLM can emit valid RFD directly. Our story agent does exactly this — it writes YAML, not clicks.
- Portable. The canvas is one view of the file, not the source of truth. Author in the editor, in a text file, or from a script, and you end up in the same place.
The minimum viable file
A valid RFD file needs four things: a format version, a title, a cover, and a list of nodes.
rfd: "1.0"
title: "Night Shift"
cover: "cover.jpg"
nodes:
- id: scene_01
video: "clips/01.mp4"
next: scene_02
That's already playable. It's a one-scene drama that ends after scene_02, which is not much of a drama — but it validates and it runs.
Nodes are scenes
Every node is one video clip plus instructions for what happens when it ends.
- id: scene_01 # unique within the file
video: "clips/01.mp4" # path relative to the package root
next: scene_02 # linear continuation
next is the linear case. Replace it with choices and the node becomes a fork.
Choices are edges
- id: scene_01
video: "clips/01.mp4"
choices:
- ["Help the stranger", scene_help]
- ["Keep walking", scene_ignore]
Each entry is [label, target_node_id]. The player freezes on the clip's last frame, renders the labels as buttons, and jumps to the target the viewer picks. Two options is the common case; three is fine; four starts to feel like a form rather than a story.
Conditions and effects: state across the whole drama
This is where it stops being a flowchart. effects writes to a variable when a branch is taken. if gates whether a node or a choice is available at all.
- id: scene_help
video: "clips/help.mp4"
effects:
trust: +1
next: act_two
- id: act_two
video: "clips/act2.mp4"
choices:
- ["Ask him for the key", scene_key]
if: trust >= 1
- ["Break the door", scene_force]
Variables persist for the whole watch session, which is how a decision in episode one can open — or permanently close — a route in episode three.
The practical advice: keep the variable count small. Two or three named flags (trust, has_key, told_truth) cover almost every story you'd actually shoot. A dozen booleans produces a state space you cannot test.
Text game nodes: visual-novel scenes without video
RFD also supports a textgame node type for visual-novel style scenes: background art, character sprites, typewriter dialogue, music, and voice — all rendered live by the player, with no video file involved. A text game node declares type: textgame and carries a compiled textgame payload (schema: reelfork.textgame) plus a next field for its exit.
A published example shows the economics: in the wuxia drama "Three Invitations at the Ferry", the 54-line centerpiece — three characters arriving one by one, sprites trading center stage, backgrounds cutting between wide and close views — is a single text game node that compiles to about 60KB, less than one second of video. One branch even ends in a second text game node with no exit at all: a text-only ending. The choice buttons render over the scene's lingering final frame.
Unlike the rest of RFD, the payload is not meant to be hand-written. You author text game scenes in the ReelFork Studio story tree with the built-in Text Game editor; publishing compiles the scene into the RFD automatically. Hand-crafted payloads are rejected at upload validation.
What ships in a package
An RFD package is a plain zip: one YAML file plus every asset it references, with paths resolved relative to the package root.
Validation happens on upload
Uploading runs the file through a validator before anything is published. It checks:
- the YAML parses and
rfdis a supported version - every
videopath resolves to a file that's actually in the package - every
nextand every choice target points at a node that exists - the start node is reachable and at least one ending is reachable from it
- no node is orphaned
Failures come back as a list with node IDs, not a generic "upload failed". The point is to catch a broken reference at upload time rather than when a viewer hits a dead end.
Why you might care even if you never use ReelFork
The interesting idea here isn't our specific keys. It's that branching video is a graph, and graphs belong in text. Once the story is a file:
- code review works on narrative structure
- CI can validate every path is reachable on every commit
- a script can generate 40 localized variants from one source
- an LLM can author the structure and a human can edit the diff
If you're building anything with branching media, steal the shape of this even if you don't use the format.
Full reference and the rest of the spec: The RFD format on the ReelFork blog. If you want to see a finished branching piece before reading a spec, there's a worked example here.



Top comments (0)