Lore Story Engine
Runs compiled interactive-fiction programs as server-side sessions. You author
a story in the Lore DSL, compile it to JSON, and the server executes it -
emitting story events (speak, choose) and blocking until the player
answers. Branching, variables, and save points live on the server, so the
client stays a renderer. Every example on this page was executed against a
live server.
When to reach for it: visual novels and branching fiction, RPG dialogue trees whose state must not live on the client, scripted onboarding flows, live-ops narrative beats changed without shipping a new build.
When not to: entity lifecycles with guards and transitions belong to FSM; storing universes, actors, and stories without running them is Narrative.
Concepts
- Only compiled programs run. The interpreter executes a
LoreCompilerOutputJSON document, never.loresource. Compile locally withlore compile, or hand the source to Narrative to compile server-side. - Lore runs on Narrative at runtime, not just at
build time. Every session is backed by a Narrative session,
@imported actors resolve to Narrative actor templates, and in-script@eventand@awarddirectives write through to Narrative actors. - Sessions are in-memory and owned. A session lives in the API process, is keyed to the creating principal, and does not survive a restart. Checkpoints, which live in Redis, do.
- The play loop is acknowledgement-driven. When the interpreter needs
input it emits an event carrying an
ackIdand blocks until it gets areadyreply quoting that id. A WebSocket streams those events live;POST .../stepsends one command and returns the events that follow. - Checkpoints are scoped to universe + story + user, not to a session. That is what makes resuming in a fresh session work at all.
Running a story without publishing it
The dev endpoint takes compiled output inline and fabricates the Narrative
universe, story, and actor templates it needs - the fastest path from a
.lore file to a running session:
lore compile -i docs4-vault.lore -o docs4-vault.json
snug --output json lore dev -o @docs4-vault.json --auto-start{
"session_id": "HWqbERgJhJQK",
"websocket_url": "/ws/lore?session=HWqbERgJhJQK"
}Each dev session mints a brand-new story id, so its checkpoints are invisible to the next one. Use a published story when you need progress to carry over.
Running a published story
Give Narrative the source and it compiles it into the story's lore_compiled
field; Lore then reads that field. Over HTTP:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"universe_id": "BSvXeGcHSGZV", "story_id": "VMfTzXyKdCjU", "auto_start": true}' \
http://localhost:4000/api/v1/lore/sessions{
"status": 200,
"msg": "OK",
"data": {
"session": {
"id": "EWtGUFrGpqwP",
"universe_id": "BSvXeGcHSGZV",
"story_id": "VMfTzXyKdCjU",
"user_id": "docs-wave",
"state": "created",
"created_at": "2026-08-29T12:48:04.772965Z",
"last_activity_at": "2026-08-29T12:48:04.772965Z"
},
"websocket_url": "/ws/lore?session=EWtGUFrGpqwP"
}
}The equivalent CLI call is snug lore create -u <universe> -s <story> --auto-start. A story whose source was never saved fails here with
400 compilation_failed ("Story has no compiled lore output"), and an unknown
story id with 404 story_not_found.
The step loop
step sends one command and returns everything the interpreter emits before
it blocks again. With no --event it sends ready and auto-resolves whichever
acknowledgement is outstanding, which is all a straight playthrough needs:
snug --output json lore step EWtGUFrGpqwP{
"session_id": "EWtGUFrGpqwP",
"state": "running",
"events": [
{ "event": "did_load", "data": {} },
{ "event": "will_start", "data": {} },
{ "event": "did_start", "data": {} },
{
"event": "speak",
"data": {
"ackId": "20ec6912-...",
"character": "narrator",
"text": "You wake in a cold stone corridor."
},
"ackId": "20ec6912-..."
}
],
"awaiting_input": true
}awaiting_input means the interpreter is blocked. Answering a choose sends
the index of the option you want, under the key the interpreter reads:
snug --output json lore step EWtGUFrGpqwP -d '{"choice":{"value":1}}'Keep stepping until state is completed, which arrives alongside will_stop
and did_stop; stepping a session that is not running returns
409 session_not_running. Variables can be seeded at creation - adding
-i '{"difficulty":"nightmare"}' to snug lore dev rendered a ${difficulty}
interpolation as Difficulty is nightmare.
WebSocket
GET /ws/lore?session={id} upgrades to a raw stream of the same
{event, data, ackId} messages and accepts the same replies, with the token
in the Authorization header or a token query parameter. Every failure -
missing session, bad token, unknown session, someone else's session - is
caught during the handshake, so a rejected connection never reaches 101 at all
rather than opening and then closing. The event vocabulary is in the
API reference.
Behaviors and gotchas
--auto-startis effectively mandatory outside WebSocket. The RESTcommandendpoint rejects any session that is not alreadyrunning, andstartis precisely the command that would make it running - so sendingstartover REST always returns409 session_not_running. A session created withoutauto_startcan only be started by connecting a WebSocket.- Finishing a story does not free its slot. The per-user cap of five
counts every session the process holds,
completedones included; the sixth create returns429 session_limit_exceeded. Reclaim slots withsnug lore delete <id> --force. - Another user's session reads as absent, not forbidden. Ownership
failures return
404 session_not_foundrather than403, and the session is simply missing from theirlore list. - Checkpoint names are not symmetric between the two surfaces. This bites everyone once: see Checkpoints and resuming.
Limits and configuration
Five concurrent sessions per user, 1000 server-wide, and a one-hour idle timeout swept every minute. Unlike most services these are compile-time defaults only - Lore reads no environment variables at all, as the Lore CONFIG reference spells out.
Reference
- Lore API - every endpoint, callable
- Checkpoints and resuming - save points and the naming trap
- Related: Narrative for the universes, stories, and actors Lore runs on, WebSocket for the shared realtime infrastructure