On this page
- The short answer
- Why documentation rots, mechanically
- The Three Shelves
- Decision records that name their own expiry
- The staleness gate
- Runbooks are written for 2am
- Where it lives, and the no-code problem
- The minimum set, and the section everyone leaves out
- What eighteen months does to a document
- The vocabulary, used precisely
The short answer
Write down only what cannot be recovered from the system itself: the decisions, the exceptions, and the things you deliberately chose not to build. Generate everything else, because anything a script can produce will be wrong the moment a human types it by hand. Put the written part in the repository beside the thing it describes, give each document a list of the files it covers and a date it was last verified, and fail the build when a covered file changes and the date does not. Documentation does not survive because people are disciplined. It survives because something breaks when it goes stale.
- 3 shelvesgenerated, derived and decided. Only the third is worth a human's writing time
- covers + verified_onthe two front matter fields that turn staleness from a feeling into a failing build
- Review triggerthe field missing from almost every decision record: the condition that should force a revisit
- "HTTP Request 4"the cheapest documentation debt in any automation, and the easiest to never take on
- The 2am testa runbook is written for someone with no context, one hand on a phone, and no time to read prose
This is the least glamorous part of running automation and the one that decides whether the system outlives its author. The good news is that it is mostly mechanical. Almost every documentation problem is either a placement problem or a class problem, and both have concrete fixes rather than a call for better habits.
Why documentation rots, mechanically
Documentation does not decay through neglect. It decays because of three structural properties, and each one is a property of where and what you wrote rather than of the person who wrote it. This matters most on long-running work such as business process automation, where the system outlives everyone who touched it.
- The cost is paid by the wrong person. Whoever changes the code pays the cost of updating the document, and whoever reads it six months later gets the benefit. Any system that separates cost from benefit degrades, so the fix is to make the cost land in the same commit as the change.
- It describes state rather than decisions. State changes constantly: node names, thresholds, endpoints, field lists. Decisions barely change at all. A document full of state has a half life measured in weeks, and every one of those facts is recoverable from the system anyway.
- It lives where no change forces you to open it. A wiki page is never in the path of a code change. Nothing about editing a workflow makes anyone visit it, so it drifts silently and is discovered wrong at the worst possible moment, usually by someone debugging at speed.
| Artifact | Who reads it and when | Rot rate | Where it belongs | How staleness gets caught |
|---|---|---|---|---|
| Node or step inventory | Anyone tracing a flow, weekly | Very high | Generated at build time from the workflow definition | It cannot go stale, because it is regenerated |
| Environment and config reference | Whoever is deploying, monthly | Very high | Generated from the config schema | The generator fails if a key has no description |
| Decision record | Whoever proposes to change the design, rarely but urgently | Low | Repository, beside the code it covers | Covered file hash changes without verified_on moving |
| Exception policy | Exception queue owner, weekly | Medium | Repository, next to the rules it describes | Review on the same cadence as the queue itself |
| Runbook | On-call at 2am, hopefully never | Medium | Repository, linked directly from the alert payload | Executed in a rehearsal at a stated cadence |
| What it does not handle | Anyone proposing new scope, monthly | Low | Repository, in the readme, near the top | Reviewed at every scope change |
The Three Shelves
Sort every piece of documentation onto one of three shelves before writing a word. The shelf decides who writes it, how often it is refreshed, and whether a human should be touching it at all.
The Three Shelves
Sorted by one question: can this be recovered from the system itself, and at what cost?
Recoverable exactly, for free. Node lists, environment variables, tool signatures, API endpoints, schema fields, the current prompt text. Never write these by hand. Emit them at build time into a file marked do not edit, and make the build fail if a field lacks a description. Hand-written state is not documentation, it is a second copy of a number, and two copies of a number always disagree eventually.
Recoverable, but needing interpretation to be useful. Dependency maps, data flow diagrams, cost breakdowns per stage, the list of which automations write to which system. Regenerate on a schedule rather than on demand, and make the generator fail loudly when a source it depends on has moved instead of silently producing a smaller diagram.
Not recoverable at any price. Why this vendor, what you deliberately excluded, which exception you chose to leave manual, what a rule was protecting against, the failure that caused the retry count to be three. This is the shelf that pays for itself, it barely rots, and it is where every hour of writing effort should go.
Prose that is actually configuration: thresholds, retry counts, schedules, recipient lists, cutoff times. It looks like documentation and behaves like a bug, because the moment the config changes you have two versions of the truth. Move the value into the config file, reference the file, delete the sentence.
This single rule removes most of the maintenance burden from an automation's documentation. The test is quick: could a short script produce this paragraph from the workflow definition, the config file or the code? If yes, write the script once instead of writing the paragraph every quarter. The script is also self-correcting, because when it breaks you find out immediately, whereas a stale paragraph waits patiently to mislead someone.
Decision records that name their own expiry
A decision record captures why, which is the one thing the system cannot tell you. The standard format is nearly right, and it is missing one field: the condition under which the decision should be revisited. Without it, every record is either defended forever or quietly ignored, and neither is a review.
---
# front matter is what makes this document checkable by a machine
id: ADR-014
title: "Invoice PDFs are parsed with a layout model before any LLM call"
status: accepted # proposed | accepted | superseded-by ADR-0xx
date: 2026-06-18
owner: "Priya Nadar"
covers: # paths whose change should force a re-read of this decision
- src/ingest/parse_invoice.py
- config/model_aliases.yaml
covers_hash: "sha256:9f2c...c41a" # written by the staleness gate, never by hand
verified_on: 2026-08-12
max_age_days: 180
---
## Context
Suppliers send four input shapes: native PDFs, scans, photographs of printed
invoices, and emails with the figures in the body and no attachment at all.
Feeding raw text of a scanned two-column invoice to a model produced correct
figures attached to the wrong supplier, because reading order collapsed the
columns. The failure was silent: every field was plausible.
## Options considered
1. Model only, whole page as text. Cheapest, and the column failure is invisible.
2. Layout model first, then a model call per detected region. More moving parts.
3. Template rules per supplier. Accurate for the top twenty, unmaintainable at 400.
## Decision
Option 2. Layout extraction runs first and hands the model bounded regions with
coordinates. Region confidence below the threshold routes to the human queue
instead of being guessed.
## Consequences
Cost per document rises because of the extra stage. Latency rises by roughly a
second. Two-column and stamped invoices stop producing confident wrong answers,
which was the failure that mattered.
## Reversal cost
About two days. The model-only path still exists behind a flag. Nothing is
migrated or destroyed, so reverting loses no data.
## Review trigger
Revisit if any of these becomes true, and do not wait for the review date:
- a model we can use accepts page images directly with reliable region grounding
- scanned share of input drops below 10 percent for two consecutive quarters
- per-document cost of the layout stage exceeds the cost of the model call
- Options considered must include the one you rejected, and why. The next person will propose exactly that option, and without the reason they will implement it and lose a week to the same problem.
- Reversal cost is stated in time and data. Two days with nothing lost is a different decision from two weeks and a migration you cannot undo.
- The review trigger is a condition, not a date. Dates get rescheduled. A condition such as scanned input dropping below a tenth of volume is something a dashboard can watch, so the document pages you instead of waiting to be read.
- Record the failure, not just the choice. The sentence about columns collapsing and figures landing under the wrong supplier is worth more than the decision paragraph, because nobody could reconstruct it.
- Supersede, do not edit. Write a new record and mark the old one superseded. Editing history destroys the only account of why the previous version made sense.
The staleness gate
This is the mechanism that makes the rest of it work. A document declares which files it covers. Continuous integration hashes those files. When the hash moves and the document's verified date does not, the build fails and names the document. It takes an afternoon to build and it converts documentation from a virtue into a constraint.
- Add front matter to the documents that matter
Not every document opts in. Start with decision records, exception policies and runbooks for tier critical automations. Each gets a covers list, a verified_on date, an owner and a max_age_days.
- Fail on dead references first
Resolve every glob. A pattern matching nothing means the document points at a renamed or deleted file, the most common rot and the easiest to detect. Ship this check alone and you will find stale documents on day one.
- Hash the covered files and store the digest
Concatenate covered files in sorted order, hash them, keep the digest in front matter. The gate writes this value and a human never does, so there is exactly one way to acknowledge a change.
- Fail when the code moved and the document did not
A differing digest fails the build and prints the new one. The author re-reads, edits if needed, bumps verified_on and pastes the digest. Acknowledging that nothing changed is one line, which keeps compliance cheap.
- Add a shelf life sweep
Anything older than its own max_age_days gets a ticket for its owner. This catches documents whose covered files never change while the reality around them does.
#!/usr/bin/env python3
"""docs_gate.py -- run in CI. Fails when a document's covered code moved and
the document did not. Catches the two rots that matter: dead references and
silent drift."""
import glob, hashlib, pathlib, re, sys, datetime, yaml
FAILS = []
TODAY = datetime.date.today()
for doc in glob.glob("docs/**/*.md", recursive=True):
text = pathlib.Path(doc).read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
if not m:
continue # not every doc opts in, and that is fine
fm = yaml.safe_load(m.group(1)) or {}
covers = fm.get("covers") or []
if not covers:
FAILS.append(f"{doc}: front matter present but covers is empty")
continue
# rot 1: the document points at something that no longer exists
paths = []
for pattern in covers:
hits = sorted(glob.glob(pattern, recursive=True))
if not hits:
FAILS.append(f"{doc}: covers '{pattern}' matches nothing")
paths += hits
# rot 2: the covered code changed and nobody re-read the document
h = hashlib.sha256()
for p in paths:
h.update(pathlib.Path(p).read_bytes())
digest = "sha256:" + h.hexdigest()
if fm.get("covers_hash") != digest:
FAILS.append(f"{doc}: covered files changed. Re-read it, then bump "
f"verified_on and set covers_hash to {digest}")
# rot 3: nobody has looked at it inside its own stated shelf life
verified = fm.get("verified_on")
max_age = int(fm.get("max_age_days", 365))
if verified and (TODAY - verified).days > max_age:
FAILS.append(f"{doc}: verified_on is {(TODAY - verified).days} days old, "
f"max_age_days is {max_age}")
for f in FAILS:
print("STALE", f)
sys.exit(1 if FAILS else 0)
A gate applied to every document in the repository turns every small change into a documentation chore, and within a month somebody adds a skip flag that becomes permanent. Apply it only to documents that opted in with front matter, and only to the automations you have tiered as critical. A gate covering six documents that nobody bypasses is worth more than a gate covering sixty that everyone does. The same logic applies to prompts, which are code and deserve the same treatment, as covered in prompts as code.
Runbooks are written for 2am
A runbook is a different genre from documentation and should not be written by the same instinct. Its reader has been woken up, has no context loaded, is possibly reading on a phone, and needs to act rather than understand. Everything explanatory belongs somewhere else, linked, and the link should not be needed.
- Open with how to tell whether this is the right runbook. One symptom list. The reader arrived from an alert and needs three seconds to confirm they are in the right document.
- One action per line, in the imperative. No paragraphs. No background. If a step needs justification, the justification is a decision record and it can wait until morning.
- Exact commands, exact URLs, exact button labels. Not "go to the admin panel". A tired reader cannot search a menu tree, and a phone screen makes it worse.
- State the expected output of each step. Without it the reader cannot tell success from a silent failure, and will happily continue past the step that did not work.
- Give every step a branch for failure. The sentence "if that returns nothing, go to step 7" is the difference between a runbook and a wish.
- Close with how to confirm it is fixed, and what to write down. The incident note is what turns tonight's outage into tomorrow's decision record.
Then test it the only way that works. Hand the runbook to someone who has never touched the system, ask them to execute it against a staging copy, and time them without helping. Every place they hesitate is a defect. A runbook that has only ever been executed by its author is a draft, no matter how carefully it was written, because the author silently supplies the missing steps from memory. Run that rehearsal on a cadence, because a runbook written against last year's console is a runbook that will fail exactly when it is needed.
Where it lives, and the no-code problem
Put the written documentation in the repository next to the code. This is not a preference about tools. It is the only arrangement where changing the system puts the document in front of the person changing it.
If the automation lives in a visual tool with no repository, you have a harder version of the same problem, and there is a fix. Export the workflow definition on a schedule and commit it. The diff is the change history you otherwise do not have, and it is the only way to answer the question you will eventually be asked, which is what changed on the fourteenth. Strip credentials before committing, commit only when the export differs, and alert if the export job itself stops running, because a silent exporter looks exactly like a workflow nobody has touched. The same discipline is why mapping a process before you automate it pays off later: the map becomes the document the tool cannot produce.
The minimum set, and the section everyone leaves out
Six documents per automation. Any more and none of them are maintained. The last one on this list is the one that is always missing and the one experienced people read first, because knowing the edges of a system is faster than inferring them.
The what it does not do section deserves its own paragraph because it is the highest value writing in the set and it takes twenty minutes. List the input classes that route straight to a human, the failures that produce no alert, the step somebody still does by hand every Friday, and the thing that looks automated but is a scheduled reminder to a person. Each item is something a newcomer would otherwise discover by assuming coverage that does not exist, which is the most expensive assumption in automation work. It also stops scope creep in meetings, because it turns a vague worry into a specific gap somebody can decide about.
What eighteen months does to a document
Rot is not gradual and even. It arrives in identifiable events, and each one has a countermeasure. This is the shape of it for a document with no gate.
The document matches reality perfectly and nobody needs it, because the author is sitting there answering questions faster than anyone can read.
A threshold changes in config during an incident. The document still names the old value, and nothing announces the gap.
A step is renamed and two paragraphs now describe something that no longer exists. A dead-reference check would have caught this in one build.
Handover happens against the document, so whatever it says becomes the official account, including the threshold from month two.
Someone follows it, hits the divergence, and tells the team it is out of date. Nobody reads it after that, so nobody corrects it either.
An outage forces a reconstruction of how the system actually works, at a cost of several engineer days, producing a document that will follow the same path unless staleness now fails loudly.
The vocabulary, used precisely
- Decision record
- A short document capturing why a design choice was made, the options rejected and the cost of reversing it. It records the one thing that cannot be recovered from the system itself, which is intent.
- Review trigger
- A condition, not a date, under which a decision should be revisited. Written as something observable such as a volume threshold or a vendor capability appearing, it lets a dashboard raise the review instead of relying on someone remembering.
- Staleness gate
- A continuous integration check that fails the build when the files a document declares it covers have changed and the document's verified date has not. It moves the cost of stale documentation from the future reader to the present author.
- Covered path
- A file or glob listed in a document's front matter, declaring that the document describes that code. Dead covered paths are the fastest detectable form of documentation rot.
- Runbook
- A procedure written for a person acting under pressure with no context, consisting of imperative steps, exact commands, expected output per step and a failure branch for each. It is not documentation and should not read like it.
- Tribal knowledge
- Operational fact that exists only in someone's head, usually an exception rule or a workaround. It is invisible until that person is unavailable, and converting it into decision records is most of what a good handover is.
The honest summary is that documentation quality is an engineering problem with an engineering fix, not a character problem with a cultural fix. Nobody in your team is lazy. They are responding rationally to a system where writing the document is costly, skipping it is free, and the consequence lands on a stranger. Change those incentives with a gate, a repository location and a rule about what is worth writing at all, and the documents stay true without anybody becoming a better person. Pair it with a real owner per surface, as in who owns the automation after launch, and the system survives its authors.
ChatGPTalker, "Documentation That Survives the Person Who Wrote It" (2026). Sort documentation onto three shelves, generated, derived and decided, and hand-write only the third. Give each document a covers list and a verified date, and fail the build when covered code changes without the document being re-read.
Questions readers ask next
How much documentation does one automation actually need?
Should we use a wiki or keep documentation in the repository?
What if the automation is built in a no-code tool with no repository?
Will a staleness gate slow every change down?
How do we capture knowledge from someone who is already leaving?
Are AI generated summaries of a codebase a substitute for documentation?
ChatGPTalker. "Documentation That Survives the Person Who Wrote It." chatgptalker.com, 2026-08-26. https://chatgptalker.com/guides/documentation-that-survives/