Narrative Engine
Scoped actor state for interactive fiction: a universe holds characters, items and locations whose typed attributes and narrative flags can be isolated per story and per play session, then promoted into shared canon when the session ends. It is the persistence layer beneath Lore, and useful on its own wherever story state must outlive a session. Every example on this page was executed against a live server.
When to reach for it: RPG character state that survives across sessions, branching-story experiments isolated per player, shared-canon multiplayer worlds with promotion rules, AI NPCs with persistent attributes, tabletop campaign state.
When not to: high-frequency simulation entities with component queries belong to ECS; guarded lifecycle state machines belong to FSM; an append-only activity feed belongs to Timeline - narrative events are mutable flags, not a log.
Concepts
- Universe - the top-level container. Every actor, story, session,
template, tag and event lives inside one, every route is nested under
/universes/{universe_id}, and deleting one cascades to all of it. - Scope hierarchy - an actor exists at
universe,storyorsessionscope: session is a private scratch copy, universe is canon, story sits between them. See Scopes and promotion. - Actor - a
character,itemorlocationwith typed attributes, tags and metadata, usually stamped out of a template; see Actors and attributes. - Event - an idempotent key-value flag on an actor (
quest_started,door_opened). Re-setting a key overwrites it in place. - Control grant - a lock deciding who may mutate an actor during play. Optional, and destructive by default when it expires; see Control grants.
- Everything is tenant-wide - narrative has no per-creator privacy. Any token in the tenant can read and write any universe, verified live with a second subject.
Universes
snug narrative create-universe -n "Fantasy World" -d "Medieval setting" \
-m '{"genre":"high-fantasy"}'
snug narrative list-universes -p 1 -l 25
snug narrative get-universe -u <universe-id>
snug narrative update-universe -u <universe-id> -n "New Name"
snug narrative delete-universe -u <universe-id>list-universes is the one command that takes no universe id, and the one
that pages with -p/--page plus -l/--page-size. Every other list command
uses -l/--limit, sent as page_size.
Stories
A story is a narrative arc - a chapter, a branch, an alternate timeline:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"docs4-sidequest","story_type":"side","description":"An optional detour"}' \
http://localhost:4000/api/v1/narrative/universes/$UNIVERSE_ID/stories{
"status": 201,
"msg": "Created",
"data": {
"story_id": "fJJLdcqYUbLB",
"universe_id": "maGYUryytJtg",
"name": "docs4-sidequest",
"description": "An optional detour",
"story_type": "side",
"publish_state": "draft",
"tags": [],
"settings": {
"allow_sessions": false,
"max_sessions": 0,
"actor_import_mode": "copy_on_write"
},
"created_at": "2026-08-29T12:53:22.629035Z",
"updated_at": "2026-08-29T12:53:22.629035Z"
}
}story_type is a free-form string, not an enum - main, side and
anything-goes are all accepted. The CLI equivalents are create-story,
list-stories, get-story, update-story and delete-story.
Sessions
A session is a TTL-bounded, player-scoped instance of play, and the unit that makes session-scoped actor state disposable:
snug narrative create-session -u <universe-id> -p <player-id> \
-s <story-id> --ttl-seconds 3600
snug narrative list-sessions -u <universe-id> --active-only
snug narrative update-session -u <universe-id> --session-id <id> \
--extend-ttl-seconds 3600
snug narrative end-session -u <universe-id> --session-id <id>TTL defaults to 86400 seconds. Sessions and the actors inside them are deleted, not archived - see Scopes and promotion.
Tag definitions
snug narrative create-tag -u <universe-id> -n "boss" -c "#ef4444"
snug narrative list-tags -u <universe-id>
snug narrative update-tag -u <universe-id> --tag-id <id> -n "elite"
snug narrative delete-tag -u <universe-id> --tag-id <id>Color defaults to #3b82f6. Tag definitions are a display registry only -
an actor may carry any tag string, defined or not.
Events
Events are the narrative flags on an actor - quest progress, doors opened,
choices made. Setting an existing key overwrites the value in place and
keeps the same event_id:
snug narrative set-event -u <universe-id> -a <actor-id> -k "quest_started" \
--value-json '{"quest":"The Quest","step":1}'
snug narrative get-event -u <universe-id> -a <actor-id> -k "quest_started"
snug narrative delete-event -u <universe-id> -a <actor-id> -k "quest_started"
snug narrative list-events -u <universe-id> -a <actor-id> # one actor
snug narrative list-events -u <universe-id> -k door_opened # one key
snug narrative aggregate-events -u <universe-id> -k door_openedaggregate-events answers "how many actors have this flag" and returns just
{"event_key": "door_opened", "total_count": 2}. To seed many at once,
batch-set-events -f <file> takes a JSON array of
{"actor_id", "event_key", "event": {"value_json": ...}} entries.
Behaviors and gotchas
list-actor-eventsis broken. The CLI callsGET /universes/{id}/actors/{id}/events, a route the server does not serve, and returns404 NOT_FOUND("Resource not found: route"). Uselist-events -a <actor-id>instead.- An overwritten event keeps its original
created_at. Events carry noupdated_at, so put a timestamp invalue_jsonif you need one. - Universe and story
settingsare inert.allow_story_creation,max_stories,allow_session_creationandmax_concurrent_sessionsare stored and returned but never consulted - a universe withallow_story_creation: falsestill accepts stories. Enforce caps yourself. - List payloads use
items, not an array named after the entity. - Deleting a universe deletes everything under it - controls, events, actors, sessions, stories, tags and both kinds of template.
qandsort_byare HTTP-only. List endpoints accept the shared search grammar parametersq,sort_byandsort_order, but the CLI exposes only typed filters.
Limits and configuration
Session TTL (86400s), control-grant duration (3600s), the sweep intervals for
expired sessions (30s) and control grants (10s), the 500-result query cap and
the 500-event batch cap are all tunable through the NARRATIVE_* variables in
the
Narrative CONFIG reference.
Reference
- Narrative API - every endpoint, callable
- Actors and attributes - templates, typed attributes, querying
- Scopes and promotion - session, story and universe scope
- Control grants - who may mutate an actor, and when
- Related: Lore builds a scripted story runtime on this service; ECS for component-oriented entities; FSM for guarded state machines