After an incident, a postmortem should explain what happened, what people knew at the time, and which changes could reduce future harm. Blamelessness makes candid reporting safer. It does not guarantee that an incident cannot recur, remove ownership, or turn every explanation into an established fact. Learning requires both an honest account and evidence that useful follow-up actually happened.

Reconstruct the decision context

Hindsight makes an earlier decision look obvious once its consequences are known. Ask what signals, documentation, time pressure, and alternatives were available when the decision was made. An operator’s action belongs in the timeline when relevant, but stopping at that person’s name leaves the conditions that made the action reasonable or likely unexplored.

A timeline is an evidence-backed reconstruction, not an immutable transcript. Preserve logs and messages, distinguish observation from inference, and mark uncertain timestamps or conflicting accounts. Correct the account when stronger evidence appears. A short timeline is a reason to investigate missing context, not proof that somebody deliberately hid information. Accusing authors of concealment can reproduce the blame the review is intended to avoid.

Connect consequences, conditions, and actions

Begin with customer impact and how it was measured. Record detection, mitigation, and recovery separately: a rollback command finishing does not establish that customer traffic recovered at that exact moment. Describe the triggering change alongside contributing conditions, such as a missing limit or a dependency that made a local error spread.

Choose a small, prioritized set of actions tied to those conditions. Useful actions may prevent recurrence, improve detection, limit impact, or make recovery safer. Not every contributing factor needs its own production change; investigate uncertainty and document accepted tradeoffs. A runbook correction or training exercise can be valuable when it addresses a concrete failure, rather than serving as a generic instruction to be more careful.

Give each accepted action one accountable owner, a target date, a tracking reference, and a verifiable outcome. The owner coordinates completion; naming them does not blame them for the incident. A verification statement should describe an observable result, such as a controlled test rejecting an unsafe release, rather than simply stating that a document was written.

Follow through without inventing certainty

Schedule follow-up according to risk and delivery time. A review a week later can be useful, but no universal seven-day deadline fits every incident. Check whether each change landed, whether its validation passed, and whether it produced new problems. If the proposed fix fails, keep the work open with an updated plan rather than declaring the loop closed because another meeting occurred.

Recurring incidents deserve investigation even when previous actions were completed. The original explanation may have been incomplete, a mitigation may only reduce impact, or conditions may have changed. Compare consequences and contributing factors as well as counts. Fewer reports alone cannot establish improvement, particularly if reporting incentives have changed.

When it breaks

Vague tasks can survive every meeting because nobody can tell whether they are finished. Overly ambitious actions can remain open indefinitely because their priority and staffing never matched the commitment. Review feasibility when accepting work and record explicit deferrals instead of silently losing them.

A checklist can also become a substitute for learning. Filled fields do not establish a sound causal explanation, a safe proposed change, or a trustworthy verification method. Keep sensitive customer data and credentials out of broadly shared reports; preserve necessary evidence with appropriate access rather than copying everything into the narrative.

The metadata checker accepts both the specific canary action and the shallow be more careful action. It rejects missing owner or proof and invalid dates. Human review must connect evidence, contributing conditions and a verifiable outcome.
Complete fields can still hide a weak action. Static illustration of the source and its limits. Reported fixture outcomes were not rerun for this graphic.

Check it quickly

Save the following as postmortem_check.py and run python3 postmortem_check.py. Python 3.7 or newer checks three synthetic review records and additional date controls using only its standard library. Run without -O, -OO, or a nonzero PYTHONOPTIMIZE setting so the assertions execute. The accepted input shape is a dictionary with an action list of dictionaries; this is a local metadata exercise, not a general untrusted-document validator.

from copy import deepcopy
from datetime import date
import re


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 ("task", "owner", "proof"):
            value = action.get(field)
            if not isinstance(value, str) or not value.strip():
                errors.append(f"action {i} missing {field}")
        due = action.get("due")
        try:
            if not isinstance(due, str) or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", due):
                raise ValueError("date format")
            date.fromisoformat(due)
        except ValueError:
            errors.append(f"action {i} invalid due date")
    return errors


GOOD = {
    "timeline": ["00:01 alert fired", "00:07 rollback completed"],
    "causes": {"trigger": "deployment", "conditions": ["no canary gate"]},
    "actions": [{"task": "add a canary gate", "owner": "ana",
                 "due": "2026-10-22", "proof": "staging test blocks a bad release"}],
}
BAD = deepcopy(GOOD)
BAD["actions"][0].update(owner=None, due="soon", proof="")
SHALLOW = deepcopy(GOOD)
SHALLOW["actions"][0].update(task="be more careful", proof="manager agrees")

assert check_review(GOOD) == []
assert check_review(BAD) == ["action 0 missing owner", "action 0 missing proof",
                             "action 0 invalid due date"]
assert check_review(SHALLOW) == []  # Metadata completeness cannot judge quality.
for invalid in ("2026-02-30", "20261022", None):
    candidate = deepcopy(GOOD)
    candidate["actions"][0]["due"] = invalid
    assert check_review(candidate) == ["action 0 invalid due date"]
print("PASS: complete metadata accepted; missing owner/proof and invalid dates rejected")
print("LIMIT: a vague action still passes; causal review requires human judgment")

The complete record passes. A null owner, empty proof, informal deadline, impossible calendar date, compact date, and null date are rejected. Crucially, the vague action still passes because its fields are populated. The final LIMIT line exposes that deliberate boundary: metadata validation supports review, but cannot certify learning or prevent another incident.

Further reading

Sources