Skip to content

A branching story is a graph, and the media stays on the writer's own disk

A desktop editor where scenes are nodes and choices are edges, so a writer can see the shape of a branching story, and where audio and images never leave the machine they were recorded on.

ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
Scene100% Start Ending One handle per choiceOpen while draftingPlaytest from S01
S01Night Shift
Answer the radio
Log it and wait
S02The Caller
Ask who's calling
Switch off the set
S03Tom at the Door
Let him in
Read his logbook
S04Coordinates
Write them down
S05Alone with the Static
Go back to the radio
Open the door
S06The Pump House
Take the key
Open the hatch
S07Below the Waterline
Call the harbor
Seal the valve
S08Tom's Logbook
No choices
Dead end
S09Morning Boat
Ending
S10The Lamp Relit
Ending
S11Second Caller
Follow the voice
Unreachable
No scene yettarget: null
Map Layout saved in saltmarsh-signal.story
Structure, as drawn2 endings1 dead end · S081 unreachable · S112 loops back1 open choice
ChoiceS06 · choice 2Draft
FromS06The Pump House
LabelOpen the hatch
Requireshas_coordinates
TargetNo scene yet
Create scene herePick a scene
A target is a scene that exists, or nothing yet. Export stops on nothing yet.
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded

Who, what, and how long

Industry

Media & Publishing

Duration

12 weeks

Cooperation model

Fixed price

Services
Desktop engineeringAuthoring toolsExport pipeline
Integrations
Local file system accessAudio playbackApplication auto-updateCrash reporting
Technologies
ElectronNext.jsTypeScriptReact FlowZod
Team
1 Project lead2 Full-stack engineers1 Product designer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

The story is a canvas

Scenes are nodes and choices are edges, so the shape of the branching is on screen for the writer to see.

React Flow renders the story as a graph (scenes as nodes, choices as edges), so the shape of the branching is something a writer looks at instead of reconstructing from a document's headings. Layout is persisted, so a story arranged to make sense stays arranged. Dead ends, unreachable scenes and loops become visible as shapes, and those were exactly the structural problems being found late.

What shipped
  • Scenes as nodes, choices as edges, on a persisted layout
  • Dead ends and unreachable scenes visible as shapes
  • Structure looked at, not reconstructed from headings
ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
Scene66%Lenstrusts_tomset in 1 · read in 3
S01Night Shift
Answer the radio
Log it and wait
S02The Caller
Ask who's calling
Switch off the set
S03Tom at the Door
Let him in
Read his logbook
Sets
S04Coordinates
Write them down
S05Alone with the Static
Go back to the radio
Open the door
S06The Pump House
Take the key
Open the hatch
Reads
S07Below the Waterline
Call the harbor
Seal the valve
Reads
S08Tom's Logbook
No choices
Dead end
S09Morning Boat
Ending
S10The Lamp Relit
Ending
Reads
S11Second Caller
Follow the voice
Unreachable
No scene yettarget: null
Structure, visible as shapes11 scenes · 14 choices
Endings2S09Morning BoatS10The Lamp Relit
Dead ends1S08Tom's LogbookNo choices, not an ending
Unreachable1S11Second CallerNo choice leads here
Loops back2S05 → S02 The CallerS05 → S03 Tom at the Door
Layout is part of the storyWhere a writer put a scene is saved in saltmarsh-signal.story, so an arranged story reopens arranged.
"layout": {
  "S05": { "x": 430, "y": 150 },
  "S06": { "x": 638, "y": 220 },
  "S07": { "x": 846, "y": 120 },
  "zoom": 0.66 }
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded
On screen

