Skip to content

Write thegamebook.Lute proves it.

Marina looks up from the ramen counter.‘Oh! Mr. Fixer!’

  1. If you ask flatly turn to 2
  2. If you ask gently turn to 3
Get started
Playground

One command, from any repo

The language

You ask flatly. “Marina. Your number.” She writes it down, and you think: I am, apparently, taking that woman somewhere this evening.

Scenes read like scripts. Section 1 is this .lute document: dialogue, a <branch> of choices, and a <match> that calls back to what you chose. <match> is first-match-wins with a mandatory <otherwise>, so every branch is total — the branch always gets the number; asking gently just warms her.

scene.lute
---
kind: scene
character: marina
season: 1
episode: 2
title: The Hostess with a Name
pov: fixer
state:
scene.affect.marina: { type: int, default: 0 }
enums:
emotion: [neutral, surprised, delighted, shy, content, angry, sad]
framing: [close, tight]
defs:
fond: { type: bool, cel: "scene.affect.marina >= 1" }
---
## The Hostess with a Name
::camera{focus="marina" framing="close" duration="0.5"}
@marina{emotion="surprised"}: Oh!
@marina{emotion="delighted"}: Mr. Fixer!
@marina{emotion="delighted"}: From the ramen counter — you eat at Mechageki too, isn't it wonderful?
/* a choice of TONE — both paths get the number; the gentle ask warms her */
<branch id="number">
<choice id="blunt" text="Just ask, flatly">
@fixer: Marina. Your number.
</choice>
<choice id="soft" text="Ask gently">
@fixer: Marina — would you mind terribly if I had your number?
::set{scene.affect.marina += 1}
</choice>
</branch>
/* state-driven callback — first-match-wins; <otherwise> is mandatory */
<match subject="scene.choices.number">
<when test="@fond">
@fixer{mono}: I asked nicely, which I am electing not to examine.
</when>
<otherwise>
@fixer{mono}: I am, apparently, taking that woman somewhere this evening.
</otherwise>
</match>

Leave out the <otherwise> and the checker refuses the scene, naming the choices nothing covers:

lute check scene.lute
scene.lute:36:1: error [E-NONEXHAUSTIVE] non-exhaustive `<match>`: `blunt`, `soft` are not covered and there is no `<otherwise>`
failed: scene.lute (1 error(s), 0 warning(s))

Split a single subject across property-scoped tracks (pos, opacity) alongside an independent camera track:

beat.lute
---
kind: scene
character: marina
season: 1
episode: 1
enums:
anchor: { members: [left, center, right], default: center }
action: { members: [slideInLeft, fadeInUp, fadeOut], exits: [fadeOut] }
framing: [close, tight]
cameraMove: [shake]
---
## Beat
<timeline duration="1.2">
<track subject="marina" property="pos">
::actor{character="marina" anchor="left" action="slideInLeft"}
</track>
<track subject="marina" property="opacity">
::actor{character="marina" action="fadeInUp"}
</track>
<track subject="camera">
::camera{focus="marina" framing="tight" duration="0.6"}
::camera{move="shake" duration="0.3" at="0.7"}
</track>
</timeline>

Story-driven RPGs run on world state, not just lines. A <hub> re-presents its choices until you leave; a choice is offered only when its when guard holds; a clue is a relational fact you ::assert; and a closing <match> reacts to how far the case has moved. The checker validates every relation signature, guard, and match arm before a player ever sees it.

study.lute
---
kind: scene
character: detective
season: 1
episode: 1
pov: detective
entities:
clue: { members: [cufflink, ledger] }
suspect: { members: [mireille] }
relations:
found: { args: [clue], tier: run }
accused: { args: [suspect], tier: run }
state:
run.caseProgress: { type: int, default: 0 }
---
## The Study
@narrator: The lamp still burned. The ledger had already given you one name.
<hub id="investigate">
<choice id="desk" text="Search the writing desk" once>
@detective: A cufflink, wedged behind the drawer.
::assert{ found(cufflink) }
</choice>
<choice id="confront" text="Confront Mireille with the ledger" when="holds('found', ['ledger'])">
@mireille: ...You've read it, then. Fine.
::assert{ accused(mireille) }
::set{run.caseProgress += 1}
</choice>
<choice id="leave" text="Step back into the hall" exit>
@detective: Enough for now.
</choice>
</hub>
<match subject="run.caseProgress">
<when is="1..">
@detective{mono}: A name, a motive. The case has a spine now.
</when>
<otherwise>
@detective{mono}: Still only shadows. Keep looking.
</otherwise>
</match>
Clue discovered
found(cufflink) lands in the run fact store; the confront choice unlocks because holds('found', ['ledger']) already holds.
Quest updated
run.caseProgress advances, and the closing <match> flips to “A name, a motive. The case has a spine now.”
Total, not Turing-complete
Every scenario provably terminates — no infinite loops, no unbounded recursion — and ::end names the ending it reached. Code after it in the same straight-line body is reported as dead (W-CODE-AFTER-END).
CEL + Datalog facts
Guards are plain CEL expressions; a Datalog fact layer lets you derive higher-order truths and query them across a scene.
Typed plugin capabilities
Directives and providers are declared with typed manifests, so the checker knows exactly what each plugin can emit and consume — and rejects a manifest that mistypes an option or claims a name the core owns.
To see what your engine receives turn to 3

