← chapterscript.com

The format reference

Everything a tool needs to read or write ChapterScript, in one page. The normative source of truth is the ChapterScript Swift package: its Codable types define the schema, and its test fixtures are working documents.

The bundle

A project is a .chapterscript directory bundle:

MyStory.chapterscript/
├── chapter.json      the entire document
└── assets/           your media, byte for byte as imported
    ├── intro.mp4
    ├── ambience.m4a
    └── lantern.usdz

chapter.json describes everything a player needs. Files under assets/ are referenced through the manifest by relative path; the directory layout under assets/ is up to the author. Copy the folder and you have copied the project.

chapter.json, top to bottom

The document (format version 3) opens with identity, then the main collections:

KeyWhat it is
formatVersionSingle integer schema version, currently 3. Drives migration on open.
idStable experience identifier.
displayNameHuman-readable title. An optional description may sit alongside it.
defaultSequenceIdThe sequence to play first.
sequencesThe timed scenes of the chapter (detailed below).
entitiesDefinitions for everything placeable: primitives, USDZ models, 3D text, lights, video panels, particle emitter bindings, plus a custom-factory escape hatch.
particlePresetsReusable emitter recipes referenced by emitter entities.
manifestOne entry per asset file: relative path, byte size, SHA-256 hash, plus probed duration and dimensions for media.

Sequences

A chapter contains sequences; players run one sequence at a time. Each sequence carries:

Backdrops

The backdrop is a kind-tagged object:

{ "kind": "video", "file": "shore_360.mov", "layout": "mono",
  "field": "equirect360", "radius": 1000, "loop": true, "audioEnabled": false }

{ "kind": "image", "file": "dusk.heic", "field": "equirect360", "radius": 1000 }

{ "kind": "usdz", "assetId": "forest_scene" }

Steps and gates

A step is a timed beat:

{
  "id": "step_2", "name": "The lantern", "duration": 10.0,
  "gate": { "type": "tap", "timeout": 30, "prompt": "Tap the lantern" },
  "actions": [ … ],
  "scheduledActions": [ { "at": 2.0, "action": { … } } ]
}

The action catalog

Actions are externally tagged: every action object carries a "kind" plus a case-specific payload, either inline keys or a payload object ("reveal", "move", "video", "audio", "config", and so on):

{ "kind": "revealEntity", "reveal": { "entity": "lantern", "fadeIn": 1.5 } }
{ "kind": "stopAudio", "channel": "narration" }
GroupKinds
EntityshowEntity · hideEntity · revealEntity · moveEntity · scaleEntity · fadeEntity · persistEntity · unpersistEntity · animateMotion
AttachmentsshowAttachment · hideAttachment · fadeAttachment · setAttachmentView · positionAttachment
AudioplayAudio · stopAudio · fadeAudio · onAudioComplete (nests follow-up actions)
Audio mixsetMasterVolume · setCategoryVolume · setBusVolume · setBusEffect · removeBusEffect
Audio zonesaddAudioZone · removeAudioZone · removeAllAudioZones
VideoplayVideo · prepareVideo · stopVideo
EffectsshowPulseRing · hidePulseRing · startSparkBurst · stopSparkBurst
GesturesenableGesture · disableGesture
SystemsetUpperLimbVisibility · setKeyboardPassthrough
Escape hatchcustom: an opaque id plus a free-form JSON parameter blob, handled by app-registered factories.

Field-level payload documentation lives with the types in the package source, and the representative.json fixture exercises 30+ kinds as real JSON.

Video payloads

Animation tracks

Keyframe animation lives on the sequence: one track per animated entity, up to ten scalar channels (tx ty tz, rx ry rz, sx sy sz, opacity). Keys sit at absolute seconds from sequence start:

{
  "entity": "lantern",
  "rotateOrder": "xyz",
  "channels": {
    "ty": [
      { "t": 0.0, "v": 1.0, "interp": "bezier",
        "inTangent":  { "dt": -0.4, "dv": 0.0 },
        "outTangent": { "dt":  0.4, "dv": 0.0 } },
      { "t": 4.0, "v": 1.6, "interp": "bezier" }
    ]
  }
}

The asset manifest

{ "entries": [
  { "id": "intro.mp4", "relativePath": "intro.mp4",
    "byteSize": 48211930, "sha256": "9f31ab…" }
] }

Hashes let players cache aggressively and never show a stale frame; byte sizes let them budget transfers. Probed media metadata (duration, dimensions) rides along where it applies.

Versioning, migration, forward compat

Pre-1.0: the schema may still change. The first stable release will lock the wire format and start the strict back-compat regime.