Every flag the story sets, and every scene that reads it: trusts_tom as a lens over the canvas, set in one scene and read in three, above the structure drawn as shapes (two endings, a dead end, an unreachable scene, two loops back) and the layout saved in the story file.

ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
Scene100% Start Ending One handle per choiceOpen while drafting
S01Night Shift
Answer the radio
Log it and wait
S02The Caller
Ask who's calling
Switch off the set
S03Tom at the Door
Let him in
Read his logbook
S04Coordinates
Write them down
S05Alone with the Static
Go back to the radio
Open the door
S06The Pump House
Take the key
Open the hatch
S07Below the Waterline
Call the harbor
Seal the valve
S08Tom's Logbook
No choices
Dead end
S09Morning Boat
Ending
S10The Lamp Relit
Ending
S11Second Caller
Follow the voice
Unreachable
No scene yettarget: null
DeleteS06The Pump House6 lines · 6 audio
Two choices lead to this scene. A choice holds a reference to its scene, so it goes with the scene or the scene stays. Nothing is left pointing at a scene that is gone.2 choices lead here · deleted with it
S03Tom at the DoorLet him in
S04CoordinatesWrite them downS04 is left with no choices
Its own 2 choices · deleted with it
Take the keyS07Below the Waterline
Open the hatchNo scene yet
Not offered
Keep the 2 choices, pointing at S06. A choice can only hold a scene that exists, or nothing yet. Renaming never breaks one: the id stays S06.
Choices point at ids, not titlesKeep the scene Delete scene and 2 choices
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded
On screen

Deleting The Pump House while two choices lead to it: the dialog lists both, deletes them with the scene or keeps the scene, and has no option to leave a choice pointing at a scene that is gone.

Dangling choices are impossible

An edge can't point at a scene that doesn't exist, which removes the entire class of error they were finding in the studio.

An edge is a reference to a scene, so it can't point at one that doesn't exist: deleting a scene either takes its incoming choices with it or is refused with them listed. The entire class of error they had been finding in the studio, a choice leading nowhere, simply can't be represented, so there's nothing to validate.

What shipped
  • Edges are references; a dangling choice can't be created
  • Deleting a scene handles or refuses its incoming edges
  • The studio's most common defect is now unrepresentable

Dialogue is structured

Speaker and line are fields in an ordered list, never prose somebody has to parse at export.

Dialogue is an ordered list of speaker-and-line records, with no naming convention to follow, so the export writes structure instead of parsing it back out of formatting. That also makes a speaker rename a single operation across a whole story, and it means a line can carry its own audio reference without a separate cue sheet.

What shipped
  • Speaker and line as fields in an ordered list
  • Export writes structure, with no prose to re-parse
  • A speaker rename is one operation across the story
ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
S06SceneCanvas position 638, 220
The Pump House
images/S06_pump_house.pngOn disk· reached fromS03Tom at the DoorS04Coordinates
Dialogue6 lines · speaker and line, in orderLine
1NarratorThe pump house smells of diesel and cold iron. The floor hums.S06_L01_narrator.wav0:09
2Ines VargaThere's water under the grate. It wasn't there an hour ago.S06_L02_ines.wav0:06
3Tom HaleKey's on the nail by the gauge. Don't touch the hatch.S06_L03_tom.wavonly if trusts_tom
4Ines VargaWhoever was on the radio said the hatch. Twice.S06_L04_ines.wav0:05
5NarratorBelow the grate, something knocks. Three times, then nothing.S06_L05_narrator.wav0:08
6Ines VargaOkay. Okay. Think.S06_L06_ines.wav0:03
Add a line · drag a row to reorder
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded
On screen

