After the incident is over, the value lives in what you decide to change. A postmortem that assigns blame gets a defensive team; one that asks “what can we change so this does not happen again” gets durable improvement.
Blame kills the honest report
The moment anyone asks ‘who is to blame’ the room quiets and the timeline shortens. People retell only what makes them look safe, so the record loses the decisions nobody wants to own. A blame-free report treats the timeline as a fixed record of what happened and treats people as the working part of a system with real constraints, not as its cause of failure.
Two mechanisms do most of the damage. Hindsight bias turns every prior step into an obvious wrong call, even though the people in the moment had no knowledge of the outcome. And stopping at a single person’s action hides the system underneath: the missing check, the overloaded shift, the ambiguous handoff, the budget that routed the work together. A useful postmortem keeps asking why until the answer is a design decision or a process gap that can be changed, not a name.
Structures of the write-up
The durable write-up has four parts. The timeline is a neutral, evidence-backed sequence of who did what and what they observed, kept separate from interpretation. The causes section separates the triggering event from the contributing conditions, and the deeper fix usually lives in the conditions. The action list is where the value hides: each item carries one owner, a due date, and a proof that it works, written as a task that fails if it is skipped rather than a to-do that evaporates.
Two review gates keep it honest. The first rereads the timeline against the actions and asks whether each contributing cause got a change, not just the most-visible symptom. The second weeds out unverifiable actions like wait and see, and converts them into concrete work: add the check, tighten the alert, add the test, update the runbook. An action without an owner and a verification step is a wish, not a change.
The follow-up is the point
The meeting is the midpoint, not the endpoint. A postmortem whose actions get no follow-up repeats; weeks later the pager produces the same shape and the team only notices because it pages again. What closes the loop is a scheduled review about a week after the incident, where each owner reports on the accepted change that landed. Some changes fail under real traffic, and the report should record that too, because a follow-up that tracks the change and replaces it with a better one is still a closed loop.
Teams that run this well keep a short, recurring review slot in the calendar and judge retrospectives by whether the change landed and was verified, not by how good the meeting felt. The useful signal is a repeat-cause count that falls, not an incident count that falls because people stopped reporting honestly. A learning team may look worse before it looks better, and protecting that honesty is the whole point.
When it breaks
A blame-shaped review fails in four predictable ways. First, the timeline shrinks: defensive edits remove uncertain observations, so the record jumps from alert to fix with no decisions in between. If nobody’s confusion, retry, or handoff appears, the timeline was filtered, not recorded. Second, actions have no owner and no proof: items read like improve monitoring with no name, no date, and no check. The root cause is treating the document as the outcome instead of the change; recognize it when every action could be marked done without touching production. Third, the fix targets only the trigger: the deploy is rolled back but the missing guardrail, flaky alert, or unclear runbook stays. Ask which contributing condition each action changes; a list that maps one-to-one onto the trigger is incomplete. Fourth, follow-up dies quietly: the review is never scheduled, owners drift, and the same cause pages again within weeks. A repeat shape with an open action list is the signature.
Check it quickly
Save the following as postmortem_check.py and run python3 postmortem_check.py. It checks two in-memory reviews using only the standard library and writes no files.
def check_review(doc):
errors = []
for key in ("timeline", "causes", "actions"):
if not doc.get(key):
errors.append(f"missing {key}")
for i, action in enumerate(doc.get("actions", [])):
for field in ("owner", "due", "proof"):
if not str(action.get(field, "")).strip():
errors.append(f"action {i} missing {field}")
return errors
GOOD = {
"timeline": ["00:01 alert fired", "00:07 rollback done"],
"causes": {"trigger": "bad deploy", "conditions": ["no canary check"]},
"actions": [
{"task": "add canary check", "owner": "ana", "due": "2027-03-12", "proof": "CI blocks deploy without canary"},
],
}
BAD = {
"timeline": ["00:01 alert fired"],
"causes": {"trigger": "bad deploy", "conditions": []},
"actions": [
{"task": "be more careful", "owner": "", "due": "soon", "proof": ""},
],
}
def main():
good_errors = check_review(GOOD)
bad_errors = check_review(BAD)
assert good_errors == [], f"good sample should pass, got: {good_errors}"
assert any("missing owner" in e for e in bad_errors), f"bad sample should flag owner, got: {bad_errors}"
assert any("missing proof" in e for e in bad_errors), f"bad sample should flag proof, got: {bad_errors}"
print("PASS: good review has timeline, causes, and fully owned actions")
print(f"FAIL as expected: bad review -> {'; '.join(bad_errors)}")
print("summary: 1 passed check, 1 failed check (expected 1 and 1)")
if __name__ == "__main__":
main()
The good sample passes with timeline, causes, and fully owned actions; the bad sample fails on a missing owner and empty proof. Expect one PASS, one FAIL naming the fields, and a one-passed-one-failed summary.
Further reading
Sources
- Google SRE: postmortem culture, blame-free learning, checked 2027-03-02.
- Atlassian: blameless postmortems, why blame harms learning, checked 2027-03-02.
- PagerDuty: incident response, pairing an incident record with learning, checked 2027-03-02.