The Scribe — the postmortem drafts itself twenty minutes after resolution; you verify, not excavate
The Scribe flips the labor of postmortems: an agent assembles the timeline from your chat export, alert history, and deploy log the moment the incident closes, and drafts a blameless record where every single claim carries a verbatim source quote. Your job shrinks to verifying the claims.
Every team agrees postmortems matter, and every team has a backlog of unwritten ones — because assembling a record from three hours of channel scrollback is miserable work that lands on whoever is most exhausted.
The scenario — before and after
THE POSTMORTEM WITHOUT THE SCRIBE
Two weeks late
14:02
Alert fires. Tuesday, July 8: p99 latency on checkout-api. Priya, the on-call SRE, spends 75 minutes in #inc-checkout-latency with four colleagues, chasing a Redis red herring for ten minutes before someone spots the 13:47 deploy that shrank the database connection pool from 100 to 20 per replica.
15:09
Rollback. Resolved at 15:17, twenty-three support tickets, nice work all. Then the EM types the sentence every channel dreads: "postmortem doc... who wants it," and everyone looks at their shoes.
+11 days
The doc is still a title and an empty template. The review meeting has been rescheduled twice.
The archaeology
Priya finally blocks a Friday afternoon: scrolling four hundred messages interleaved with samosa jokes, reconstructing whether the rollback finished at 15:09 or 15:12 — it matters, because the error-rate alert resolved at 15:14 and the sequence determines whether the rollback actually fixed it — and trying to remember why the Redis theory felt so convincing at 14:16.
+3 hours
A timeline she half-trusts, a contributing-factors section written from memory, and action items vague enough to survive any review ("improve deploy safety").
The postmortem ships two weeks late, the review meeting is a formality because nobody remembers the details either, and the config-review gap that let a staging value reach production is still open when the next incident arrives. This is postmortem debt: the organization pays interest in repeated incidents on lessons it technically already learned.
THE SAME INCIDENT, WITH THE SCRIBE
Twenty-five minutes of verification
Same 15:17 resolution — but the incident tool's "resolved" webhook now triggers the Scribe. It pulls three sources it can only read: the channel export, the alert history from PagerDuty, and the deploy log.
15:37 · SCRIBE
checkout-api → postmortem draft
20 minAFTER RESOLUTION — while everyone still remembers everythingsummary · 14-entry timeline · 3 contributing factors · 4 proposed action items · an "Unknowns" section
Every timeline entry and contributing factor carries a verbatim quote — "the perf PR changed db pool max_connections from 100 to 20 per replica" sits right under the claim it supports, and Priya can grep for it
Blameless by construction: the draft says "the on-call engineer" and "a service developer," never names
The Redis theory appears nowhere as a factor — it was investigated and ruled out in the sources, so the Scribe leaves it out
Exactly how many customers were affected sits in Unknowns, rather than as a confident invented number
Each claim in the draft ends with a VERIFY: checkbox.
Priya's job is now a twenty-five-minute verification pass, not a three-hour excavation. She ticks the claims the quotes support, edits one timeline entry where the agent's phrasing overstated certainty, strikes an action item that duplicates existing work, and publishes the next morning. The review meeting three days later argues about the fixes, because nobody has to argue about the facts.
Note
Client-safe by design — the drafting step runs against an approved tenancy (Claude on AWS Bedrock, Azure OpenAI) or fully local via Ollama, with hostnames, account IDs, and customer names replaced by stable placeholders before the export reaches a model so quotes stay greppable after redaction. Full setup is in the build guide below.
The flow — where AI plugs in, where value lands
Flow diagram — The Scribe
Blue = AI-added step · Amber = human control point · Gray = your existing tools.
Value at each step
Step
Who acts
What changes
Why it matters
Incident closes
Your incident tooling
The "resolved" event triggers the Scribe automatically
Drafting starts while memory is fresh; nobody has to volunteer
Evidence gathering
Agent (read-only)
Chat export, alert history, and deploy log are pulled into one context
Three hours of channel archaeology drops to zero human minutes
Timeline assembly
Agent
Sources are interleaved into one timestamped sequence, each entry quoting its source
"When did the rollback actually finish" stops being a memory contest
Draft postmortem
Agent
Summary, contributing factors, action items — and an explicit Unknowns list
The blank page is gone; gaps are declared instead of papered over
Claim-by-claim verification
Human
Reviewer ticks, edits, or strikes each VERIFY: checkbox against the quotes
Human judgment is spent where it matters — on truth, not typing
Publication
Human
Verified record ships to the wiki or incident tool
Postmortems land in days, not weeks; review meetings discuss fixes, not facts
Audit
Your existing tools
Draft, edits, and approver identity are all retained
Trust is inspectable, and drafts become a corpus for spotting repeat patterns
The six components in your stack
Component
In this lesson's build
In your stack
Event source
You run scribe.py by hand
Incident-resolved webhook from PagerDuty, incident.io, Opsgenie, or your status tooling
Context stores (read)
Three local mock files: chat export, alerts.json, deploys.log
Slack/Teams export APIs, PagerDuty/Alertmanager alert history, deploy records from any CI (GitHub deployments, GitLab, Argo CD)
Action interface (allowlisted writes)
Writes postmortem-draft.md and nothing else
Create a draft page in Confluence, Notion, or incident.io — the agent never gets the publish permission
Approval channel
The VERIFY: checkboxes inside the draft
Doc review workflow, or a PR against a postmortems repo — the merge is the approval
Verification
You grep each quote against the source files
Claim-by-claim human check; quotes link out to the original message, alert, or deploy record
Audit sink + identity
Draft and sources committed to git
Agent runs under its own service account; draft history, edits, and approver retained in git or the incident tool
Key Takeaway:
The Scribe's safety model is one asymmetry: the agent may draft anything and publish nothing. The draft-with-evidence is the product; the human verification pass is the gate; the publish button never belongs to the agent.
Autonomy tier — start here, promote deliberately
TIER 1 · START AND MOSTLY STAY
Advise — drafts on request
The agent produces drafts, humans publish. This pattern is unusual among the eight in that the ceiling is low by design.
TIER 2 · DELIVERY, NOT PUBLICATION
Auto-post the draft
Sensible promotion is about delivery: let the agent post the draft into the incident channel automatically instead of waiting to be run.
TIER 3 · NOT ON THE LADDER
Publication
A postmortem is an organizational truth claim, and truth claims get a human signature.
Review the drafts-vs-published diff monthly; if reviewers are rubber-stamping without checking quotes, that is a verification failure even when the drafts happen to be right.
Warning
A confident message in the incident channel is a claim, not a fact. "Pretty sure it was DNS" must never become a contributing factor just because someone typed it with conviction — the Scribe's rules send uncorroborated claims to Unknowns, and your verification pass is what keeps that rule honest.
▸Build it yourself — step by step (~60 min, laptop only)optional deep dive — click to expand
You will build a working Scribe against a realistic mock incident: a chat export full of noise and one seductive dead-end, an alert history, and a deploy log. Everything runs locally; the only external dependency is an Anthropic API key.
✅ Checkpoint: python -c "import anthropic, pydantic; print('ok')" prints ok.
No API key? Run it fully local. Install Ollama (from ollama.com), pull a model, and pip install ollama:
ollama pull llama3.1 # good at drafting/summarizing; qwen2.5-coder also works
The Scribe makes exactly one model call — the client.messages.parse(...) you write below — so only that client swaps; the source-loading and render verification interface are plain Python. That call uses a structured Pydantic output, which Ollama supports via its format= argument: pass Postmortem.model_json_schema() and validate the returned JSON with the same Pydantic model. Small local models are more likely to invent quotes, so keep the VERIFY: pass strict and keep Anthropic (claude-opus-4-8, adaptive thinking) as the recommended default.
Step 1 — the mock incident sources (10 min). Create incident/slack-export.txt with exactly this content — note the samosa noise, the Redis dead-end (raised at 14:14, ruled out at 14:23), and the real cause surfacing at 14:40:
[2026-07-08 13:52] priya.k: anyone else's dashboard slow or is it just my wifi
[2026-07-08 13:55] marco.d: works for me. also leftover samosas in the 4th floor kitchen, act fast
[2026-07-08 14:03] PagerDuty BOT: TRIGGERED: CheckoutLatencyP99High - checkout-api p99 > 2s for 5m
[2026-07-08 14:04] priya.k: ack, I'm on call today. looking
[2026-07-08 14:06] PagerDuty BOT: TRIGGERED: DBConnWaitTimeHigh - checkout-api db pool wait p95 > 500ms
[2026-07-08 14:07] priya.k: p99 on /checkout/confirm is 4.8s, normally ~300ms
[2026-07-08 14:09] tanvi.r: EM here - seeing it too, support just pinged me
[2026-07-08 14:11] PagerDuty BOT: TRIGGERED: CheckoutErrorRateHigh - checkout-api 5xx > 2%
[2026-07-08 14:12] priya.k: 5xx now at 4%, mostly 504s from checkout-api upstream
[2026-07-08 14:14] marco.d: redis memory on cache-01 is at 78%, that looks high?
[2026-07-08 14:16] marco.d: maybe eviction storm? checkout uses redis for cart sessions
[2026-07-08 14:18] priya.k: hmm maybe. checking redis dashboards
[2026-07-08 14:21] jess.w: 78% is normal for cache-01 btw, it's been 75-80% for weeks
[2026-07-08 14:23] priya.k: confirmed, redis memory flat vs last tuesday, evictions zero. red herring
[2026-07-08 14:24] marco.d: fair, withdrawn
[2026-07-08 14:25] tanvi.r: status page updated to degraded - checkout
[2026-07-08 14:26] devraj.s: is this the CDN thing from last month again?
[2026-07-08 14:27] priya.k: no, CDN metrics are clean, this is server-side latency
[2026-07-08 14:29] priya.k: db pool wait time went vertical at 14:00. connections to checkout-db look... low?
[2026-07-08 14:31] jess.w: wait, there was a checkout-api deploy at 13:47, version 2026.07.08-1
[2026-07-08 14:33] priya.k: timeline fits. what was in it?
[2026-07-08 14:36] marco.d: pulling the diff
[2026-07-08 14:40] marco.d: found it. the perf PR changed db pool max_connections from 100 to 20 per replica. that value was meant for the staging load test config
[2026-07-08 14:41] priya.k: that would do it. 6 replicas x 20 = 120 conns vs ~600 before
[2026-07-08 14:43] tanvi.r: options?
[2026-07-08 14:44] marco.d: I can push a config-only fix in ~30 min, or we roll back to 2026.07.07-4
[2026-07-08 14:46] priya.k: rollback is faster and known-good. checkout is bleeding
[2026-07-08 14:48] devraj.s: +1 rollback, fix forward tomorrow with proper review
[2026-07-08 14:52] tanvi.r: support says 23 tickets so far
[2026-07-08 15:02] tanvi.r: decision: roll back. marco driving
[2026-07-08 15:05] marco.d: rollback pipeline running
[2026-07-08 15:09] marco.d: rollback deployed, v2026.07.07-4 live on all replicas
[2026-07-08 15:12] priya.k: pool wait time recovering, p99 down to 900ms and falling
[2026-07-08 15:14] priya.k: 5xx back under 0.5%
[2026-07-08 15:17] PagerDuty BOT: RESOLVED: CheckoutLatencyP99High
[2026-07-08 15:18] priya.k: calling it resolved. 75 min end to end
[2026-07-08 15:19] tanvi.r: postmortem doc... who wants it
[2026-07-08 15:20] marco.d: *everyone looks at their shoes*
[2026-07-08 15:24] devraj.s: samosas gone btw
Your stack: these three files stand in for the Slack/Teams export API, your PagerDuty/Alertmanager alert history, and your CI's deployment record (GitHub deployments API, GitLab, Argo CD history). The Scribe only ever reads them.
✅ Checkpoint: ls incident/ shows all three files; wc -l incident/slack-export.txt shows 39 lines.
Step 2 — the Scribe itself (15 min). Create scribe.py:
"""The Scribe — post-incident artifact assembly with claim-by-claim verification."""
import json
from pathlib import Path
import anthropic
from pydantic import BaseModel
INCIDENT_DIR = Path("incident")
SOURCES = {
"slack-export.txt": "Chat channel export (#inc-checkout-latency)",
"alerts.json": "Alert history (fired/resolved times)",
"deploys.log": "Deploy log",
}
class TimelineEntry(BaseModel):
ts: str
event: str
source_quote: str
class ContributingFactor(BaseModel):
factor: str
evidence: str
class ActionItem(BaseModel):
item: str
owner_role: str
rationale: str
class Postmortem(BaseModel):
summary: str
timeline: list[TimelineEntry]
contributing_factors: list[ContributingFactor]
action_items: list[ActionItem]
unknowns: list[str]
SYSTEM = """You are an SRE scribe drafting a blameless postmortem from raw incident sources.
Hard rules:
1. EVIDENCE OR NOTHING. Every timeline entry's source_quote and every contributing
factor's evidence MUST be a verbatim quote copied exactly from the sources.
Never paraphrase inside a quote. Never invent timestamps.
2. Anything plausible but not directly evidenced goes in `unknowns` — never into
the timeline or contributing factors. Do not guess. Do not fill gaps.
3. A confident statement in chat is a claim, not a fact. Treat it as established
only if corroborated — by another source, a quoted dashboard reading, or the
deploy log. Theories that were raised and then ruled out in the sources are
dead ends: exclude them from contributing factors entirely.
4. BLAMELESS: refer to people only by role (the on-call engineer, a service
developer, the engineering manager). Never use names or handles.
5. If a listed source is missing, record that in `unknowns` (for example:
"Alert history unavailable — fire/resolve times could not be verified").
6. Timeline in chronological order, timestamps exactly as they appear in sources.
"""
def load_sources() -> str:
blocks = []
for fname, label in SOURCES.items():
path = INCIDENT_DIR / fname
if path.exists() and path.stat().st_size > 0:
blocks.append(f"=== SOURCE: {label} ({fname}) ===\n{path.read_text()}")
else:
blocks.append(
f"=== SOURCE: {label} ({fname}) ===\n[MISSING - this source was not provided]"
)
return "\n\n".join(blocks)
def render(pm: Postmortem) -> str:
lines = ["# Postmortem draft — VERIFY EVERY CLAIM BEFORE PUBLISHING", ""]
lines += ["## Summary", "", pm.summary, ""]
lines += ["## Timeline", ""]
for t in pm.timeline:
lines.append(f"**{t.ts}** — {t.event}")
lines.append(f'- [ ] VERIFY: source quote — "{t.source_quote}"')
lines.append("")
lines += ["## Contributing factors", ""]
for f in pm.contributing_factors:
lines.append(f"- {f.factor}")
lines.append(f' - [ ] VERIFY: evidence — "{f.evidence}"')
lines += ["", "## Action items", ""]
for a in pm.action_items:
lines.append(f"- {a.item} (owner: {a.owner_role})")
lines.append(f" - [ ] VERIFY: rationale — {a.rationale}")
lines += ["", "## Unknowns (not evidenced — do not publish as fact)", ""]
for u in pm.unknowns:
lines.append(f"- {u}")
return "\n".join(lines) + "\n"
def main() -> None:
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=4096,
thinking={"type": "adaptive"},
system=SYSTEM,
messages=[
{
"role": "user",
"content": "Draft the postmortem from these sources.\n\n" + load_sources(),
}
],
output_format=Postmortem,
)
pm = response.parsed_output
Path("postmortem-draft.md").write_text(render(pm))
print(
f"Wrote postmortem-draft.md: {len(pm.timeline)} timeline entries, "
f"{len(pm.contributing_factors)} contributing factors, "
f"{len(pm.action_items)} action items, {len(pm.unknowns)} unknowns"
)
if __name__ == "__main__":
main()
Two things to notice before running it. The Pydantic model is the contract — client.messages.parse guarantees the draft arrives in that shape or not at all. And the render function is the human verification interface: one VERIFY: checkbox per claim, which is what makes the review a checklist instead of a re-investigation.
Step 3 — run it (5 min).
python scribe.py
✅ Checkpoint: the script prints something like Wrote postmortem-draft.md: 12 timeline entries, 3 contributing factors, .... Open postmortem-draft.md and confirm: (1) the timeline runs from the 13:47 deploy through the 15:17 resolution; (2) people appear as roles, not as priya.k or marco.d; (3) the Redis theory does not appear as a contributing factor — it was raised and ruled out in the sources, and a Scribe that promotes a dead-end to a factor has failed the core test.
Step 4 — the verification pass (10 min). Now do the human half. Every quote in the draft must exist verbatim in a source:
grep -F "changed db pool max_connections from 100 to 20" incident/slack-export.txt
✅ Checkpoint: every quote greps successfully in exactly one source file. If any quote returns nothing, you have caught a fabrication — that is the verification gate doing its job, and in production it would be a demotion trigger. Tick the checkboxes as you go; a claim whose box you cannot tick gets edited or struck, never published.
Your stack: in production this pass lives wherever your postmortems live — a PR review on a postmortems repo, or a draft page in Confluence/Notion where each quote links back to the source message or alert.
Step 5 — red-team exercise A: the confident liar (7 min). Insert this line into incident/slack-export.txt after the 14:18 message:
[2026-07-08 14:19] devraj.s: pretty sure it was DNS, it's always DNS
Re-run python scribe.py.
✅ Checkpoint: DNS must not appear as a contributing factor. Acceptable outcomes: the message is ignored, or it lands in unknowns as an uncorroborated theory, or it is noted as contradicted (nothing else in any source implicates DNS, and the deploy log plus pool-wait evidence point elsewhere). If your draft adopted DNS as a factor, your system prompt's rule 3 is being ignored — tighten it and re-run. This exercise is the whole pattern in miniature: confidence in the channel is not evidence.
Step 6 — red-team exercise B: the missing source (5 min). Simulate an integration outage:
✅ Checkpoint: the script still completes (the load_sources function degrades to a [MISSING] block rather than crashing), and unknowns now contains an explicit note that the alert history was unavailable and fire/resolve times are unverified. The timeline should survive using chat and deploy-log timestamps only. A Scribe that silently produces an equally confident draft with a source missing is worse than one that crashes. Restore the file: mv incident/alerts.json.bak incident/alerts.json.
Step 7 — closing artifact (5 min). Write autonomy-memo.md, five lines, the way you would for a real rollout: current tier (advise — drafts only), the promotion you would consider (auto-post draft to the incident channel after four clean incidents), your three demotion triggers (fabricated quote, named individual, dead-end promoted to factor), and who reviews the drafts-vs-published diff monthly. This memo — not the code — is what you would bring to your team.
Client-safe variant: if incident channels or postmortems are contractually sensitive, run the identical script against an approved tenancy — Anthropic's Claude on AWS Bedrock or a client-approved Azure OpenAI deployment (swap the SDK call accordingly), or fully local via Ollama with a capable open model for non-production drills — and redact identifiers first: replace hostnames, account IDs, and customer names in the export with stable placeholders (cache-01 → CACHE-A) so quotes remain greppable after redaction.
Pro Tip
Keep the quote-grep from Step 4 as a script and run it in CI on every draft. "Every quote must grep in a source" is a postmortem test suite — it mechanically catches fabrication before a human even starts the verification pass.
This is the free reading view. Open BXUP
to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.