Chapters
Real work nests. An agent spawns subtasks; a batch runs one operation per item; a request crosses services. chapter() keeps each piece a complete story of its own, linked to its parent.
Opening one#
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");
Each chapter emits its own record, carrying parentStoryId. It's not folded into the parent's beats — one record per story stays true, and a nested story is still a story.
What a chapter inherits#
- The parent's audiences — the same registry, not a copy. An audience you add to the parent later reaches the chapter too; remove one and it's gone for both.
- Origin, shallow-merged with anything you pass. Above, each chapter keeps
who: "sync-agent"and adds its ownwhat. - Narration, level threshold, error handling, and the in-flight bound — all overridable per chapter.
parentStoryId is captured when the chapter is created, so a parent that finishes first doesn't orphan its chapters.
Rebuilding the run#
Follow parentStoryId and the whole run comes back as a tree. Nest as deep as the work does.
tree.ts
// Every chapter record carries parentStoryId. That is all you need to rebuild the run. const byParent = groupBy(records, (r) => r.parentStoryId ?? "root"); // Sync complete (1 beat) // Synced acct-1 (2 beats) // Synced acct-2 (2 beats) // Synced acct-3 (2 beats)
This is what turns a pile of records into a trace — and it's the precondition for asking "what happened during that run?" across more than one step.