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