Module 6: Postmortems And Iterating

Timeline, contributing factors, and action items: a postmortem's structure

Description

The previous lesson installed the principle — investigate the system, not the person — but a principle, on its own, doesn't produce a useful document. This lesson builds the module's central piece: buildPostmortem(), the function that takes an incident's raw information — timestamped events, free-text contributing factors, proposed action items — and organizes it into a postmortem's canonical structure: a chronologically ordered timeline, contributing factors (with lesson 2's blameless check already built in), and action items with an owner and a date. The same function also verifies the full result: if any contributing factor names a person, the entire postmortem gets flagged as not blameless.

Connection to the module. This is the function lesson 4 is going to reuse without changing a line — the same pattern you already saw in earlier modules of this guide, where isEnabled(), guardrailCheck(), or rolloutPlan() get built once and reused in the following lessons. Lesson 8's project is going to run this exact function on the full recommendations incident, with no modification.

An analogy: the black box, now with the three tapes together

The previous lesson's analogy — the plane's black box — had two recorders: the flight data one (what the system did, second by second) and the cockpit voice one (what the crew said and decided). An aviation investigation report doesn't report those two tapes separately, or without order: it synchronizes them into a single timeline, adds why — the factors the system and the training allowed — and closes with what changes — the safety recommendations, with a responsible body to implement them and a deadline. Without those three pieces together, the black box is just a pile of data; with them, it's a document another pilot, in another cockpit, can use to avoid repeating the same incident.

buildPostmortem() does exactly that synthesis with the recommendations incident: it takes the loose events (the black box), orders them into a readable timeline, adds the contributing factors with their blameless check, and closes with the action items — Mercado's team's "safety recommendations," each with its owner and its date.

Worked example: buildPostmortem() on the real recommendations incident

// buildPostmortem: given an incident's raw timeline (timestamped events, out of
// order) and its contributing factors, assembles a structured postmortem --
// ordered timeline + contributing factors + action items -- and flags ANY
// factor that names a specific person (instead of a system or process) as a
// red flag: the signal that the postmortem stopped being blameless. Pedagogical
// model, not a complete postmortem template.
function buildPostmortem(incident) {
  const timeline = [...incident.events].sort((a, b) => a.time.localeCompare(b.time));
  const namedPeople = ['Ana', 'Bruno', 'Carla', 'Diego', 'Elena'];
  const factors = incident.contributingFactors.map((description) => {
    const redFlag = namedPeople.some((name) => description.includes(name));
    return { description, redFlag };
  });
  const blameless = factors.every((f) => !f.redFlag);
  return {
    incidentName: incident.name,
    timeline,
    contributingFactors: factors,
    actionItems: incident.actionItems,
    blameless,
  };
}

// The real incident: recommendations's latency regression, exactly as module 5
// left it. The events are deliberately NOT in order -- that's exactly what
// buildPostmortem() has to resolve.
const latencyIncident = {
  name: 'recommendations: p95Latency breaks the guardrail at the 10% stage',
  events: [
    { time: '14:20', what: 'The incident is declared (Sev-2 severity, following module 5\'s runbook).' },
    { time: '13:58', what: 'The rollout advances from canary (1%, clean) to the 10% stage.' },
    { time: '16:30', what: 'The technical cause is identified: the call to the recommendation engine is synchronous/blocking and doesn\'t scale to the 10% stage\'s volume.' },
    { time: '14:12', what: 'guardrailWatch() flags HALT at the 10% stage: p95Latency=910ms, ceiling 800ms.' },
    { time: '16:42', what: 'The incident is closed: rollbackDecision() confirms exposure at 0% and the guardrail is no longer at risk.' },
    { time: '14:15', what: 'The team activates the kill switch: recommendationsFlag.enabled flips to false.' },
    { time: '14:45', what: 'Exposure at 0% is confirmed: no new user sees the variant since the kill switch.' },
  ],
  contributingFactors: [
    'The call to the recommendation engine is synchronous and blocking; it wasn\'t designed for the traffic volume of the rollout\'s 10% stage.',
    'The ramp\'s advance criteria (guardrailWatch) didn\'t include a load test equivalent to the next stage\'s volume before advancing.',
    'No cache existed for the most-requested recommendations, so every request recalculated the engine\'s full result.',
  ],
  actionItems: [
    { owner: 'recommendations-team', action: 'Redesign the call to the recommendation engine in async / non-blocking mode.', due: '2026-08-08' },
    { owner: 'platform-sre', action: 'Add a load test equivalent to the next stage\'s volume as part of guardrailWatch()\'s advance criteria.', due: '2026-08-08' },
    { owner: 'recommendations-team', action: 'Implement caching for the most-requested recommendations.', due: '2026-08-15' },
  ],
};

const postmortem = buildPostmortem(latencyIncident);

console.log('=== Postmortem: ' + postmortem.incidentName + ' ===\n');
console.log('--- Timeline (ordered) ---');
postmortem.timeline.forEach((e) => console.log(e.time + '  ' + e.what));

console.log('\n--- Contributing factors ---');
postmortem.contributingFactors.forEach((f, i) => console.log((i + 1) + '. [' + (f.redFlag ? 'RED FLAG' : 'blameless') + '] ' + f.description));

console.log('\n--- Action items ---');
postmortem.actionItems.forEach((a, i) => console.log((i + 1) + '. (' + a.owner + ', due ' + a.due + ') ' + a.action));

console.log('\nPostmortem blameless: ' + postmortem.blameless);

What to expect. Running the file with Node, the output is exactly this:

=== Postmortem: recommendations: p95Latency breaks the guardrail at the 10% stage ===

--- Timeline (ordered) ---
13:58  The rollout advances from canary (1%, clean) to the 10% stage.
14:12  guardrailWatch() flags HALT at the 10% stage: p95Latency=910ms, ceiling 800ms.
14:15  The team activates the kill switch: recommendationsFlag.enabled flips to false.
14:20  The incident is declared (Sev-2 severity, following module 5's runbook).
14:45  Exposure at 0% is confirmed: no new user sees the variant since the kill switch.
16:30  The technical cause is identified: the call to the recommendation engine is synchronous/blocking and doesn't scale to the 10% stage's volume.
16:42  The incident is closed: rollbackDecision() confirms exposure at 0% and the guardrail is no longer at risk.

--- Contributing factors ---
1. [blameless] The call to the recommendation engine is synchronous and blocking; it wasn't designed for the traffic volume of the rollout's 10% stage.
2. [blameless] The ramp's advance criteria (guardrailWatch) didn't include a load test equivalent to the next stage's volume before advancing.
3. [blameless] No cache existed for the most-requested recommendations, so every request recalculated the engine's full result.

--- Action items ---
1. (recommendations-team, due 2026-08-08) Redesign the call to the recommendation engine in async / non-blocking mode.
2. (platform-sre, due 2026-08-08) Add a load test equivalent to the next stage's volume as part of guardrailWatch()'s advance criteria.
3. (recommendations-team, due 2026-08-15) Implement caching for the most-requested recommendations.

Postmortem blameless: true

Read the three sections in the order buildPostmortem() produces them, because that order is the postmortem's logic. The timeline reconstructs, chronologically, exactly what happened — from the advance to 10% at 13:58 through the incident's closure at 16:42 — with no interpretation yet, just ordered facts. The contributing factors take a step further: they explain why the timeline unfolded that way — three technical and process causes, none of which names a person, so lesson 2's automatic check marks all three blameless. The action items close the loop: each contributing factor has at least one concrete action attacking it, with an owner (recommendations-team, platform-sre) and a date (2026-08-08, 2026-08-15), not a vague intention to "be more careful."

Going deeper: why the timeline → factors → action items order isn't arbitrary

It's worth noting that these three sections couldn't be written in any other order without losing rigor. If a team tried to write the action items first — "we're going to redesign the call to the recommendation engine" — without having first reconstructed the full timeline and contributing factors, it would risk fixing a symptom without having confirmed the real cause: maybe the problem wasn't the blocking call, but the lack of caching, or the advance criteria without a load test — or all three at once, as actually happened here. The timeline and the contributing factors are the evidence that justifies each action item; without them, an action item is a hunch dressed up as a plan.

Also notice something this example's output shows clearly: three contributing factors produce three action items, each directly attacking one of the factors. This isn't a coincidence or a rigid requirement of buildPostmortem() — the function accepts any number of each — but it is a good practice worth imitating: a postmortem that identifies five contributing factors and produces a single vague action item probably didn't attack the other four causes, even if it named them.

Common mistakes

Writing the contributing factors without connecting them to any specific event on the timeline. What happens: the postmortem lists general causes — "lack of load tests," "non-scalable architecture" — without any of them being traceable to a concrete moment on the timeline that confirms them. Why it happens: it's faster to write causes in the abstract than to review the full timeline looking for which specific event reveals each cause. How to spot it: if you ask "at what point on the timeline does this show up?" about a contributing factor and nobody can point to a concrete event, the factor might be an assumption, not a cause confirmed by evidence. How to fix it: as in today's example, every contributing factor should be traceable to at least one event on the timeline — the blocking-call factor is directly confirmed by the 16:30 event, it isn't a loose theory.

Leaving an action item without an owner or a date, "to define later." What happens: the postmortem ends with a list of good intentions — "we should add caching," "it'd be good to review the advance criteria" — without any of them having an assigned who or when. Why it happens: at the moment of closing the postmortem, after a long investigation, it's tempting to leave execution details for "a follow-up meeting" that often never happens. How to spot it: if an action item doesn't have an owner with a team name (not an individual person's name, to keep it blameless) and a concrete date, it's, in practice, indistinguishable from not having written it at all. How to fix it: as in buildPostmortem(), every actionItem has owner and due as mandatory fields of the structure — a postmortem that can't fill in those two fields for an action item probably hasn't finished investigating the cause enough to know what to do about it.

Confusing "many contributing factors" with "the incident was nobody's fault in particular, so there's no need to dig into any of them." What happens: seeing that there are three distinct causes — the blocking call, the advance criteria, the lack of caching — someone concludes that since "there are many causes," none is important enough to attack thoroughly, and the action items end up superficial on all three fronts. Why it happens: responsibility distributed across several causes mistakenly feels like diluted responsibility — as if three "minor" causes added up to less urgency than one "big" cause. How to spot it: if all three action items propose partial or low-effort fixes for the three causes, instead of a real fix for each, the investigation fell short. How to fix it: a system that fails from the combination of three factors — none sufficient on its own, but all three together are — needs all three fixed, not a diluted version of each. This example's three action items each attack one full cause, not a superficial patch.

Exercises

Exercise 1 — Add an event to the timeline. Mercado's team discovers that, at 15:30, someone from customer support reported "some buyers mention the page is slow to load" — a report that arrived before guardrailWatch() confirmed the HALT at 14:12... wait, does that make sense? Check the times carefully and explain what continuity error adding this event as described would create.

See solution

The event has a continuity error: it says it arrived "before guardrailWatch() confirmed the HALT at 14:12," but the timestamp given is 15:30, which is after 14:12, not before. If the event really happened at 15:30, buildPostmortem() would correctly order it between the 14:45 event and the 16:30 one — not before the HALT — regardless of what the text description says. This exercise is an important reminder: buildPostmortem() orders by the time field, not by the order in which someone writes down or narrates the events — if an event's prose description doesn't match its own timestamp, you fix the data, not the function.

Exercise 2 — Spot the red flag. A teammate proposes adding this contributing factor to the postmortem: "Carla temporarily disabled the latency alert the day before to test an unrelated change, and forgot to re-enable it." Mentally run buildPostmortem()'s check on this sentence. What would it return, and how would you rewrite it so it passes the check without losing the real fact?

See solution

buildPostmortem() would flag this factor with redFlag: true, because the sentence contains the name "Carla" — one of the namedPeople — and the entire postmortem would become blameless: false. A reasonable systemic rewrite: "The process didn't require an automated confirmation that latency alerts were active before advancing the rollout to a new stage, which let a temporary disable for testing go unnoticed." The underlying fact — an alert was disabled when it was needed — is fully preserved; what changes is that the cause points at the lack of an automated check, not at a person "forgetting" something, which could happen to anyone under the same system.

Exercise 3 — Design a complete action item. The team identifies a fourth contributing factor buildPostmortem() doesn't yet have in this example: "Module 4's monitoring dashboard didn't show a projection of what would happen to latency if the rollout advanced to the next stage, only the current state." Write a complete action item — owner, action, due — that directly attacks this cause.

See solution

A reasonable action item: { owner: 'platform-sre', action: 'Add to the monitoring dashboard (module 4) a simple projection of the latency guardrail extrapolated to the rollout\'s next stage, before enabling the advance button.', due: '2026-08-22' }. Notice the structure: the owner is a team (not a person), the action is specific and describes exactly what gets built (a projection on the dashboard, not "improve monitoring" in general), and the due is a concrete date — not "soon" or "next sprint" with nothing more.

Summary and next step

In this lesson you built and ran buildPostmortem(), the module's central function: it converts an incident's raw events, factors, and action items into the canonical structure — ordered timeline, contributing factors with their blameless check, action items with owner and date — and verifies the full result. On the real recommendations incident, you saw the seven events ordered from 13:58 through 16:42, the three contributing factors — all technical or process-related, none pointing at a person — and the three action items directly attacking them, closing with blameless: true.

Before moving on you should be able to: explain why the timeline → factors → action items order isn't arbitrary; identify when an action item is incomplete for lacking an owner or a date; and detect, in a new sentence, whether a contributing factor names a person instead of pointing at the system.

Lesson 4 digs into the question this example left open: buildPostmortem() can detect a proper name in a factor, but why is language — beyond whether it literally contains a name or not — what truly determines whether a team's culture is blameless?

Resources

  • Google SRE Book, Chapter 15, "Postmortem Culture: Learning from Failure" — sre.google/sre-book/postmortem-culture. The section on what a good postmortem includes — summary, impact, root cause, follow-up actions — is the formal reference for the structure buildPostmortem() implements in code. In English.
  • Atlassian, "A Guide to the Incident Postmortem Process" — atlassian.com/incident-management/postmortem/templates. Real postmortem templates with the same three sections — timeline, causes, actions — used in industry practice. In English.
  • GitHub, "postmortem-templates" collection — github.com/dastergon/postmortem-templates. A collection of public postmortem templates from real companies (including Etsy's and others), useful for comparing how different teams structure the same three pieces. In English.