A Decision Record Without Its Assumptions Is Just a Verdict

Open almost any decision log and you will find verdicts.

We chose Postgres. Approved: migrate the billing service to the new platform. Decision: use a managed queue for notifications. Sometimes there is a date, sometimes a name, occasionally a paragraph of rationale written in the past tense by someone who was already convinced.

Then, a year later, someone asks the only question a decision record is for: should we still be doing this? And the record cannot answer. It tells you what was decided. It does not tell you what had to be true for that to be the right call, so there is nothing to check against today.

The first post in this series argued that decisions you don't write down become debt. Writing them down turned out to be the easy part. What you write is where most records quietly fail, and the failure is not obvious, because the log looks complete. Every decision is there. None of them can be revisited.

The verdict is the least useful part

This is the counterintuitive bit. The verdict is the part everyone records, and it is the part you need least.

The system already shows what was chosen. The database is Postgres. The billing service runs on the new platform. Anyone can see the verdict by looking at what exists. What nobody can see by looking is why it exists that way, what else was considered, and what the people in the room were assuming about the world when they chose.

That missing half is exactly what decides whether a decision ages well. A choice that was right under one set of conditions becomes wrong when the conditions change, and if the conditions were never written down, nobody notices the moment they did.

The whole deliberation is not the answer either

The obvious fix is to record more. Capture the discussion, the meeting notes, the full reasoning, every argument for and against.

I have seen that tried, and it fails the other way. A full deliberation gets recorded once and read never. It is twenty pages that nobody opens, and when somebody finally does, the one sentence that matters is buried under everything that did not.

The process matters. But only a narrow slice of it earns a place in the record: the part that lets someone, later, tell whether the decision is still standing on solid ground.

Six fields that earn their place

After a good number of these, these are the sections I keep.

Context. What was known at the time, and dated. Not the full history, just enough that a reader a year from now can judge the decision fairly, against what was knowable then rather than what is obvious now.

Decision. The verdict, in one or two sentences. It is the least informative field and it should be the shortest.

Options rejected. What else was on the table and the one reason each lost. This is the field that stops the same debate from happening again in eighteen months, because the next person to propose the rejected option can see why it lost and whether that reason still holds.

Assumptions. What has to remain true for this to be the right decision. Volume stays under a threshold. A provider keeps a guarantee. A team keeps owning a component. This is the heart of the record, and it is the field most records do not have.

Expected outcome. What we believe will happen if we are right. A number if possible, an observable change if not.

Reopen when. The specific, observable conditions that should send someone back to this decision. Not a date on a calendar, which is a ritual somebody has to remember to hold, but a signal: a metric crossing a line, a contract renewal, a dependency changing hands.

Expected outcome and assumptions do different jobs

A suggestion I hear often is to record the expected outcome and later compare it with what actually happened. That is how you find out whether the decision was right.

It is a good field, and I keep it. But on its own it judges the wrong thing.

An outcome mixes the quality of a decision with luck. A sound decision can go badly and a poor one can work out, and comparing expected with observed cannot tell you which of those happened. Poker players have a name for judging decisions by their results, and it is not a compliment.

What separates the two is the assumptions. If they held and the outcome was still bad, that was variance, and the decision may have been fine. If they broke, the decision should have been reopened, and the question is why nobody noticed.

There is also a difference in timing that matters even more in practice. The outcome usually arrives late, months or years after the decision, when changing course is expensive. The assumptions can be checked while the decision is still live. So the two fields do different jobs. The expected outcome judges the decision afterwards. The assumption tells you when to reopen it in time.

The template

This is the shape I use, with a filled-in example. The numbers are illustrative; the structure is the point.

# 0007. Use a task queue for outbound notifications

Status: accepted · 2026-03-12 · Owner: platform team

## Context

- Notifications are sent by a scheduled job.
- Production shows duplicates.

## Decision

Move outbound notifications to a managed task queue.

## Options rejected

- Keep the scheduler and add a lock: fixes duplicates, not retries.

