Examples
Real shapes of work. Copy the one that looks like yours.
An API request#
One story per request. The failure path finishes with the error attached; the record carries the whole lifecycle either way.
middleware.ts
const story = new Storyteller({ origin: { who: "api", where: { service: "users" } }, }); story.report("Request received", { what: { method: "POST", path: "/users" } }); try { const user = await createUser(body); story.report("User created", { what: { id: user.id } }); story.finish("User registration complete"); } catch (error) { story.finish("User registration failed", { level: "oops", error }); }
A background job#
Report progress as you go; pick the level at the end based on what actually happened.
sync-job.ts
const story = new Storyteller({ origin: { who: "sync-worker" } }); story.report("Starting daily sync"); const records = await fetchRecords(); story.report(`Fetched ${records.length} records`); let failures = 0; for (const record of records) { try { await processRecord(record); } catch { failures++; } } story.report(`Processed with ${failures} failures`); if (failures > 0) { story.finish("Sync completed with errors", { level: "warn" }); } else { story.finish("Sync completed"); }
A nested sync, narrated live#
One chapter per account. Each becomes its own record, linked to the parent, while the console shows every beat as it happens.
sync.ts
const story = new Storyteller({ origin: { who: "sync-agent" }, narration: "live" }); story.report("Starting sync"); for (const account of accounts) { const chapter = story.chapter({ origin: { what: account.id } }); chapter.report("Fetching invoices"); chapter.report("Reconciling"); chapter.finish(`Synced ${account.id}`); } story.finish("Sync complete");
Failures to Discord#
discord.ts
story.audience.add({ name: "discord", accepts: (event) => event.level === "Error", hear: async (event) => { await fetch(process.env.DISCORD_WEBHOOK_URL!, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: "``` " + event.summarize({ colors: false, detail: "brief" }).text + " ```", }), }); }, });
A stream, piped to jq#
Same code as above. Only the environment changes.
terminal
$ STORYTELLER_NARRATION=live STORYTELLER_FORMAT=ndjson node sync.js \ | jq -r 'select(.kind=="note") | "\(.sequence) \(.level) \(.note)"' 0 Information Reading config 1 Information Fetched invoices 2 Warning Rate limited