Guide
Team Documentation in Markdown: Meeting Notes, SOPs, PRDs and Release Notes
Published July 30, 2026
Internal documents have a different failure mode from client-facing ones. Nobody judges your competence by their typography; they fail by being out of date, unfindable, or never written because the tool made it tedious. Markdown addresses all three: it is fast enough to write during a meeting, it diffs so you can see what changed, and it lives next to the code it describes.
This guide covers five internal document types with templates, plus the question of where they should actually live.
Meeting notes
Most meeting notes are useless because they transcribe discussion instead of recording outcomes. A useful record answers three questions: what was decided, who owes what by when, and what remains open.
# Platform sync — 21 May 2026
**Attendees:** Jordan Rivera, Katrin Mueller, Sam Okoye, Priya Nair
**Absent:** Tom Bergström (on leave)
**Chair:** Katrin Mueller
**Duration:** 45 minutes
## Decisions
1. **Postgres 17 upgrade moves to the June window.** The May window
conflicts with the marketing campaign launch. *Decided by: Katrin.*
2. **We will not adopt the new queue library this quarter.** Maintenance
burden outweighs the latency gain at current volume. Revisit in Q4.
*Decided by: Jordan, with Sam dissenting — see below.*
3. **On-call rotation extends to five people from 1 June.** Priya joins after
shadowing two rotations.
## Action items
| # | Action | Owner | Due |
| --- | --- | --- | --- |
| 1 | Draft the Postgres 17 upgrade runbook | Sam | 28 May |
| 2 | Book the June maintenance window with ops | Katrin | 23 May |
| 3 | Schedule Priya's shadow rotations | Jordan | 26 May |
| 4 | Benchmark queue library at 5× volume for Q4 review | Sam | 30 Sep |
## Open questions
- Do we need a second read replica before the upgrade, or after? **Sam to
investigate, report at next sync.**
- Who owns the runbook after Sam's team reorg? **Unresolved.**
## Discussion notes
Sam's dissent on the queue library: the latency improvement is real (p99
from 40ms to 12ms in his benchmark) but the library has one maintainer and
no releases in eight months. Agreed the risk is the deciding factor at
current volume, and that this changes if volume triples.
## Next meeting
28 May, same time. Standing agenda plus Postgres runbook review.
The structure puts decisions first, then actions, then open questions, and discussion last. That ordering reflects how notes are actually read — someone checking what they owe should not have to scroll past a transcript.
Two details worth adopting. Record who decided, not just what — six months later, “why did we do this” is answerable. And record dissent, as in Sam’s case; a decision that everyone appears to have agreed with is hard to revisit when circumstances change.
Action items that persist
The weakness of notes-in-documents is that action items get buried. Two ways to fix it:
Aggregate them. Keep every meeting’s notes in one directory and grep across them:
# Everything Sam owes, across all meetings
rg '\| Sam \|' meetings/
# Anything due in May that has not been struck through
rg '\| .* \| \d+ May \|' meetings/ | rg -v '~~'
Or mark completion in place, using task list syntax so state is visible in the document:
- [x] Draft the Postgres 17 upgrade runbook — Sam, 28 May
- [ ] Book the June maintenance window — Katrin, 23 May
- [ ] Schedule Priya's shadow rotations — Jordan, 26 May
GFM task lists render as real checkboxes, and in a PDF they render as checked or unchecked boxes, which reads correctly on paper.
Decision records
For decisions with consequences beyond one meeting, a separate lightweight record is worth the overhead. The Architecture Decision Record format is the common convention:
# ADR-014: Defer adoption of the async queue library
**Status:** Accepted
**Date:** 21 May 2026
**Deciders:** Jordan Rivera, Katrin Mueller
**Supersedes:** —
## Context
Our current queue implementation adds ~40ms p99 latency to order
acknowledgement. A candidate replacement benchmarks at 12ms. The library has
a single maintainer and no releases since September 2025.
## Decision
We will not adopt it this quarter. We will re-evaluate in Q4 2026, or sooner
if daily order volume exceeds 120,000.
## Consequences
**Positive:** No new single-maintainer dependency in the critical path. No
migration work this quarter.
**Negative:** We carry 28ms of avoidable latency. If volume grows faster than
forecast we will be making this decision under time pressure.
**Follow-up:** Sam to re-benchmark at 5× volume by 30 September (action 4,
21 May sync).
The value is entirely in the Context and Consequences sections. Anyone can see what was decided from the code; almost nobody can reconstruct why, or what the team knew at the time. Numbering them sequentially and never deleting one — superseding instead — keeps the history intact.
Standard operating procedures
An SOP is a procedure someone unfamiliar with the task can follow correctly under pressure. The design constraints are unusual: it may be read at 3am by someone who did not write it, possibly printed, possibly while the system it describes is on fire.
# SOP-07: Production database failover
**Version:** 2.3
**Owner:** Platform Engineering
**Last reviewed:** 21 May 2026
**Next review due:** 21 November 2026
**Estimated duration:** 20–35 minutes
## When to use this procedure
Execute this when the primary database is unreachable for more than
5 minutes and the cause is not a network partition affecting only your
workstation. **Verify from a second network path before starting.**
Do **not** use this procedure for planned maintenance — see SOP-12.
## Prerequisites
- [ ] Production `kubectl` context and `psql` access
- [ ] Incident channel open, incident commander assigned
- [ ] Read access to the replication lag dashboard
- [ ] SOP-09 (rollback) open in a second window
## Safety notes
> **Data loss risk.** Failover promotes a replica. Any transaction committed
> to the primary but not yet replicated is lost. Check replication lag in
> step 2 before proceeding — above 30 seconds, escalate rather than
> proceeding.
## Procedure
### 1. Confirm the primary is genuinely unreachable
```bash
kubectl exec -n data statefulset/pg-primary -- pg_isready
Expected on failure: no response. If it responds, stop — this is not a
failover situation.
2. Check replication lag on the standby
kubectl exec -n data statefulset/pg-standby-1 -- \
psql -qtc "SELECT now() - pg_last_xact_replay_timestamp();"
Decision point. Under 30 seconds: continue to step 3. Over 30 seconds: stop and escalate to the database on-call. Do not proceed.
3. Fence the primary
kubectl scale -n data statefulset/pg-primary --replicas=0
Wait for the pod to terminate fully before continuing. Verify:
kubectl get pods -n data -l app=pg-primary
Expected: no pods listed.
4. Promote the standby
kubectl exec -n data statefulset/pg-standby-1 -- \
pg_ctl promote -D /var/lib/postgresql/data
Expected output: server promoting.
5. Repoint the service
kubectl patch svc pg-primary -n data \
-p '{"spec":{"selector":{"app":"pg-standby-1"}}}'
6. Verify
-
pg_isreadyagainst the service returnsaccepting connections - Application error rate returns to baseline within 3 minutes
- A test write succeeds
- Incident channel updated with completion time
Rollback
If the promoted standby is not accepting writes after step 4, do not attempt to un-promote — it is not reliably reversible. Go to SOP-09 (restore from snapshot) and escalate.
After the incident
- Reconfigure a new standby (SOP-08)
- File an incident report within 48 hours
- Review whether this SOP needed changes; update the version above
### What makes that work
**Explicit decision points.** Step 2 says what to do in both branches. An SOP that only describes the happy path fails at exactly the moment it is needed.
**Expected output for every command.** The reader can tell whether it worked without knowing the system. This is the difference between a procedure and a list of commands.
**A stated rollback, including where rollback is impossible.** Saying "this is not reliably reversible" is more useful than pretending there is a clean undo.
**Preconditions as checkboxes.** Printable, and forces explicit verification.
**A review date.** An SOP that describes infrastructure from two years ago is worse than no SOP, because it is trusted. Reviewing on a schedule and recording the date is what keeps that from happening.
**No prose where a command will do.** Under pressure people skim. Numbered steps, commands in blocks, decisions in bold.
### SOP print CSS
SOPs get printed and taped to walls. Design for it:
```css
@media print {
.sop h3 {
/* Never split a step from its command. */
break-after: avoid;
page-break-after: avoid;
}
.sop h3 + pre,
.sop h3 + p + pre {
break-before: avoid;
page-break-before: avoid;
}
.sop pre {
break-inside: avoid;
page-break-inside: avoid;
background: #f4f4f6;
border-left: 2.5pt solid #333;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.sop blockquote {
border: 1pt solid #b91c1c;
border-left-width: 3pt;
background: #fef2f2;
padding: 8pt 10pt;
break-inside: avoid;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* Real checkboxes for printed use. */
.sop input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 9pt;
height: 9pt;
border: 0.75pt solid #333;
margin-right: 4pt;
vertical-align: -0.5pt;
}
@page {
@top-right {
content: "SOP-07 v2.3";
font-size: 8pt;
color: #888;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 8pt;
color: #888;
}
}
}
The checkbox styling matters: GFM task lists render as <input type="checkbox" disabled>, and browsers draw those as small grey boxes that print faintly or not at all. Restyling them as bordered squares makes them usable with a pen.
The version number in the page header means a printed copy on a wall can be checked against the current version.
Checklists
Distinct from SOPs: a checklist assumes competence and guards against omission. Pilots use them not because they cannot fly but because humans skip steps.
# Pre-release checklist — v4.2.0
**Release manager:** Priya Nair
**Target date:** 28 May 2026
## Code freeze
- [ ] Release branch cut from `main` at agreed commit
- [ ] No commits to release branch except approved fixes
- [ ] Version bumped in `package.json` and `CHANGELOG.md`
## Verification
- [ ] Full test suite green on the release branch
- [ ] Migration tested against a production-sized snapshot
- [ ] Rollback tested from the release build back to v4.1.3
- [ ] Load test at 2× peak passes
- [ ] Security scan shows no new high or critical findings
## Communication
- [ ] Release notes drafted and reviewed
- [ ] Support team briefed on changes
- [ ] Status page maintenance window scheduled
- [ ] Customer-facing changes flagged to the account team
## Deploy
- [ ] Deployed to staging, smoke tests pass
- [ ] Canary at 5% for 30 minutes, error rate flat
- [ ] Full rollout
- [ ] Post-deploy verification complete
## After
- [ ] Release tagged in Git
- [ ] `CHANGELOG.md` merged back to `main`
- [ ] Retro scheduled if anything went wrong
Two rules for checklist design. Every item must be objectively verifiable — “code reviewed” is checkable, “code is good quality” is not. And keep it short enough to actually be used; past about thirty items people start ticking without reading, at which point the checklist is worse than nothing because it provides false assurance.
Product requirements documents
A PRD’s job is to make disagreement visible before code is written. The template below front-loads the parts that surface disagreement.
# PRD: Saved export presets
**Status:** In review
**Author:** Priya Nair
**Reviewers:** Jordan Rivera (eng), Katrin Mueller (design)
**Target release:** v4.4
**Last updated:** 21 May 2026
## Problem
Users converting documents repeatedly reconfigure the same settings — page
size, theme, margins — on every visit. Support sees roughly 15 requests a
month asking whether settings can be remembered. Session analytics show 34%
of returning users apply an identical configuration each time.
## Goals
1. A returning user can apply a previously-used configuration in one action.
2. Configurations persist across sessions on the same device.
3. No account is required.
## Non-goals
- Syncing presets across devices. Requires accounts; out of scope.
- Sharing presets between users.
- Server-side storage of any kind.
## Users
Primary: returning users with a consistent document type — someone exporting
weekly reports at A4 with the same theme.
Secondary: first-time users, who should be unaffected. The feature must not
add UI complexity to the initial experience.
## Proposed solution
A "Presets" control in the export panel. Saving captures the current
configuration under a user-supplied name. Selecting applies it. Presets are
stored in `localStorage`, capped at ten.
### User flow
1. User configures export settings as today.
2. Clicks *Save preset*, enters a name, confirms.
3. On a later visit, opens the presets dropdown and selects it.
4. All captured settings apply at once.
### Out of scope for v1
Reordering, editing in place, and export/import of presets. Add later if
requested.
## Success metrics
- 20% of returning users have at least one saved preset within 60 days.
- Support requests on this topic drop below 3 per month.
- No measurable increase in time-to-first-export for new users.
## Open questions
- What happens when a preset references a theme that has been removed?
**Proposal:** fall back to default, warn once. *Needs eng input.*
- Should the most recent configuration be offered implicitly as
"Last used" without an explicit save? *Needs design input.*
## Risks
| Risk | Likelihood | Impact | Mitigation |
| --- | --- | --- | --- |
| `localStorage` cleared, presets lost | Medium | Low | Document the limitation in the UI |
| Preset UI clutters the export panel | Medium | Medium | Design review before build |
| Scope creep toward accounts and sync | High | High | Non-goals are explicit above |
Non-goals is the most valuable section. It is where you prevent the feature from growing into an account system. Open questions with named owners turn vague unease into assigned work. And a risk table with mitigations is the difference between acknowledging risk and managing it.
Release notes from Git history
Release notes are the internal document most amenable to automation, because the source material is already structured — if your commits are.
With Conventional Commits, generation is nearly free:
git log v4.1.3..v4.2.0 --pretty=format:"%s (%h)" \
| grep -E "^(feat|fix|perf)" \
| sort
A script that groups by type:
#!/usr/bin/env bash
# release-notes.sh v4.1.3 v4.2.0
set -euo pipefail
FROM="$1"; TO="$2"
DATE=$(git log -1 --format=%cd --date=short "$TO")
printf '# %s\n\n_Released %s_\n\n' "$TO" "$DATE"
emit() {
local prefix="$1" heading="$2"
local body
body=$(git log "$FROM..$TO" --pretty=format:"%s|%h" \
| grep -E "^$prefix(\(.+\))?!?:" || true)
[ -z "$body" ] && return 0
printf '## %s\n\n' "$heading"
while IFS='|' read -r subject hash; do
# Strip the conventional-commit prefix for readability.
printf -- '- %s (`%s`)\n' \
"$(printf '%s' "$subject" | sed -E "s/^$prefix(\(.+\))?!?: *//")" \
"$hash"
done <<< "$body"
printf '\n'
}
emit "feat" "New features"
emit "fix" "Fixes"
emit "perf" "Performance"
# Breaking changes are flagged with ! or a BREAKING CHANGE footer.
BREAKING=$(git log "$FROM..$TO" --pretty=format:"%s|%h" | grep -E "^[a-z]+(\(.+\))?!:" || true)
if [ -n "$BREAKING" ]; then
printf '## Breaking changes\n\n'
while IFS='|' read -r subject hash; do
printf -- '- %s (`%s`)\n' "$subject" "$hash"
done <<< "$BREAKING"
fi
Then convert the Markdown to PDF for distribution, or publish it as-is.
The obvious caveat: generated notes are a draft, not a deliverable. Commit messages are written for other developers. “fix: null check in resolveTheme” means nothing to a user; “Fixed a crash when applying a preset whose theme had been deleted” does. Generate the skeleton, then rewrite for the audience.
Two audiences means two documents. Internal notes can be commit-derived and terse. Customer-facing notes need rewriting, grouping by user-visible capability rather than by commit type, and omitting everything that has no user-visible effect.
Where these documents should live
The format question is easier than the location question. Some guidance:
In the repository, next to the code: ADRs, SOPs for systems that repo describes, PRDs for features in it, release notes. The argument is version coupling — the SOP for deploying a service should change in the same commit that changes the deployment. A docs/ directory with adr/, sop/ and prd/ subdirectories covers most needs.
In a wiki or knowledge base: anything cross-team, anything non-engineers need to edit, anything with no natural repository home. Markdown-in-Git only works if the readers can use Git.
Meeting notes: genuinely contested. In-repo gives you grep and history; a shared wiki gives you access for people without repository access. Split by content: platform sync notes in the platform repo, cross-functional notes in the wiki.
PDF exports: for the subset that needs to leave the system — a printed SOP by the server rack, release notes emailed to customers, a PRD circulated for sign-off. The Markdown remains the source of truth; the PDF is a snapshot with a date on it.
That last point is the discipline that makes this work. Once a PDF starts being treated as the current version, you have two sources of truth and one of them is wrong. Put a generation date and a link back to the source in every export:
@page {
@bottom-left {
content: "Generated from docs/sop/07-db-failover.md";
font-size: 7.5pt;
color: #999;
}
}
Try it yourself
The editor here renders GFM task lists as real checkboxes and supports the print CSS above, so SOPs and checklists export in a form that works on paper. Nothing is uploaded, which matters for internal runbooks containing infrastructure detail.
Related: technical documentation for READMEs and API docs, business documents for client-facing work, and automation for generating release notes in CI.
About this guide
Written and maintained by the developer of MarkdownToFile — the browser-based converter this site runs on. Techniques described here are tested against the engines named in the text (Chromium's print pipeline, Paged.js, WeasyPrint), and engine limitations are stated rather than glossed over. Corrections are welcome via the contact page; more about the project on the about page.
Try it yourself — free, no signup
Convert your Markdown to a polished PDF right in your browser.
Open the editor