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.

  • Architecture
  • $architecture-decision-records
  • 198 lines
  • 3 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- architecture-decision-records

Installs to ~/.agents/skills/architecture-decision-records/ for every project on this machine.

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.

~/.agents/skills/architecture-decision-records/SKILL.md

--- name: architecture-decision-records description: Writes and maintains Architecture Decision Records (ADRs) in Markdown using the Nygard or MADR 4 format - one numbered file per decision with… license: MIT ---

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.

SituationADR?
Choosing a datastore, message broker, auth provider, cloud serviceYes
Adopting a pattern repo-wide (outbox, CQRS, monorepo, feature flags)Yes
Public API style, versioning or wire formatYes
Rejecting a popular option the team will keep being asked aboutYes, status rejected
Library swap behind an existing interface, local refactorNo, a PR description is enough
Coding style, lint rulesNo, put it in AGENTS.md or the linter config
Multi-week design with diagrams and rollout planWrite a design doc ($design-docs-rfcs) and an ADR that links to it

Before writing

  1. Read AGENTS.md, then look for an existing log: ls docs/adr docs/decisions doc/adr adr 2>/dev/null and rg -l "^## Status|^status:" docs. Follow the format already in use; never start a second numbering scheme.
  2. Read the ADRs that touch the same area. A new decision often supersedes or amends one.
  3. Collect the facts the context needs: current constraints, load figures, team skills, deadlines, costs. An ADR without numbers is an opinion.
  4. Identify who decides and who must be consulted. Codex drafts; humans accept.

Choose a format

FormatUse 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 issueLightweight 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

FromToHow
proposedacceptedPR approved by the named decision-makers; set the date of acceptance
proposedrejectedKeep the file; record why. Rejected ADRs stop the same debate recurring
acceptedsupersededNew ADR states Supersedes ADR-0011; old one gets superseded by ADR-0015
accepteddeprecatedThe 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 accepted is 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 in AGENTS.md so future sessions find it.

Verification

  • python3 scripts/new_adr.py docs/adr --index-only then git diff docs/adr/README.md: every file appears once, numbers are contiguous, statuses match each file.
  • rg -n "superseded by" docs/adr and 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.yamlgenerated from this page

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.py185 lines, 5.2 KB
#!/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.

esc