Architecture Decision Records skill for Codex
Record why you chose it, not just what. Codex drafts ADRs that a newcomer can read in five minutes: a decision-stating title, neutral context with numbers, at least two genuinely considered options, consequences that include the costs, and a revisit trigger. A bundled script numbers new records, marks superseded ones and regenerates the index, and Codex leaves status at proposed until the people who own the decision accept it.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- architecture-decision-records Installs to ~/.agents/skills/architecture-decision-records/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install architecture-decision-records The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project architecture-decision-records Installs to .agents/skills/architecture-decision-records/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/architecture-decision-records.zipunzip -o architecture-decision-records.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) architecture-decision-records PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $architecture-decision-records, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and maintains Architecture Decision Records (ADRs) in Markdown using the Nygard or MADR 4 format - one numbered file per decision with context, options considered, the decision, consequences and status - and keeps the index, supersession links and lifecycle correct. Use when a team picks a database, framework, protocol or pattern, when someone asks "why did we choose X", when reversing an earlier decision, or when setting up a docs/adr folder in a repo.
Use it when
- The team is choosing a database, broker, framework or architectural pattern
- Someone keeps asking why an earlier technical choice was made
- An accepted decision is being reversed or replaced
- A repo needs a decision log that lives next to the code
Use something else when
- You need a long design proposal with diagrams and rollout phases: use design-docs-rfcs
- The decision is a business or product call rather than a technical one: use decision-memos
How to check the result
Regenerate the index and confirm every ADR appears once with contiguous numbers and matching status, and that each 'superseded by' link points to an existing ADR that links back.
The mistake it prevents
Editing an accepted ADR to match what happened later instead of writing a new record that supersedes it, which destroys the history the log exists to keep.
A good ADR lets a new engineer, two years from now, understand why the team chose what it chose, what it gave up, and when the choice should be revisited, in under five minutes. It records one decision, is written before or at the moment of deciding, and is never rewritten afterwards; changes arrive as a new ADR that supersedes it.
When an ADR is warranted
Write one when the decision is architecturally significant: hard to reverse, affects more than one team or module, or constrains future choices.
| Situation | ADR? |
|---|---|
| Choosing a datastore, message broker, auth provider, cloud service | Yes |
| Adopting a pattern repo-wide (outbox, CQRS, monorepo, feature flags) | Yes |
| Public API style, versioning or wire format | Yes |
| Rejecting a popular option the team will keep being asked about | Yes, status rejected |
| Library swap behind an existing interface, local refactor | No, a PR description is enough |
| Coding style, lint rules | No, put it in AGENTS.md or the linter config |
| Multi-week design with diagrams and rollout plan | Write a design doc ($design-docs-rfcs) and an ADR that links to it |
Before writing
- Read
AGENTS.md, then look for an existing log:ls docs/adr docs/decisions doc/adr adr 2>/dev/nullandrg -l "^## Status|^status:" docs. Follow the format already in use; never start a second numbering scheme. - Read the ADRs that touch the same area. A new decision often supersedes or amends one.
- Collect the facts the context needs: current constraints, load figures, team skills, deadlines, costs. An ADR without numbers is an opinion.
- Identify who decides and who must be consulted. Codex drafts; humans accept.
Choose a format
| Format | Use when |
|---|---|
| Nygard (Title, Status, Context, Decision, Consequences) | Small team, fast decisions, existing log uses it |
| MADR 4 (front matter + considered options with pros/cons + confirmation) | Several real options, need an audit trail of trade-offs |
| Y-statement one-liner inside an issue | Lightweight record of a minor but recurring question |
Default to MADR-style for new logs: the considered options section is what readers miss most.
Template (MADR 4 style)
---
status: proposed # proposed | accepted | rejected | deprecated | superseded by ADR-0012
date: 2026-03-14
decision-makers: [platform team lead, payments tech lead]
consulted: [SRE, security]
informed: [all backend engineers]
---
# 0011. Use PostgreSQL logical replication for read models instead of Kafka Connect
## Context and problem statement
Read models for the reporting UI lag the orders database by 4-9 minutes using the nightly
plus hourly batch. Product needs < 30 s staleness by Q3. Write volume is ~400 rows/s peak.
We run no Kafka today; the team has strong Postgres operational experience.
## Decision drivers
- Staleness under 30 s at p99
- No new stateful infrastructure for a three-person team
- Must survive a primary failover without a full resync
## Considered options
1. Postgres logical replication into a reporting replica
2. Debezium + Kafka Connect into the reporting store
3. Application-level dual writes
## Decision outcome
Chosen: **option 1**, because it meets the staleness target with infrastructure we already
operate, and failover is handled by the managed service's logical slot failover.
### Consequences
- Good: no new cluster to run; lag observable through `pg_stat_replication`.
- Bad: reporting schema is coupled to the orders schema; column renames need coordination.
- Bad: if we later need fan-out to more than two consumers, we revisit (see option 2).
### Confirmation
Replication lag alert at 20 s; load test at 2x peak shows p99 lag < 10 s before acceptance.
## Pros and cons of the options
### Debezium + Kafka Connect
- Good: fan-out to many consumers, replayable.
- Bad: Kafka, Connect and schema registry to operate; estimated 0.5 FTE.
### Dual writes
- Bad: no atomicity across the two stores; lost updates on partial failure.
## More information
Design notes: docs/design/reporting-freshness.md. Revisit if consumers > 2 or write rate > 5k rows/s.
For the short Nygard form, keep the five headings and put options in Context.
Workflow
- [ ] 1. Find the existing ADR folder and numbering (or agree on docs/adr/ with the user)
- [ ] 2. Read related ADRs; decide if this supersedes one
- [ ] 3. Draft with status "proposed", next free number, today's date
- [ ] 4. Fill context with facts and numbers; list at least two real options
- [ ] 5. State the decision in one sentence, then consequences (good AND bad)
- [ ] 6. Add a confirmation step: how we will know the decision held
- [ ] 7. Update the index; add supersession links both ways if applicable
- [ ] 8. Open as a PR; status changes to "accepted" only when the decision-makers approve
To create a numbered file and update the index deterministically:
python3 scripts/new_adr.py docs/adr "Use PostgreSQL logical replication for read models"
python3 scripts/new_adr.py docs/adr "Move reporting to ClickHouse" --supersedes 11
python3 scripts/new_adr.py docs/adr --index-only # rebuild README.md index
The script picks the next number, writes a MADR-style skeleton with status: proposed,
marks a superseded ADR’s status line, and regenerates README.md in the folder. It never
deletes files. If the repo uses adr-tools (.adr-dir file present), use adr new "Title"
and adr new -s 11 "Title" instead so the existing tooling stays consistent.
Lifecycle rules
| From | To | How |
|---|---|---|
| proposed | accepted | PR approved by the named decision-makers; set the date of acceptance |
| proposed | rejected | Keep the file; record why. Rejected ADRs stop the same debate recurring |
| accepted | superseded | New ADR states Supersedes ADR-0011; old one gets superseded by ADR-0015 |
| accepted | deprecated | The context no longer exists (feature removed); no replacement |
- After acceptance, only fix typos and add links. Never edit the decision or consequences of an accepted ADR to match what later happened; write a new one.
- Numbers are permanent and never reused, including for rejected ADRs.
- Filenames:
NNNN-kebab-title.md, four digits, matching the heading number.
Writing rules
- Title states the decision, not the topic: “Use PostgreSQL logical replication for read models”, not “Read model sync”.
- Context is neutral: describe forces and constraints so a reader who disagrees with the outcome still recognises the situation as fair.
- List options you actually considered, including “do nothing” when it was viable. Strawman options make the record worthless.
- Consequences must include costs. An ADR with only positive consequences was not thought through.
- Record the revisit trigger: the measurable condition under which this should be reopened.
- Keep it to one or two screens. Link to design docs, benchmarks and spikes rather than pasting them.
Gotchas
- Writing the ADR months after the fact produces rationalisation, not history. If backfilling, say so in Context (“Recorded retroactively in 2026-03; original reasoning reconstructed from #412 and Slack thread”).
- Two ADRs that contradict each other with both
acceptedis the most common log rot. When superseding, update both files in the same PR. - ADRs in a wiki drift from the code. Keep them in the repo next to what they govern so they are reviewed in PRs and searchable by Codex.
- Do not mark an ADR accepted on the user’s behalf. Draft it, and let the PR approval or the user’s explicit instruction change the status.
- Monorepos: one log per independently deployed system, or a root log with a
scope:field. Mention the location inAGENTS.mdso future sessions find it.
Verification
python3 scripts/new_adr.py docs/adr --index-onlythengit diff docs/adr/README.md: every file appears once, numbers are contiguous, statuses match each file.rg -n "superseded by" docs/adrand confirm each target ADR exists and links back.- Read the draft as a newcomer: can you state the decision, the main trade-off and the revisit trigger after one read? If not, tighten Context and Consequences.
Output contract
When reporting back:
**ADR:** docs/adr/0011-use-postgresql-logical-replication-for-read-models.md (status: proposed)
**Supersedes:** none | ADR-0007 (status updated)
**Decision:** <one sentence>
**Key trade-off:** <what we give up>
**Open questions for decision-makers:** <list, or none>
**Index:** docs/adr/README.md regenerated What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Architecture Decision Records"
short_description: "Record why you chose it, not just what"
default_prompt: "$architecture-decision-records Write an ADR for switching our job queue from Redis to Postgres with SKIP LOCKED" scripts/new_adr.py
#!/usr/bin/env python3
"""Create the next numbered ADR, mark a superseded ADR, and rebuild the index.
Usage:
python3 new_adr.py <adr-dir> "Decision title" [--supersedes N]
python3 new_adr.py <adr-dir> --index-only
Standard library only. Never deletes files. Exit code 1 on any error.
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import sys
from pathlib import Path
FILE_RE = re.compile(r"^(\d{4})-[a-z0-9-]+\.md$")
def slugify(title: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
return slug[:70].rstrip("-") or "decision"
def adr_files(folder: Path) -> list[tuple[int, Path]]:
found = []
for p in folder.iterdir():
m = FILE_RE.match(p.name)
if m:
found.append((int(m.group(1)), p))
return sorted(found)
def read_status(text: str) -> str:
fm = re.match(r"^---\n(.*?)\n---\n", text, re.S)
if fm:
m = re.search(r"^status:\s*(.+?)\s*(#.*)?$", fm.group(1), re.M)
if m:
return m.group(1).strip().strip("\"'")
m = re.search(r"^##\s+Status\s*\n+\s*(.+)$", text, re.M)
return m.group(1).strip() if m else "unknown"
def read_title(text: str, fallback: str) -> str:
m = re.search(r"^#\s+(.+)$", text, re.M)
return re.sub(r"^\d+\.\s*", "", m.group(1).strip()) if m else fallback
def read_date(text: str) -> str:
m = re.search(r"^date:\s*(\S+)", text, re.M)
return m.group(1) if m else ""
def set_status(path: Path, new_status: str) -> None:
text = path.read_text(encoding="utf-8")
fm = re.match(r"^---\n(.*?)\n---\n", text, re.S)
if fm and re.search(r"^status:", fm.group(1), re.M):
body = re.sub(r"^status:.*$", f"status: {new_status}", fm.group(1), count=1, flags=re.M)
text = f"---\n{body}\n---\n" + text[fm.end():]
elif re.search(r"^##\s+Status\s*\n", text, re.M):
text = re.sub(r"(^##\s+Status\s*\n+)\s*.+$", rf"\g<1>{new_status[:1].upper() + new_status[1:]}", text, count=1, flags=re.M)
else:
raise SystemExit(f"error: no status field found in {path.name}")
path.write_text(text, encoding="utf-8")
def write_index(folder: Path) -> int:
rows = []
for num, p in adr_files(folder):
text = p.read_text(encoding="utf-8")
title = read_title(text, p.stem)
rows.append(f"| [{num:04d}]({p.name}) | {title} | {read_status(text)} | {read_date(text)} |")
lines = [
"# Architecture decision records",
"",
"Generated by new_adr.py. Do not edit the table by hand.",
"",
"| ADR | Title | Status | Date |",
"|---|---|---|---|",
*rows,
"",
]
(folder / "README.md").write_text("\n".join(lines), encoding="utf-8")
return len(rows)
TEMPLATE = """---
status: proposed
date: {date}
decision-makers: []
consulted: []
informed: []
---
# {num:04d}. {title}
## Context and problem statement
{supersedes_line}<Facts, constraints and numbers that force a decision now.>
## Decision drivers
- <driver>
## Considered options
1. <option>
2. <option>
## Decision outcome
Chosen: **<option>**, because <reason tied to the drivers>.
### Consequences
- Good: <benefit>
- Bad: <cost or risk>
### Confirmation
<How we will verify the decision holds, and the revisit trigger.>
## Pros and cons of the options
### <option>
- Good: <...>
- Bad: <...>
## More information
<Links to design docs, benchmarks, issues.>
"""
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("folder", type=Path)
ap.add_argument("title", nargs="?")
ap.add_argument("--supersedes", type=int, metavar="N")
ap.add_argument("--index-only", action="store_true")
args = ap.parse_args()
folder: Path = args.folder
if args.index_only:
if not folder.is_dir():
print(f"error: {folder} is not a directory", file=sys.stderr)
return 1
n = write_index(folder)
print(f"index rebuilt: {folder / 'README.md'} ({n} ADRs)")
return 0
if not args.title:
print("error: a title is required unless --index-only is given", file=sys.stderr)
return 1
folder.mkdir(parents=True, exist_ok=True)
existing = adr_files(folder)
num = (existing[-1][0] + 1) if existing else 1
old = None
if args.supersedes is not None:
old = next((p for n, p in existing if n == args.supersedes), None)
if old is None:
print(f"error: ADR {args.supersedes:04d} not found in {folder}", file=sys.stderr)
return 1
path = folder / f"{num:04d}-{slugify(args.title)}.md"
if path.exists():
print(f"error: {path} already exists", file=sys.stderr)
return 1
sup = f"Supersedes [ADR-{args.supersedes:04d}]({old.name}).\n\n" if old else ""
path.write_text(
TEMPLATE.format(date=dt.date.today().isoformat(), num=num, title=args.title, supersedes_line=sup),
encoding="utf-8",
)
print(f"created: {path}")
if old:
set_status(old, f"superseded by ADR-{num:04d}")
print(f"updated: {old} -> superseded by ADR-{num:04d}")
n = write_index(folder)
print(f"index rebuilt: {folder / 'README.md'} ({n} ADRs)")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.