Inside a scene: six ordered speaker-and-line rows, each with its own audio file and one that plays only if a flag is set, the scene's two choices, and a speaker rename that follows twelve lines across six scenes.

ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
Audio for line 2S06The Pump House Ines Varga
“There's water under the grate. It wasn't there an hour ago.”
/Volumes/Recordings/Saltmarsh Signal/audio/S06/S06_L02_ines.wavChoose file…
0:06
WAV · 48 kHz · 24-bit · mono0:060.86 MBRead in placeNot copied · not uploaded
Media in S06 · paths, not copiesMissing files are named by path at export
LineFileSpeakerLengthSizeStatus
1S06_L01_narrator.wavNarrator0:091.30 MBOn disk
2S06_L02_ines.wavInes Varga0:060.86 MBOn disk
3S06_L03_tom.wavTom Hale0:050.72 MBOn disk
4S06_L04_ines.wavInes Varga0:050.72 MBOn disk
5S06_L05_narrator.wavNarrator0:081.15 MBOn disk
6S06_L06_ines.wavInes Varga0:030.43 MBOn disk
images/S06_pump_house.pngBackdrop——On disk
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded
On screen

A local audio attachment: a line's recording read in place from the studio's drive, the scene's files listed by path, and the story file holding forty-four audio and nine image references with nothing uploaded.

Media never uploads

Audio and images are local file references, which is the reason this is a desktop tool.

Audio and images are local file references. Nothing uploads, which is precisely why this is a desktop tool: the studio's unreleased voice recordings aren't going through anyone's server, and a browser can't hold a stable reference to a file on disk across sessions. Missing files are reported by path at export, never silently omitted.

What shipped
  • Local file references; nothing leaves the machine
  • The reason it's desktop, not web
  • Missing media reported by path at export, never dropped
ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
Validate before exportEvery scene of Saltmarsh Signal is checked against the export schema. Drafts are never checked on load.
11 scenes checked2 errors
Required fields present11 / 11
Every choice resolves to a scene13 / 14
Every media reference on disk52 / 53
Holding the bundle back
Choice has no sceneS06The Pump House
scenes[5].choices[1].target· Expected a scene id, received null
“Open the hatch” was left open while drafting. It is drawn on the canvas as No scene yet.Show on canvasPoint it at S07 · Below the Waterline
Media not on diskS07Below the Waterline
scenes[6].lines[2].audio· File not found
/Volumes/Recordings/Saltmarsh Signal/audio/S07/S07_L03_tom.wavShow on canvasRelink to retakes/S07_L03_tom.wav
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded

Export validates first

On screen

Validation before export, with two errors holding the bundle back: a choice with no scene yet and an audio file missing from disk, each naming its scene and schema path, and no file written.

A story that would break in playback produces an error at the desk instead of a file.

Export validates every scene against a Zod schema before a file is written (required fields present, every edge resolvable, every media reference on disk) and fails at the desk with the offending scene named. A story that would break in playback never becomes a file, which moves the discovery of a fault from a studio session to a writer's afternoon.

What shipped
  • Zod validation of every scene before a file is written
  • Failures name the offending scene
  • Faults surface at the desk, never in a studio session
Introduction

What we were brought in to do

The studio writes branching audio fiction. We built the tool they write it in: a canvas of scenes and connectors, a dialogue editor inside each scene, local audio and image attachment, and an export step that validates the whole story before it produces anything.

A studio writing branching audio fiction in a word processor, where every choice was a heading reference typed by hand. A story of any size accumulated references pointing at scenes that had been renamed or deleted, and nobody found them until a playthrough hit one. That made finding them a recording session's job, with voice talent in the room.

Full-Stack Engineering

Where the old way broke

They were writing branching stories in a document. Every choice was a heading reference typed by hand, and a story of any size had references pointing at scenes that had been renamed or deleted. Nobody found those until a playthrough hit them, which meant finding them was a recording session's job.

The document format made the structure invisible and the references unchecked. A writer had to hold the branching shape in their head, and a rename in one place silently broke every choice pointing at it. The cost was concentrated in the most expensive hour of the production: a dangling reference discovered in the studio stops a session.

Make the structure visible and make it checked. Scenes are nodes on a canvas, choices are edges between them, and an edge can't point at a scene that doesn't exist. The export validates every scene against a schema before writing a file, so a broken story fails at the desk, long before it reaches the studio.

