Start here — new to ChatMapper
This section assumes no prior ChatMapper knowledge. It is written for the person who will either wire ChatMapper output into an engine or hand the editor to a writing team and then consume what comes out.
If your studio already has Desktop ChatMapper projects, read Migrating from ChatMapper Desktop instead.
1. The data model
Section titled “1. The data model”A project is a single JSON document with five asset collections:
{ "Language": "en-US", "Title": "The Ferry at Dusk", "Version": "1.5.1.0", "Author": "", "Description": "", "UserScript": "", "Assets": { "Actors": [], "Items": [], "Locations": [], "Conversations": [], "UserVariables": [] }}UserScript is Lua run once at the start of a playthrough, after the asset
tables are built.
Assets
Section titled “Assets”Actors, Items, Locations and UserVariables share one shape:
{ "ID": 1, "Fields": { "Name": "Ferryman" } }IDis unique within its own collection.Fieldsmaps a field title to a value, and every value is a string —"True","30","[]"— regardless of the field’s declared type. Coerce at the boundary of your code, once.
Conventional field titles:
| Collection | Fields |
|---|---|
| Actors | Name, IsPlayer ("True"/"False"), Description, Pictures, Age, Gender |
| Items | Name, Description, Pictures, Purpose, Scene |
| Locations | Name, Description, Pictures |
| UserVariables | Name, Initial Value, Description |
Conversations and nodes
Section titled “Conversations and nodes”A conversation is a tree (strictly, a directed graph — cycles are legal and common) of dialogue nodes:
{ "ID": 1, "NodeColor": "White", "Fields": { "Title": "Asking for passage", "Actor": "1", "Conversant": "2" }, "DialogNodes": []}Actor and Conversant hold a stringified actor ID ("2", not
"Ferryman"). "" and "-1" both mean unset. Exactly one node must have
IsRoot: true; the root is a start marker and is never spoken, even if it
carries text.
A node carries ID, ConversationID, IsRoot, IsGroup, ConditionsString,
UserScript, NodeColor, DelaySimStatus, FalseConditionAction,
ConditionPriority, OutgoingLinks, and a Fields object holding Title,
Actor, Conversant, Menu Text, Dialogue Text, Parenthetical,
Audio Files, Video File, Animation Files, Pictures — plus anything else
the project adds.
The distinction that matters: Dialogue Text is what is spoken, Menu Text
is what appears on a choice button.
Links live on the origin node and name their destination by conversation and
node ID. A Filename field on a link points at another project file — most
runtimes, including Unity’s, cannot follow those, because they load one combined
database.
2. Traversal semantics
Section titled “2. Traversal semantics”This is the part you must get right. Given a node, what is offered next:
- Collect every link destination.
- Filter by condition. A destination whose
ConditionsStringevaluates false is dropped — unless itsFalseConditionActionisPassthrough, in which case its own children join the candidate pool in its place. - Suppress by priority. Only candidates at the best (numerically lowest)
ConditionPrioritypresent survive. - Expand groups. A surviving node with
IsGroup: trueis replaced by its own children, run through the same three steps. The group’sUserScriptruns before the chosen child’s.
Then:
- No survivors → end of conversation.
- Survivors without
Menu Text→ play immediately (a line, not a choice). - Exactly one survivor with
Menu Text, not prefixed[f]→ play it straight through, no menu. - Otherwise → present a menu.
3. The Lua host API
Section titled “3. The Lua host API”ConditionsString and UserScript are Lua. A conforming host exposes:
| Binding | Meaning |
|---|---|
Variable["Name"] | User variables. |
Actor["Name"], Item["Name"], Location["Name"] | Asset field tables. |
Conversation[id], Conversation[id].Dialog[id] | With a SimStatus of "Untouched" / "WasOffered" / "WasDisplayed". |
Dialog[id] | Shorthand for the evaluating node’s own conversation. |
Names are cleaned before use as Lua keys: " [ ] . \ / are
removed, and -, spaces and newlines become _. So the field Menu Text is
Menu_Text, and an actor named Harbour Master is Actor["Harbour_Master"].
Two assets whose names clean to the same key collide, and some hosts — Unity’s Dialogue System among them — let the second silently overwrite the first. The editor’s Unity preflight reports this as an error.
A condition that cannot be evaluated should be treated as true and logged,
not as false. Failing open keeps content reachable.
4. Inline markup
Section titled “4. Inline markup”Markup is part of the stored text. A consumer that cannot honour a tag should strip it for display rather than rewriting the value.
| Markup | Meaning |
|---|---|
| | Sentence split — one node, successive lines. |
[f] | On Menu Text: force a menu even for a single option. |
[a] | On Menu Text: an action line rather than a spoken one. |
[var=Name] | Substituted with the variable’s value at display time. |
[?Name], [x=?Name] | Prompt for player input, store in Name, consume the token from the line. |
[pic=N], [pica=N], [picc=N] | Swap the displayed image mid-line. |
[em1]…[/em1] … [em4] | Author emphasis spans; colour/style/label configured per project. |
5. Extending the format without forking it
Section titled “5. Extending the format without forking it”Three rules, and they are load-bearing for the whole ecosystem:
- Custom fields. Add whatever you need to any
Fieldsobject. Consumers that do not recognise a field must preserve it, not drop it. This is how the LearnBrite runtime fields, Unity’sSequencefield and every importer’s*_rawprovenance fields work. _editor. One top-level object for editor-only metadata — canvas positions, reviewer notes, field type hints, collapsed state. It is stripped from every export and any consumer may ignore it. Never put anything there that the project’s meaning depends on.- Graceful degradation. A consumer that does not understand an extension must still produce a working project without it.
What this rules out: renaming canonical keys, changing a field value away from
string, or adding a meaningful top-level sibling to Assets.
For your own engine, the pattern is: define your fields (Sequence,
cutscene_id, vo_take, whatever), add them as a custom field template in
the editor so writers get them on every node, and read them in your importer.
No schema change, no fork, and the project still opens in every other
ChatMapper tool.
6. Working in the editor
Section titled “6. Working in the editor”Sign in at app.chatmapper.com. Your first sign-in creates a workspace and offers three starts: the working example, a blank project, or an import.
The parts that matter to a developer:
- Canvas. Drag-and-connect, right-click menus, minimap, collapse/expand subtrees, auto-layout (left-to-right or top-to-bottom), and a structured layout mode that keeps the graph tidy without manual placement.
- Keyboard tree building. Tab for a child, Enter for a sibling, Home for the root, arrows to move, Ctrl+↑/↓ to reorder siblings (menu order), double-click to edit text in place. Project-wide find and replace on Ctrl+F / Ctrl+H.
- Inspector. All node fields including conditions and user scripts, custom
fields with per-field types and an export this field flag, node colour,
group flag,
DelaySimStatus,FalseConditionAction,ConditionPriority. - Simulator. Plays a conversation with real Lua (a WebAssembly Lua runtime, with a subset interpreter as fallback). Step history — start, back, forward, end — plus a Lua console to evaluate expressions and assign variables against the live run state. This is a step debugger for narrative logic, and it is the single biggest reason writers stop filing “the branch doesn’t fire” bugs.
- Validator. Duplicate IDs, unreachable nodes, orphaned and broken links, undefined variables, cross-conversation link integrity, spoken lines with no actor, plus per-target preflights.
- History and collaboration. Autosave, revision snapshots with diff and restore, live presence, optimistic-concurrency conflict resolution, reviewer notes with a review status per node, and Owner/Admin/Editor/Viewer roles.
- Localisation. Add languages and each translatable field gains a per-language version; a toolbar switch changes what canvas, inspector and simulator display.
7. Getting data out
Section titled “7. Getting data out”Export ▾ groups the formats by what you would do with them.
Unity — Pixel Crushers Dialogue System
Section titled “Unity — Pixel Crushers Dialogue System”Dialogue System imports ChatMapper XML natively (Tools → Pixel Crushers → Dialogue System → Import → Chat Mapper). There is no separate file format: the Unity — Dialogue System XML export emits exactly the same bytes as the plain XML export, and adds a preflight for the things that import cleanly but then misbehave:
- Error: two assets whose names clean to the same Lua key (
Guardandguard) — Dialogue System keys its Lua tables by cleaned name, and one silently overwrites the other. - Warning: an asset with no
Name. - Warning: an Item with no
Is Itemfield. ChatMapper keeps items and quests in one collection;Is Itemis how Dialogue System tells them apart. - Warning: a
|inside a bracketed tag — Dialogue System splits lines on|and will tear the tag in half. - Warning: a link with a
Filename(cross-project link) — unsupported there. - Warning: a condition that does not parse as the supported Lua subset, so you know to verify it in Unity.
- Summary warning: how many dialogue nodes have no
Sequencefield.
Apply the built-in “Unity — Dialogue System” custom field template to add
Sequence and Response Menu Sequence to dialogue nodes and Is Item to
items, so your writers fill them in as they go.
Yarn Spinner
Section titled “Yarn Spinner”Exports a .yarn file for the project: dialogue chains become lines, child
nodes with menu text become -> options, links become jumps, variables are
declared, and the translatable subset of the Lua becomes <<if>> / <<set>>.
Lua that cannot be translated is emitted as a comment and reported in the export
warnings rather than silently dropped.
Your own runtime
Section titled “Your own runtime”Export ChatMapper JSON and read it. The format is specified above; the repo
also ships docs/format/chatmapper-json.md (normative), a complete fictional
example-scenario.json covered by a test so it cannot rot, and
reference-parser.ts — a single dependency-free, MIT-licensed TypeScript file
that parses and walks the format and compiles standalone under tsc --strict.
Start there rather than from scratch.
Standalone HTML player
Section titled “Standalone HTML player”One self-contained .html file — embedded scenario, embedded player, inline
CSS, no external requests, runs from file://. It records the path taken and
produces an end-of-run summary (nodes visited, endings reached, final variable
state). Useful for playtest builds, stakeholder review and QA links long before
any engine work exists.
SCORM 1.2 and xAPI
Section titled “SCORM 1.2 and xAPI”A zip of the HTML player plus imsmanifest.xml as a single SCO. On completion
it reports cmi.core.lesson_status and cmi.core.score.raw. Scoring uses a
numeric variable — one you nominate, else a project variable literally named
score — normalised to its declared min/max; failing that it falls back to
“reached an ending” or proportion of nodes visited, and tells you which rule it
used. The xAPI variant adds an emitter: initialized, one responded per
choice, completed with score.
Production and review
Section titled “Production and review”Voiceover Script CSV (one row per spoken line, markup stripped — the recording sheet), Proofreading Text, Screenplay RTF, Assets CSV, Dialogue CSV, an everything CSV, Reviewer Notes CSV, and a Conversation SVG for design docs.
Back to the Desktop app
Section titled “Back to the Desktop app”CMPKG + media packages the project XML and JSON together with every attached media file. Plain ChatMapper XML opens directly in the Windows app.
8. Getting data in
Section titled “8. Getting data in”Import accepts:
| Source | Notes |
|---|---|
.cmp, .cmpkg | Desktop ChatMapper’s native (encrypted) files, decrypted server-side. |
.xml | Desktop XML. |
.json | Canonical ChatMapper JSON. |
CSV / TSV / .xlsx | Forgiving spreadsheet import: id, actor, text, menu, parent, goto, condition, script, conversation (aliases accepted, order-free). Missing id/parent infers linear chains from row order. |
| Twine | Twee 3 source or a Twine 2 HTML story/archive; passages become nodes, [[links]] become links. |
| Arcweave | JSON project export; boards become conversations, elements become nodes, connections become links. |
| articy:draft X | JSON export; entities become actors, dialogues become conversations, fragments become nodes, hubs become group nodes, jumps become links, global variables become variables. |
Every importer produces a report before anything is applied: counts,
warnings, and a lossy list of constructs that could not be mapped faithfully.
Nothing is dropped silently — untranslatable markup and scripts are preserved
verbatim in provenance custom fields (twine_markup_raw, arcweave_script_raw,
articy_script_raw, articy_id, …) so a human can finish the job.
9. A realistic first integration
Section titled “9. A realistic first integration”- Import or author a small conversation.
- Validate, then Simulate it — confirm the branching logic is what you think it is.
- Add your engine’s fields as a custom field template.
- Export to your target and import it in the engine.
- Write the pipeline step. For Unity that is the Dialogue System importer; for a custom runtime, start from the reference parser and implement §2’s traversal rules against your own presentation layer.
- Hand the editor to the writers. Give reviewers the Viewer role and the standalone HTML player.
- Migrating from ChatMapper Desktop — if your
team has existing
.cmp/XML projects and a pipeline around them. - LearnBrite authors manual — if the same scenarios also need to run in a 3D LearnBrite space.