API reference
Everything on the public surface. For narrative and examples, start with Getting started.
Constructor#
| Option | Default | What it does |
|---|---|---|
origin | — | { who, what, where }, attached to every story. Normalized like any context. |
narration | collected | collected or live. Env: STORYTELLER_NARRATION |
format | text | Which default audience: console for text, NDJSON for ndjson. Env: STORYTELLER_FORMAT |
level | info | Minimum level delivered. Env: STORYTELLER_LEVEL |
audiences | [] | Registered on top of the default |
audience | — | Share another storyteller's registry; no default is added |
onAudienceError | throttled warning | Called when an audience throws or rejects |
maxInFlight | 1000 | Deliveries in flight per audience before beats are dropped |
Storyteller#
Member Returns Description 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.
Function Hears Description consoleAudience(options?)notes, stories Default. Compact line per beat, grouped block per story dbAudience(insert)stories at warn/oops Persists via your insert function ndjsonAudience(options?)notes, stories One JSON object per line. Options: stream, name, level storeAudience(store, options?)stories, every level Keeps 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#
Function Returns Description 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 option Default detail"normal""brief" · "normal" · "full" colorstrueANSI colors noteLimit50Maximum beats shown showDatatrueInclude the JSON block timezonesystem IANA 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.
Option Default 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.
Export Description 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"
}
}