## Assumptions

- Volume stays under 50k messages per day.
- The provider's dedup window (1 hour) is longer than our worst retry gap.

## Expected outcome

- Zero duplicates in a month of production traffic.

## Reopen when

- Daily volume exceeds 40k for a week.
- A retry gap longer than 45 minutes is observed.

Two details are deliberate. The assumptions and the reopen conditions mirror each other: each reopen condition is an assumption becoming false, caught a little before it breaks. And the reopen conditions are observable signals, not review dates, because nothing happens on a decision's anniversary.

Make the record enforce itself

A template is a suggestion, and suggestions erode. The second post in this series argued that a boundary you don't enforce is just a wish, and decision records are no exception. Left to good intentions, the first records are complete, and by the tenth the assumptions section says "TBD".

So the check lives in CI. It refuses any record that is missing one of the six sections, or whose assumptions and reopen conditions contain no real entries.

// scripts/check-adr.mjs
// Fails CI if a decision record is missing the sections that make it checkable later.
// Usage: node scripts/check-adr.mjs [dir]   (default: docs/adr)
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

const dir = process.argv[2] ?? 'docs/adr'
const REQUIRED = [
  'Context',
  'Decision',
  'Options rejected',
  'Assumptions',
  'Expected outcome',
  'Reopen when',
]
// Sections that must contain at least one real bullet, not a placeholder.
const MUST_HAVE_BULLETS = ['Assumptions', 'Reopen when']

const sections = text => {
  const out = new Map()
  let current = null
  for (const line of text.split('\n')) {
    const heading = line.match(/^##\s+(.+?)\s*$/)
    if (heading) {
      current = heading[1]
      out.set(current, [])
    } else if (current) {
      out.get(current).push(line)
    }
  }
  return out
}

const isPlaceholder = s => /^(-|\*)\s*(tbd|todo|n\/a|none|\.\.\.)?\s*$/i.test(s.trim())

const problems = []
const files = readdirSync(dir).filter(f => /^\d{4}-.+\.md$/.test(f))

for (const file of files) {
  const parsed = sections(readFileSync(join(dir, file), 'utf8'))
  for (const name of REQUIRED) {
    if (!parsed.has(name)) problems.push(`${file}: missing "## ${name}"`)
  }
  for (const name of MUST_HAVE_BULLETS) {
    const bullets = (parsed.get(name) ?? []).filter(
      l => /^\s*[-*]\s+/.test(l) && !isPlaceholder(l)
    )
    if (parsed.has(name) && bullets.length === 0) {
      problems.push(`${file}: "## ${name}" has no real entries`)
    }
  }
}

if (files.length === 0) {
  console.error(`No decision records found in ${dir}`)
  process.exit(1)
}

if (problems.length > 0) {
  console.error(`Decision records incomplete:\n  ${problems.join('\n  ')}`)
  process.exit(1)
}

console.log(`OK: ${files.length} decision record(s) state their assumptions and reopen conditions`)

It is deliberately dumb. It does not judge whether an assumption is good, only that someone had to write one. That is enough to change the habit, because the cheapest way to make the check pass is to think for two minutes about what the decision rests on, and those two minutes are the whole point.

There is a ceiling

Not every decision deserves this. Most of them are cheap to reverse, and for those a line in the pull request description is plenty. The record is worth its cost where being wrong is expensive and plausible: a shape other teams will build on, a commitment outside the building, data you cannot recreate. Recording every choice with six sections is the twenty-page deliberation again, in a different format.

The rule I use is the same one the series uses for reversibility. Pay for the record where the decision will be hard to undo, and skip it everywhere else.

The rule I'd leave you with

The verdict tells you what was decided. The assumptions tell you whether it is still right. A record that has the first without the second is a verdict with a date on it, and it will look complete right up to the moment you need it.

So pick the last decision your team recorded and read it as a stranger would. Could you tell, from the record alone, whether it is still true? And if it stopped being true tomorrow, what in the record would tell anyone to look?