Inkan
↑↓ move↵ open
GitHub

AI and data · ADR

0002: decisions.json

Date

Context#

llms.txt is for reading. Scripts, dashboards, badges and agents with tools want to filter: all open RFCs of one team, every record tagged database, the chain of records that superseded each other. Parsing front matter out of a Git checkout works but ties every consumer to the folder layout and to a YAML parser.

Decision#

Every build writes /decisions.json: an array with one object per record, holding the normalised front matter plus the computed fields the site itself uses (id, ref, type, project), the code references found for the record (codeRefs) and three links (url, markdown, source). Dates are YYYY-MM-DD strings, missing values are null, lists are always arrays.

How to use it#

Nothing to configure. After inkan build the file is at dist/decisions.json; in dev it is served at /decisions.json. The landing page lists it under “For agents” and every record links to it from its rail.

Shape#

decisions.json
[
{
"id": "payments/adr/0001-use-stripe",
"ref": "PAYMENTS-ADR-0001",
"title": "Use Stripe as the primary payment provider",
"description": "One provider for cards and wallets.",
"type": "adr",
"project": "payments",
"status": "accepted",
"date": "2026-01-18",
"authors": ["piotr.nowak"],
"deciders": ["tomasz.wojcik"],
"tags": ["payments", "vendor"],
"supersedes": ["payments/adr/0000-old-choice"],
"supersededBy": [],
"related": ["dbre/adr/0001-postgresql-as-default-database"],
"codeRefs": [{ "file": "services/billing/src/provider.ts", "line": 12 }],
"review": { "due": "2027-01-18", "reviewed": null, "every": "1 year" },
"url": "https://decisions.acme.dev/payments/adr/0001-use-stripe/",
"markdown": "https://decisions.acme.dev/payments/adr/0001-use-stripe.md",
"source": "decisions/payments/adr/0001-use-stripe.md"
}
]
FieldTypeMeaning
idstringPath inside the content folder without extension: <project>/<type>/<file>
refstring | nullHuman reference <PROJECT>-<TYPE LABEL>-<NNNN>, from the folder names and the file number
titlestringFront matter title
descriptionstring | nullFront matter description
typestringFolder name inside the project, one of the keys of docTypes
projectstringTop-level folder name
statusstring | nullFront matter status, one of the keys of statuses
datestring | nullFront matter date as YYYY-MM-DD
authors, deciders, tagsstring[]Front matter lists; a single value is wrapped in an array
reviewers{ handle, state, date?, note? }[]Front matter reviewers, normalised; empty when there are none. See Review RFCs with reviewers
rfcReviewobject | nullRFC review status (in-review, changes-requested, approved, closed), approval counts and waitingOn handles; null without reviewers
supersedes, supersededBy, relatedstring[]Record ids as written in front matter
codeRefs{ file, line }[]Places in the repository that mention the record in an @adr <record> comment; file is relative to the repository root. Empty when codeRefs: false. See Reference records from code
review{ due, reviewed, every } | nullThe record’s periodic review: due is the next review date, reviewed the last one (or null), every the interval that applies ("1 year", or null when only reviewBy schedules it). null for records without a schedule. See Review decisions on a schedule
urlstringAbsolute URL of the HTML page, built from url and base in the config
markdownstringAbsolute URL of the raw Markdown copy
sourcestringPath of the file relative to the repository root. For a record pulled with sources from another repository, its web URL there (https://github.com/acme/checkout/blob/main/docs/decisions/adr/0001-x.md), or <repo> <path> when no web URL can be derived

Records are sorted newest first by date, then by id. Only records are listed: Markdown pages, project intros and _landing.md are not in the file. Drafts are excluded from production builds.

Examples#

Open RFCs per project, with jq:

Terminal window
curl -s https://decisions.acme.dev/decisions.json \
| jq -r '[.[] | select(.type == "rfc" and (.status | IN("draft", "proposed", "in-review")))] | group_by(.project)[] | "\(.[0].project): \(length)"'

A README badge from the count of accepted decisions:

Terminal window
curl -s https://decisions.acme.dev/decisions.json | jq '[.[] | select(.status == "accepted")] | length'

The site already serves ready badges for the common counts at /badges/records.svg, /badges/accepted.svg and /badges/<project>.svg (and .json for shields.io); see Static HTML output.

Follow a lineage: start from a record, read supersededBy, look up that id in the same array. For the whole chain at once, /graph.json holds every resolved supersedes and related link as {nodes, edges, dangling}, see Decision graph.

Tip

The same data is available at build time from @tilaven/inkan/data (getDecisions()), which is the better choice for a custom page inside the site. decisions.json is for everything outside the build. An agent with tools gets the same index with filters from the MCP server, which reads this file when started with --url.

Stability#

The field list above is the contract. New fields may be added in later releases; existing fields keep their name and type. Consumers should ignore fields they do not know.

Custom fields declared with fields are in each record’s fields object, with normalised values (numbers as numbers, booleans as booleans, lists as arrays). See Custom metadata fields.

Consequences#

  • Any language with a JSON parser can query the archive without cloning the repository or parsing YAML.
  • The file is regenerated on every build and is only as current as the last deployment.
  • Record bodies are not included, to keep the file small. Fetch markdown for the text of one record, or llms-full.txt for all of them.
  • supersedes, supersededBy and related are copied as written. A typo in front matter is a dangling id here too; run inkan check in CI to catch it before the build.

History 1

  1. ImplementedInkan maintainers9e5e535 commit diff