Collected & live
Two ways to hear the same story. Collected gives you one record when the work ends. Live gives you every beat the moment it happens — and still gives you the record.
Collected — the default#
Beats are kept until you call finish(), then emitted together as one record. This is the right shape for an audit log: one row per operation, complete and self-contained.
It's the wrong shape when the work takes forty seconds and someone — a person or an agent — wants to know what's going on right now.
Live#
Set narration to live and each report() is emitted immediately. The console prints one compact line per beat:
14:30:00 info checkout / web User submitted payment {amount=49.99} 14:30:01 info checkout / web Charging card {stripe} 14:30:03 warn checkout / web Card declined {gateway timeout} 14:30:03 info checkout / web Retrying 14:30:04 info checkout / web Charge succeeded
Live adds emissions. It never removes them — the story still lands at finish(). An audience that only wants beats says so with hears: ["note"], rather than by silencing the record.
Turning it on
// Per instance const story = new Storyteller({ narration: "live" }); // At runtime — takes effect on the next report() story.narrate("live"); story.narrate("collected"); // One urgent beat out of an otherwise collected story story.report("Disk at 97%", { level: "warn", live: true });
The environment variable wins when nothing is set in code, so you can switch a deployed process to live narration without touching it.
Nothing is lost#
Every beat carries the storyId of the story it belongs to, and a sequence number assigned the instant you call report() — gap-free from zero.
That means whoever holds the streamed beats can order and group them back into exactly the record collected narration would have produced. Neither mode is the lesser one. This is the guarantee the whole design rests on, and it's enforced by tests.
Order by sequence, never by arrival time. Audiences are asynchronous, and a slow one lands late.
Configuration#
Every option can come from the environment, so the same file behaves differently for a person at a terminal and a program reading a pipe.
| Variable | Values | Effect |
|---|---|---|
STORYTELLER_NARRATION | collected · live | Whether beats stream as they happen |
STORYTELLER_FORMAT | text · ndjson | Which default audience is registered |
STORYTELLER_LEVEL | info · warn · oops | Minimum level delivered — filters the closing story too |
STORYTELLER_COLOR | 0 · 1 | Force colors off or on |
Unrecognized values fall back to the default rather than throwing. Output format is deliberately not inferred from whether stdout is a terminal — output that changes shape when a process is piped is a debugging afternoon nobody asked for.