Architecture Decision Records
An Architecture Decision Record (ADR) is a short, dated record of one important technical decision: the context, the decision, the alternatives, and the consequences.
The company process is defined in the Notion page Engineering Technical Decisions and its sub-page Architecture Decision Records. This README applies that process to this repository and records the conventions that the reviews of the first records settled.
When to write an ADR
Write an ADR when the reason for a decision can matter in the future. The test from the company process: if an engineer challenges this choice in 12 months, will the original context and alternatives help them?
Write one record for each decision. The test: can this decision be reversed on its own? If yes, it gets its own file. Do not put two independent decisions into one record.
Do not write an ADR for an implementation detail, a refactor, or a choice that follows an existing decision.
Location
Store a record in the package where the implementation lives, in its
docs/adr/ directory. For example, a record about engine v2 goes in
packages/@n8n/engine/docs/adr/. A record that concerns the whole repository
goes in the root docs/adr/, next to this README. CI rejects any other
location.
File name
The file name follows from the title and the date:
ADR-YYYYMMDD-<title in kebab-case>.md
The title is the H1 of the record. It states the decision, not the topic:
"Use PostgreSQL advisory locks", not "Database locking". The date is the date
of the decision, and it matches the Date field. CI checks both.
Do not rename a record after it is merged. Links depend on the file name.
Refer to a record by its full file name, for example
ADR-20260828-obtain-trigger-output-before-creating-the-execution, not by a
number. CI checks that every record ID a record mentions names a file.
Template
Copy docs/ADR_TEMPLATE.md from the repository root.
It is the one template, and CI checks every record against its structure.
The header has three required fields, Date, Status, and Decision Owner,
in that order, and three optional fields, Source, Supersedes, and
Superseded by. Omit an optional field that has no value. Source names
where the decision came from: a ticket, an RFC, or an incident. The body
names none of these. The RFC field under Links holds the link to the RFC.
Status
Use one of three values:
- Active. The decision stands.
- Superseded. A newer record replaced the decision. Link to it in
Superseded by. - Deprecated. The decision no longer applies and no record replaces it.
There is no Draft or Proposed status. The pull request holds that state while the record is written and checked.
The status says whether the decision stands. It does not say whether the code exists. Implementation progress belongs nowhere in the record.
What goes where
Context. The problem and the constraints. State the world as it is, in
the present tense: "In engine v1, putExecutionToWait sleeps in the process
for a time wait under 65 seconds." Do not date a fact ("at the date of this
record"), do not narrate history ("core has since decided"), and do not phase
work ("the shim's part lands later"). The record is dated once, in its header.
Aim for fewer than 150 words.
Decision. What we decided, and the main reason. State the behaviour the decision guarantees, in the present tense and the active voice: "A wait fires at its deadline and not before it. A wait fires once, however many replicas run." Describe the mechanism only when the mechanism is the decision. A sweep that sleeps until the next deadline is how the engine fires waits today; the guarantee is what the record decides. Do not write implementation state here. A sentence such as "the engine does not do this yet" goes stale exactly where a reader takes the text as the current contract.
Alternatives Considered. Each option we did not select, and why. Lead with the reason: "We rejected it for two reasons." An option that reads as an equal choice leaves the reader to guess why it lost. Use the conditional mood for what an alternative would do: "This option would need a column." The indicative mood ("this option needs a column") reads as a description of shipped code.
Consequences. What follows from the decision: benefits, trade-offs, constraints, and operational effects. A constraint on the mechanism belongs here: "One statement must read the step rows and write the status." How the code meets the constraint today, and the order in which the work lands, do not. This is the one section that may grow after the record is merged. A consequence that the team learned later belongs here. A changed decision does not.
Links. Exactly three fields, each on one line: RFC, Documentation, and
Related ADRs. Write - when a field has no value. Name related records by
full file name. There is no Tickets field.
Changing a decision
A merged record is history. Do not edit its Context, Decision, or Alternatives to reflect a later change. Write a new record instead.
Therefore a change to the Context, the Decision, or the Alternatives rides the pull request that merges the record. In a stack, the change belongs in the first pull request that carries the record, not in the follow-up that implements it. A decision can merge before its code. That is normal, and not a reason to move or edit the record.
When a new decision replaces an old one:
- Write a new record. Explain the new context.
- Set
Supersedesin the new record andSuperseded byin the old one. - Set the old record's status to
Superseded.
When a decision no longer applies and nothing replaces it, set its status to
Deprecated.
Style
- Write in ASD-STE100 Simplified Technical English, as
AGENTS.mdrequires: short sentences, the active voice, one idea for each sentence. - Wrap paragraph lines at 100 columns. CI enforces it. The
Linksfields are exempt and stay on one line each. - Do not reference a ticket in the body. Every sentence must stand without
the ID: "Send-and-wait nodes need credentials to send their message." The
Sourcefield is the one place for a ticket. - Use plain words. "Nothing moves a step from
waitingtocompletedin one step" beats "No component completes a waiting step directly". - Keep the record short. A reader should understand the decision in a few minutes without the pull request or the design document.
- Use a Mermaid diagram when a picture says it better than prose.
Checked by CI
The adr-conventions rule of @n8n/code-health runs in the Static Analysis
check of every pull request. For every ADR-*.md file it checks:
- a
docs/adrdirectory at the repository root or at a package root; - a file name that matches the H1 and the
Datefield; - the header fields, their order, and one blank line between them;
- a
Decision Ownerfrom the allowed list inpackages/quality/policy/code-health/src/index.ts; - a full file name in
SupersedesandSuperseded by; - exactly the five sections, in order, each with content;
- the three
Linksfields, one line each; - paragraph lines of at most 100 characters outside
Links; - that every record ID the record mentions is a full ID that names a file in
this repository. A date alone,
ADR-20260902, is rejected.
Run it locally:
pnpm --filter=@n8n/code-health check --rule=adr-conventions --ignore-baseline
Process
- The decision is made: in an RFC, a design review, a team discussion, or a pull request.
- The Decision Owner, or a delegate, writes the record.
- Open a pull request. The review checks that the record is accurate and clear. It does not reopen the decision.
- Merge. The record is
Active. - Implementation pull requests link to the record they implement.