Last October we shipped a deprecation notice for an internal CLI — chagctl deploy-legacy — that eleven teams still depended on. The notice was technically complete: the command being removed, the replacement, the removal date, a migration example. Sixty days later, on the removal date, three teams paged us within ninety minutes of each other. When I pulled up the Slack thread afterward, one of the affected engineers said something I have not been able to stop thinking about: “I read that notice twice. I still didn’t think it applied to me.”
That sentence is the whole problem, and it is not a documentation problem in the way we usually mean it. The notice had all the information. What it did not have was a story: a setup that told you where you stood, a decision point that told you whether you were affected, and a consequence that told you what would happen if you did nothing. I have started calling this narrative debt, and it is the reason your runbook fails at 3 AM, your ADR gets re-litigated in every design review, and your deprecation notices get read and ignored. This article is a post-mortem of that failure mode, a decision matrix for choosing documentation formats, and a CI check you can copy in before lunch.
The post-mortem: information present, narrative absent
Here is roughly what our deprecation notice looked like, with names changed:
# Deprecation: chagctl deploy-legacy
`chagctl deploy-legacy` is deprecated as of 2026-08-01
and will be removed on 2026-10-01.
Replacement: `chagctl deploy --profile standard`
Migration example:
chagctl deploy --profile standard --app my-service
See the platform handbook for details.
Every fact is correct. Every fact is also useless to the engineer who skimmed it between two meetings. Compare the version we should have written:
# Deprecation: chagctl deploy-legacy
**If you run `chagctl deploy-legacy` in CI or a cron,
your deploys will fail on 2026-10-01.**
Check in 30 seconds:
grep -rn "deploy-legacy" .github/ .gitlab-ci.yml Jenkinsfile*
If grep finds nothing, stop reading — this does not
apply to you.
If it finds a hit, you have two decisions:
1. Migrate now: `chagctl deploy --profile standard`
(drop-in for 90% of cases; see diff below)
2. Can't migrate by 10-01? File a blocker:
chagctl deprecation-exception --reason "..."
Why we're removing it: it shells out to the 2023
provisioning API, which is 4x slower and silently
ignores the retry budget. The replacement is not
optional quality-of-life — your deploys are currently
burning 40s each on a code path we are deleting.
The second version is longer, and that is the point. It has setup (are you affected? check now), decision (migrate or file an exception), and consequence (your deploys will fail, and here is the mechanism). The first version had facts. Facts do not page anyone at 3 AM; consequences do.
This is not a novel insight about incident documentation. The SRE community codified this decades ago — the Google SRE book dedicates entire chapters to postmortem culture, emergency response, and incident management, and even includes an example postmortem as an appendix, precisely because the shape of the document determines whether anyone learns from it (Google SRE book, Ch. 15 and Appendix D). What the SRE book covers for incidents, we consistently fail to apply to the quieter documents: runbooks, ADRs, deprecation notices, CLI help text.
What narrative debt actually is
Narrative debt is the gap between a document that contains true statements and a document a stressed human can act on. Like technical debt, it accrues silently and compounds. The symptoms are recognizable:
- Runbooks that list commands but never say what “working” looks like, so the on-call cannot tell whether step 4 succeeded.
- ADRs that state the decision but omit the rejected alternatives, so every new hire re-argues it.
- Deprecation notices that describe the tool instead of describing the reader’s situation.
- CLI help text that documents flags without documenting the workflow the flags exist to serve.
In each case the information is present and the story is missing. Screenwriters have understood this distinction for a century: professional screenwriting format exists not for aesthetics but so the reader follows the story instead of decoding the mechanics — scene headings, transitions, and act structure are standardized precisely to keep the audience engaged in the narrative rather than the formatting (StudioBinder’s screenwriting guide makes this explicit: structure serves the story, not the other way around). A runbook is the same artifact with worse lighting. The reader is tired, scared, and holding a pager. If your document makes them reconstruct the plot, they will reconstruct it wrong.
Every operational document needs three narrative beats, and you can audit for them mechanically:
- Setup — where the reader is, and how to verify they are in the right place. A runbook that starts with “run
kubectl get pods” without saying what output means “you have the problem this runbook solves” has no setup. - Decision — the branch points, with a checkable condition for each. “If the queue depth is above 10k, go to section 4; otherwise continue.”
- Consequence — what happens if you do nothing, do the wrong thing, or stop halfway. This is the beat almost always missing, and it is the only one that changes behavior.
The decision matrix: which format carries which story
Before you fix narrative debt, you have to pick the right container, because format determines what stories are even possible. A wiki page cannot carry a diff review; a generated CLI doc cannot carry a decision. Here is the matrix we now use, with the inputs that actually matter — team size, repo shape, and failure cost:
| Format | Best for | Fails when | Team size / repo shape | Failure cost if it rots |
|---|---|---|---|---|
| Wiki page (Confluence, Notion) | Orientation, onboarding overviews, “what exists” | No review gate, no diff history, commands drift silently | Any size; works for polyrepo sprawl | Low–medium: usually read when calm |
ADR in-repo (Markdown, docs/adr/) |
Decisions with rejected alternatives and context | Written after the fact as a rubber stamp; no “Status” header | 20+ engineers; monorepo or shared docs repo | High: re-litigated decisions cost design reviews |
| README-adjacent runbook (next to the code it operates) | 3 AM procedures, deprecation notices | Repo moves, service renamed, runbook orphaned | Any size; monorepo strongly preferred | Highest: read under stress, errors compound |
Generated CLI docs (--help, man pages) |
Flag reference, the “how” layer | Treated as the whole documentation; no workflow story | Any size | Low alone, high if it’s your only layer |
The pattern: the higher the failure cost, the closer the document must live to code review. A runbook that a human can execute wrong at 3 AM belongs in the repo, in a PR, reviewed by someone who has run it. Wiki pages are fine for things read while calm. Generated docs are fine for reference. Neither is fine for the pager.
The CI check: make narrative rot a build failure
Narrative debt has a technical component too: runbooks reference commands that stop existing. This is the most preventable form of rot, and you can catch it in CI. Here is a doc-lint script we run on every PR that touches docs/runbooks/. It extracts fenced commands, checks that referenced binaries exist on PATH or in the repo, and fails with the offending line:
#!/usr/bin/env bash
# scripts/doc-lint.sh — fail when runbooks reference
# commands that no longer exist.
set -euo pipefail
DOCS_DIR="${1:-docs/runbooks}"
fail=0
# Extract fenced code blocks, take the first token of each
# line, and check it resolves to a command.
while IFS= read -r cmd; do
bin="${cmd%% *}"
# skip shell builtins, vars, and our own repo scripts
case "$bin" in
*"$"*|""|if|for|while|export|cd|echo|sudo) continue ;;
esac
if [[ "$bin" == ./scripts/* || -x "$bin" ]]; then
continue
fi
if ! command -v "$bin" >/dev/null 2>&1; then
echo "FAIL: '$bin' (from: $cmd) not found —" \
"referenced in $DOCS_DIR" >&2
fail=1
fi
done < <(grep -rhoE '^\s{0,4}[a-z][a-z0-9_-]+' \
"$DOCS_DIR" --include='*.md' | sort -u)
exit "$fail"
Wire it into CI — GitHub Actions example:
- name: Doc lint (runbook command check)
run: ./scripts/doc-lint.sh docs/runbooks
Is this crude? Yes. It will flag commands that only exist on production hosts, and you will add an allowlist file for those within a week. But the first time it runs, expect it to find three to five dead commands in your runbooks. Each one of those is a 3 AM failure that has not happened yet. The check converts “documentation accuracy” from a virtue into a build status, which is the only form most engineering cultures reliably maintain.
A second, cheaper check: grep your runbooks for the three narrative beats. It sounds silly; run it once on your most-used runbook and count the if statements:
grep -cE '^#|if |expected|otherwise' docs/runbooks/queue-backup.md
If the count of conditional language is near zero, the runbook is a script wearing a runbook’s clothes — it has steps but no decisions, and the first anomaly will send the on-call to Slack anyway.
Where the hard part actually is: drafting the long-form documents
Here I have to admit something that took me too long to accept: the CI check fixes the easy half. The hard half is that ADRs, post-mortems, and migration narratives are writing, and most engineers were never taught to draft long-form structured documents. The facts are usually in your head within an hour of the incident. What takes the weekend is sequencing: which decision comes first, what context the reader is missing, where the consequence lands for maximum effect. I have watched excellent engineers produce a post-mortem that is a chronologically accurate list of events with no causal spine, and then wonder why the action items go unowned.
The fix is to treat the draft as a planning artifact, not a typing task. Outline the three beats before writing prose. If your team already drafts these documents in a writing tool rather than straight into the repo, use one with actual structure and planning features — outlining, section reordering, notes that survive into the draft. Some of this is what AI writing software with built-in outlining and structure planning is aiming at, and the workflow argument is sound even if you stay skeptical of anything with “AI” in the category name: the value is in forcing the narrative skeleton to exist before the prose does. Whether you use a dedicated app or a Markdown file with three headings — Setup, Decision, Consequence — the discipline is the same: sequence first, sentences second. Then paste into the repo, where the review and the CI check live.
The deprecation notice, rewritten as a contract
Let us close the loop on the incident that started this. The rewrite we shipped after the outage had one structural change beyond the three beats: it made the reader’s situation, not our tool, the subject of the first sentence. “If you run X, your deploys will fail on date” beats “X is deprecated on date” every time, because the reader’s brain is pattern-matching for am I in this story? from the first line.
Since shipping the rewritten format plus the doc-lint check, we have run two more deprecations with zero removal-day pages. Sample size of two, so hold your confidence loosely — but the mechanism is sound: the notice now answers the only question a skimming reader has, in the first line, with a checkable condition.
One more thing worth stealing: put the removal date in the tool itself, not just the doc. A deprecation notice that also prints WARN: deploy-legacy removed in 12 days — see docs/deprecations/2026-08-deploy-legacy.md on every invocation converts your documentation into a narrative the reader cannot avoid. The doc and the tool telling the same story is the whole game.
Rule of thumb
Run the audit this week: take your most-paged-on runbook and check it for the three beats — setup, decision, consequence. If any beat is missing, the document is not under-maintained; it is unfinished. And paste this into your next ADR template:
A document a stressed engineer cannot act on is not documentation — it is a liability with good grammar. Every internal doc must answer, in order: where am I, what do I decide, and what happens if I do nothing.