What we built together

  1. 01

    Modeled scenes as nodes and choices as edges, so the story's shape is the thing on screen

    Scenes are nodes and choices are edges on a persisted canvas, so the branching shape is the thing on screen, with nothing to reconstruct from headings.

  2. 02

    Made a dangling connector impossible, not merely detectable

    Making the error impossible, not just detectable, was the decision the tool turns on, and it removed the validation rule, the error message and the discipline together.

  3. 03

    Validated every scene against a schema at export, not at load

    Each line of dialogue is stored as a speaker-and-line record, which is what lets export emit clean structure.

  4. 04

    Kept audio and images as local files, which is the reason this is a desktop application

    Keeping media on disk also settled the desktop question: no browser can hold a durable handle to a file across sessions, embargo or not.

  5. 05

    Ran the export in the main process, so a large story doesn't freeze the canvas

    Export validates every scene against a schema and fails at the desk with the offending scene named, so no file is produced that would break in playback.

Process

Phase by phase

  1. Phase 1: The Canvas

    Scenes And Connectors

    Built the graph canvas with custom scene nodes and choice edges. Position is saved with the story, because a writer's mental map of their own plot is worth persisting.

    • Scene Nodes
    • Choice Edges
    • Canvas State
  2. Phase 2: Inside A Scene

    Dialogue As A Repeater

    Built the scene editor with dialogue as an ordered repeater (speaker and line, added and reordered) in place of a free-text block that has to be parsed later.

    • Scene Editor
    • Dialogue Repeater
    • Speaker Handling
  3. Phase 3: Media

    It Stays On The Disk

    Built audio and image attachment against local files. This is why the tool is a desktop application: raw recordings are large, unreleased, and belong on the machine that made them.

    • Audio Attachment
    • Image Attachment
    • Local References
  4. Phase 4: Validation And Export

    Fails At The Desk

    Built export in the main process, validating every scene and connector against a schema first. A story that would break in playback doesn't produce a file.

    • Schema Validation
    • Export
    • Error Reporting
  5. Phase 5: The Library

    Several Stories At Once

    Built the story list and drawer, because a studio has four things in progress, and switching between them shouldn't mean opening a different folder.

    • Story List
    • Switching
    • Local Store
ForkloomSaltmarsh Signal1 of 4
Saved to this disk · 4:12 PMValidate & export
The Pump HouseS06Local audio
NarratorThe pump house smells of diesel and cold iron. The floor hums.0:09
Ines VargaThere's water under the grate. It wasn't there an hour ago.0:06
Tom HaleKey's on the nail by the gauge. Don't touch the hatch.skipped: requires trusts_tom which is false on this path—
Ines VargaWhoever was on the radio said the hatch. Twice.
NarratorBelow the grate, something knocks. Three times, then nothing.0:08
Ines VargaOkay. Okay. Think.0:03
Choose
Take the key→ S07 · Below the Waterlinehas_key
Open the hatchNo scene yet: the playtest stops here
Scenes11Choices14Open1Flags5saltmarsh-signal.story53 media files on disk · 0 uploaded
On screen

Playtest: one path through the story, with the state changing as it goes. Four scenes in, one line is skipped because trusts_tom is false, each step shows the flag it set, and the open choice marks where the playtest would stop.

Operational results after launch

No

Dangling references possible

None

Media uploaded

Before export

Validation runs

Zero

Errors found in studio

Dangling references possible is no, structurally: an edge is a reference and can't point at a scene that doesn't exist. Media uploaded is none. Validation runs before export. Errors found in studio is zero across the productions since: the class of fault that used to stop sessions can't reach one.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

Ways of working

About our collaboration

01
  1. 01

    Two of the studio's writers used each build for a week before we moved on. The dialogue repeater exists because of that: our first version was a text block with a convention, and the writers ignored the convention within a day, exactly as they had in the documents.

    Twelve weeks, fixed, with the graph model settled before any editor work: everything else follows from scenes being nodes and choices being edges. Writers used builds on a live production from week five, which is where the decision to validate at export instead of at load came from. A draft in progress has to be allowed to be incomplete.