Your engine

You ask gently. “Marina — would you mind terribly if I had your number?” scene.affect.marina goes from 0 to 1, so the callback takes its first arm: I asked nicely, which I am electing not to examine.

Lute is the author-time half: a grammar, a checker, and a compiler. Everything it knows becomes a versioned JSON IR plus CEL that your engine loads through a documented runtime contract — engines never embed the Lute parser or compiler.

who does what
writer
│ writes
▼
.lute source ──▶ Lute [ parse · check · analyze · trace · compile · LSP ]
│ emits
▼
versioned JSON IR + CEL
│ loaded by
▼
engine adapter [ command loop · CEL eval · facts · quest lifecycle · bridges ]
│ drives
▼
UI · animation · save · gameplay
Lute (author + toolchain)Your engine (runtime)
Grammar + type checkingRendering + input
Choice / <match> control dataCommand execution
Quest declarationsQuest ticking
Facts + Datalog rules as dataFact store + rule evaluation
Timeline dataAnimation playback
Bridge signature checkingMinigame / QTE execution
Connectivity analysisScene entry + saves

Section 1’s choice compiles to a record whose options name the position to turn to. This is lute compile scene.lute, cut to that one command:

lute compile scene.lute — the choice record
{
"kind": "choice",
"family": "control",
"position": "001-0500",
"branchId": "number",
"selectionKey": "scene.choices.number",
"options": [
{
"id": "blunt",
"text": "Just ask, flatly",
"lineId": "marina.s01ep02.number.blunt",
"target": "001-0600"
},
{
"id": "soft",
"text": "Ask gently",
"lineId": "marina.s01ep02.number.soft",
"target": "001-0800"
}
],
"converge": "001-1100"
}

Embed the official runtime (@lute-lang/runtime, a pure step function) or interpret the IR yourself against the conformance corpus. The player (@lute-lang/player, @lute-lang/player-web) renders the story; the game owns the world. Read the full split on the runtime contract page.

To run it before anyone plays it turn to 4

Tools

Before anyone plays section 1, walk it. lute trace runs one deterministic, mock-driven path — state, facts, choices, events — and reports every decision and why. It walks that path, not every path.

lute trace scene.lute --choice number=soft
trace: scene.lute (seeds: 0 paths, 0 facts; 1 selection)
## The Hostess with a Name
::camera{focus="marina" framing="close" duration=0.5}
@marina{emotion="surprised"} Oh!
@marina{emotion="delighted"} Mr. Fixer!
@marina{emotion="delighted"} From the ramen counter — you eat at Mechageki too, isn't it wonderful?
<branch number> eligible: blunt, soft -> soft
@fixer Marina — would you mind terribly if I had your number?
::set scene.affect.marina = 1
<match scene.choices.number> -> arm 1 (@fond)
@fixer{mono} I asked nicely, which I am electing not to examine.
trace complete: 2 decisions; choices 1/2 (number), arms 1/2 (scene.choices.number @36:1)
lute check
Statically validates a document — types, guards, relation signatures, match totality — and says what to change when it refuses.
lute trace
Previews one deterministic path against the mocks you supply, so you watch a scenario play out before anyone does.
lute check --project
Reachability over your declared after: routes — visited, completed, active — returns Reachable / Unreachable / Unknown; relational gates surface as human-review boundaries, never false certainty.
lute context
Emits a structured view of a project built for agents — the language was designed to be written with a model, not despite one.
Language server
Diagnostics, hovers, and completions in your editor, so mistakes surface as you type, not at runtime.
To read the rest of the book turn to 5

Docs

Every page of the reference is numbered the way this one is. Start with the first scene, or open the chapter you came for.

  1. 1 Getting Started 7 pages
  2. 2 Cheatsheet 1 page
  3. 3 Diagnostics 1 page
  4. 4 Reserved names 1 page
  5. 5 Guides 3 pages
  6. 6 Language 14 pages
  7. 7 State & Logic 4 pages
  8. 8 Connectivity 3 pages
  9. 9 Tooling 11 pages
  10. 10 Player 5 pages
  11. 11 Plugin System 4 pages
  12. 12 Examples 2 pages
  13. 13 Specification 2 pages

Or write Lute right now — no install — in the playground: live diagnostics, the compiled JSON IR, and a mock-driven trace, all in your browser.

Get started
Playground