API reference

Everything on the public surface. For narrative and examples, start with Getting started.

Constructor#

OptionDefaultWhat it does
origin—{ who, what, where }, attached to every story. Normalized like any context.
narrationcollectedcollected or live. Env: STORYTELLER_NARRATION
formattextWhich default audience: console for text, NDJSON for ndjson. Env: STORYTELLER_FORMAT
levelinfoMinimum level delivered. Env: STORYTELLER_LEVEL
audiences[]Registered on top of the default
audience—Share another storyteller's registry; no default is added
onAudienceErrorthrottled warningCalled when an audience throws or rejects
maxInFlight1000Deliveries in flight per audience before beats are dropped

Storyteller#

MemberReturnsDescription
report(input, context?)thisReport a beat. Any value. Context: who, what, where, error, level, live, to
finish(title, options?){ to }Emit the collected story. Options: level, error. Call .to() synchronously.
narrate(mode)thisSwitch narration at runtime
chapter(options?)StorytellerA child storyteller linked by parentStoryId
reset()thisDrop collected beats without emitting; new story id
summarize(options?)FormattedReportPreview the current beats as a report
currentStoryIdstringThe id beats are being tagged with
audienceAudienceRegistryadd, remove, has, names

Deprecated, removed at 1.0: note() → report(), tell() → finish(), warn() → finish(title, { level: "warn" }), oops() → finish(title, { level: "oops", error }). They behave identically. See migrating from 0.2.

Audiences#

AudienceMember
type AudienceMember<Kind = "story"> = {
  name: string;
  hears?: Kind[];                       // "note" | "story"; defaults to ["story"]
  accepts?(emission: EmissionOf<Kind>): boolean;
  hear(emission: EmissionOf<Kind>): void | Promise<void>;
};

// No `hears` → a StoryEvent, exactly as before live narration existed.
// hears: ["note"] → a NoteEmission.  Both → the union; narrow on .kind.
FunctionHearsDescription
consoleAudience(options?)notes, storiesDefault. Compact line per beat, grouped block per story
dbAudience(insert)stories at warn/oopsPersists via your insert function
ndjsonAudience(options?)notes, storiesOne JSON object per line. Options: stream, name, level
storeAudience(store, options?)stories, every levelKeeps stories in a store. Options: name, level, accepts

Shared instance#

useStoryteller() returns the same instance everywhere in a process — services, middleware, and components contributing to one story.

singleton.ts
import { useStoryteller } from "@lovelaces-io/storyteller";

// First call creates the instance
const story = useStoryteller({
  origin: { who: "api-server" },
});

// Same instance everywhere
const sameStory = useStoryteller();
console.log(story === sameStory); // true

Formatting#

FunctionReturnsDescription
formatStory(event, options?)FormattedReportOne story as readable text plus structured data
writeStoryReport(stories, options?)stringMany stories, grouped by day
formatDuration(ms)string340ms, 3.4s, 2:05m
Report optionDefault
detail"normal""brief" · "normal" · "full"
colorstrueANSI colors
noteLimit50Maximum beats shown
showDatatrueInclude the JSON block
timezonesystemIANA timezone
locale"en-US"Date formatting

Normalization#

normalizeValue(input, options?) turns any value into JSON-safe data and never throws. report() calls it for you; it's exported for when you need it directly.

OptionDefault
maxDepth6
maxArrayLength100
maxProperties100
maxStringLength8000
redactKeyspassword, token, secret, apiKey, authorization, cookie, sessionId, privateKey, …
redacttrue
redactValues"balanced" — recognisable secret formats inside any string, and secret-shaped keys; "strict" also removes long random-looking runs; "off" matches key names only

auditRedaction(value) reports what redaction would remove — path, reason, a four-character preview — without changing anything. redactString(text) and redactJson(value) apply it to data you already hold.

Stores#

Where stories go and how they come back. The narrative is on The Library; this is the surface.

ExportDescription
StoryStoreappend(event), get(storyId), query(criteria), children(parentStoryId), prune(before)
StoryQuerysince, until, level, minimumLevel, about, from, parentStoryId, slowerThanMs, failed, limit, offset, order
memoryStore(options?)The reference store: a bounded map (capacity, default 10 000), browser-safe
fileStore(path)One JSON-lines file, append-only, prune rewrites it. Node only, from @lovelaces-io/storyteller/store/file
stories(store)The vocabulary: about, from, level, atLeast, failing, succeeding, slowerThan, since, until, under, newest, oldest, limit, skip; then all(), first(), count(), or just await it
parseDuration(input)"30s", "5m", "24h", "7d", "2w" or milliseconds; throws on anything else
canonicalRow(story)The columns an adapter stores: ids, timestamp, level, title, flattened origin, duration, error message, notes, search_text, the whole record
matchesQuery(story, criteria) / applyQuery(stories, criteria)The reference matcher an adapter's own query must agree with

Read-only access for agents is a separate package, @lovelaces-io/storyteller-mcp: see the Librarian.

The record#

Exactly what JSON.stringify(event) produces. Every field has a job.

JSON.stringify(event)
{
  "kind": "story",                            // or "note", on a live beat
  "storyId": "2c005b27-…",                     // groups beats with their story
  "timestamp": "2026-03-31T14:30:03.420Z",    // when the story was told
  "level": "Warning",                         // Information | Warning | Error
  "title": "Payment retry succeeded",         // the story's headline
  "origin": {                                  // where this story comes from
    "who": "payment-service",
    "where": { "app": "web", "page": "checkout" }
  },
  "durationMs": 3420,                         // first note to last note
  "notes": [                                   // chronologically sorted
    {
      "timestamp": "2026-03-31T14:30:00.000Z",
      "sequence": 0,                          // gap-free, assigned as reported
      "note": "Card declined by processor",
      "error": { "message": "insufficient funds" }
    },
    {
      "timestamp": "2026-03-31T14:30:02.000Z",
      "note": "Retrying with backup processor"
    },
    {
      "timestamp": "2026-03-31T14:30:03.420Z",
      "note": "Payment approved",
      "what": { "amount": "$42.00" }
    }
  ],
  "error": {                                   // top-level error (from oops)
    "name": "CardDeclinedError",
    "message": "Insufficient funds"
  }
}