What it settled

What we'd carry into the next one

04
  1. 01

    If the work is a graph, the tool should be a graph. A document forces the writer to hold the structure in their head.

    When the work is a graph, a document makes the writer the validator: they hold the structure in their head, and the mistakes are the ones memory makes.

  2. 02

    Make the broken state unrepresentable, not just detectable. A dangling edge you can't draw needs no validation rule.

    An unrepresentable broken state needs no validation rule, no error message and no discipline, which is strictly better than detecting it well.

  3. 03

    Validate at export instead of at load, so a work in progress is allowed to be incomplete.

    Validating at load would have made every incomplete draft an error; validating at export lets the work in progress be exactly that.

  4. 04

    Large unreleased media is a good enough reason on its own to build a desktop application.

    Large unreleased media under embargo is on its own a sufficient reason to build a desktop application, before any argument about performance.

Play it, then export it

A draft can be unfinished. A bundle can’t.

Walk four scenes of Saltmarsh Signal and watch the flags change, then run the export check on the same draft and fix the two errors it names. Switch tabs, or use the arrow keys once one is focused.

The playtest starts at the first scene with every flag false. Take the highlighted choice to walk the path.

Step 1 of 4S01Night Shift
  1. NarratorTwo a.m. at the Saltmarsh relay station. The kettle has gone cold.
  2. Ines VargaChannel sixteen. Nobody should be on sixteen tonight.
  3. The Caller…anyone… relay station… can you hear me…
Choose
Log it and wait→ S03 · Tom at the Door, another path
Story state on entering
  • radio_answeredInes picked up channel sixteenfalse
  • heard_signalThe Caller's warning was heard outfalse
  • has_coordinatesGrid seven is written downfalse
  • trusts_tomTom was let infalse
  • has_keyThe pump-house key is takenfalse

Why the check waits for export: a draft in progress has to be allowed to be incomplete, so an open choice is fine on the canvas and refused only when a file would be written.

Architecture

From a connector on the canvas to a bundle that plays

The graph rules out a dangling choice as it's drawn. The export catches what a draft is allowed to leave unfinished, before a file exists, so the fault surfaces at a writer’s desk instead of in a recording session.

  1. 01 · Source
    Scene nodes and choice edges on the canvasThe branching shape is the thing on screen, and the layout a writer arranged is saved with the story.
  2. 02 · Model
    Story graph: a choice is a referenceAn edge can't point at a scene that doesn't exist. Deleting a scene takes its incoming choices or is refused with them listed.
  3. 03 · State
    Local story file and media on diskAudio and images are local file references. Nothing uploads, and dialogue is stored as speaker-and-line records.
  4. 04 · Engine
    Zod validation in the main processEvery scene is checked at export, never at load: required fields, every edge resolvable, every media file on disk.
  5. 05 · Delivery
    Bundle written for the playerWritten only when validation passes. A failure names the offending scene, and no file is produced.
Stays on the studio’s machine The gate: no file until every scene passes
What can't reach a recording session

Broken paths, lost media and failed bundles, stopped at the desk

A broken path can't be drawnA choice is a reference to a scene, so it can't point at one that doesn't exist. Deleting a scene takes its incoming choices with it or is refused with them listed, and a choice still being drafted shows as an explicit No scene yet.
Recordings stay where they were madeAudio and images are local file references. Nothing is uploaded, so unreleased voice recordings never pass through anyone's server. A missing file is reported by its path at export, never silently dropped.
A bundle that would break never existsExport validates every scene against a schema before writing: required fields present, every edge resolvable, every media reference on disk. A failure names the scene and produces no file.

Building an authoring tool where a broken story can’t reach the studio? Scope your build in 3 minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.