Skip to content
Chapter 101Lesson 3

AGENTS.md, the conventions file

Write AGENTS.md, the single file that briefs coding agents and new contributors on a repo's commands, conventions, and watch-outs.

Last lesson’s thin README routed every reference doc to the source that owns it: the schema is the data-model doc, env.ts documents the env vars, a Server Action’s signature is its own API doc. That leaves the conventions homeless, the build and test commands, the “Server Components by default” rules, the traps a newcomer hits on day one. None of it belongs in a first-contact README, and none of it lives in any single source file.

Its home is AGENTS.md: one plain-markdown file at the repo root, written for two readers at once, the coding agents that work in the codebase daily and the humans who skim it to learn how the team works. By the end of this lesson you’ll write a high-signal AGENTS.md for a SaaS repo and know which facts belong in it rather than the README, a source file, or an ADR. This course ships from a repo that has one at its root, so open it in another tab and read along.

AGENTS.md is a markdown file, named exactly that, at the repository root. There’s no schema, no required headings, no special syntax: it’s prose, and whatever you write, an agent reads.

A coding agent dropped into an unfamiliar repo has the same problem a new hire does: it doesn’t know your build command, your conventions, or the rules you’ve all silently agreed to follow. It reads AGENTS.md to load that context before touching a line of code. This is a real, adopted standard, used across tens of thousands of repos and the major coding tools.

With no schema to satisfy and no validator to pass, the file’s value rests entirely on what you put in and leave out. The metric isn’t completeness or section count, it’s signal per line. A short file where every line earns its place beats a thorough one where half the lines are noise, because noise buries the lines that matter.

Before a coding agent can do useful work it needs the exact commands to build, test, and lint; where things live, like which folder holds routes and which holds the data layer; the conventions, so its code matches what’s already there; and the watch-outs, the rules you can’t see by reading the code. A human contributor in their first week needs that same list.

So you write one file for both, in plain English with no agent-specific syntax. Agents parse the same prose a person does, and a sentence that wouldn’t help a human doesn’t belong in a file a human is meant to read.

Brief AGENTS.md the way you’d brief a sharp new hire over coffee: be direct, skip the filler, give the non-obvious rules and the exact commands, and add an example only where it helps.

One file, two readers, and where it sits between the README and the ADR log.

What belongs in the file: the inclusion test

Section titled “What belongs in the file: the inclusion test”

With no schema to tell you what goes in, you need a filter:

Would a competent developer or agent joining next Monday need this to be productive in week one?

Run every candidate line through it. If yes, it goes in. If no, the line isn’t neutral: it’s noise that dilutes the lines that matter, because the reader has to wade past it to reach them.

See the filter at work:

  • The pnpm test command: yes, they run it on day one.
  • The team’s Slack channel: no, that’s onboarding trivia, not week-one productivity.
  • “We use Drizzle for all database access”: yes, it changes how they write code immediately.
  • Why you used Prisma before switching: no, that’s an architectural decision and belongs in an ADR.
  • The full list of environment variables with their types: no, that already lives in env.ts, so point at it rather than copy it.

That last one is the “could this be a link?” reflex from two lessons ago, applied inside AGENTS.md. The file is a curated brief, not a vault.

There’s no required structure, but a few sections show up in most SaaS AGENTS.md files, because most SaaS repos share the same week-one needs. We’ll build the file one section at a time, each justified by the inclusion test, then assemble it.

One paragraph of orientation, so everything after it has context: a sentence or two on what the product does, who uses it, and the stack core. It’s the README’s opening, just terser.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.

This is one of exactly two things AGENTS.md deliberately shares with the README; we’ll return to why that duplication is acceptable when we draw the boundaries.

Neither a newcomer nor an agent can navigate the codebase without a map. This section is a directory listing, so render it as one; the dimmed comments are the whole explanation.

  • Directorysrc/
    • Directoryapp/ Next.js routes (Server Components by default)
    • Directorycomponents/ shared React components
    • Directorydb/ Drizzle schema, relations, and the client
      • schema.ts source of truth for all tables
    • Directorylib/ pure helpers and side-effect adapters
    • Directoryserver/ Server Actions (mutations)
    • env.ts validated environment variables
  • Directorytests/ integration tests against real Postgres

A map, not a tour: enough to point someone at the right folder, with the detail living in the folders.

The most-used section, the first thing an agent looks for and a new hire copies. List the exact commands plainly; narration between them would only dilute the signal.

## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:generate # generate a migration from schema changes
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database

These are the script names from your README’s “Common tasks” section, because both files read from one source of truth: the scripts block in package.json. Rename a script there and both docs update in the same breath.

This is what stops a newcomer’s first pull request from getting bounced for rules they couldn’t have known. State the non-obvious decisions, one line each; the density is the value. A reader scans the list, absorbs the house style in fifteen seconds, and writes code that fits.

## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for third-party
webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
- A file that exports one thing is named after it, kebab-cased.

By normal code-review standards this looks like over-commenting. But stating conventions is this file’s job: the one-liners aren’t redundant, they’re the deliverable. “Code explains itself, comments are for the non-obvious why” governs your .ts files; AGENTS.md is the one place the conventions get written down on purpose.

The section people underrate. A competent newcomer’s mistakes are predictable: things that look reasonable but break something in your specific codebase. Naming them up front is pure signal, three lines that save a failed run, a confusing error, or a burned review cycle.

## Don't
- Don't import server modules into Client Components — they leak
secrets. Next.js catches it, but naming it saves a wasted run.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch data on the server — fetch
in a Server Component.
- Don't ship `console.log` — the `no-console` lint rule fails CI.

Keep this to rules, not reasons. When a “don’t” rests on a real architectural decision, state the rule here and link the why to the ADR; a paragraph of justification means you’ve drifted into ADR territory.

They’ll open a pull request that week, so they need the house rules, but only the rules.

## Pull requests
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`.
- CI must be green before merge; no force-pushing to `main`.

How to review a pull request is a topic of its own and lives elsewhere; here you just need the contract for getting one merged.

Links, not copies. This section is the “could this be a link?” reflex made into a heading.

## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

This is the seam where AGENTS.md hands off to the docs that live next to the truth. The env-var list, the schema, and your architectural reasoning aren’t copied here; each is a link, because a copy drifts the moment the source changes.

Now read the file as one piece: the sections you built, in order, as a single coffee-brief for the invoice SaaS. Notice how each section does exactly one job and stops.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

One paragraph of orientation: what, who, the stack core. The README’s opening, terser. Everything after it now has context.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

A map, not a tour. Just enough to point a person or an agent at the right folder; the detail lives inside the folders.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

The most-used section. Exact commands, no narration, mirroring package.json — the same source the README’s tasks read from.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

The house style in five lines: the high-signal core that stops a newcomer’s first PR from breaking rules they couldn’t have known.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

Predictable mistakes, pre-empted. Three lines to save a failed run. Rules only; the why links out to an ADR.

AGENTS.md
Multi-tenant invoice management SaaS. Organizations sign up, invite
teammates, and issue invoices to their customers. Stack: Next.js 16
(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)
- src/components — shared React components
- src/db — Drizzle schema, relations, and the client
- src/server — Server Actions (mutations)
- src/lib — pure helpers and side-effect adapters
- tests — integration tests against real Postgres
## Commands
pnpm install # install dependencies
pnpm dev # start the dev server
pnpm test # run the test suite
pnpm db:push # push schema changes to the local database
pnpm db:seed # seed local data
pnpm db:migrate # apply pending migrations
pnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.
- Server Actions for mutations; route handlers only for webhooks.
- Zod schemas live next to the action that uses them.
- Never `any` — use `unknown` and narrow at the boundary.
- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.
- Don't write raw SQL outside Drizzle.
- Don't reach for `useEffect` to fetch server data.
- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/
- Data model: src/db/schema.ts
- Environment variables: src/env.ts

The handoff. Links to the schema, env, and ADRs, never copies. The “could this be a link?” reflex, made into a section.

1 / 1

A complete AGENTS.md that fits on one screen. Compare it to the course’s own root AGENTS.md: shaped a little differently, because there’s no single template, only the filter. Both are short, both are scannable, and neither has a section that’s there “for completeness.”

An agent editing a file doesn’t read every AGENTS.md in the repo. It walks up the directory tree and uses the nearest one. The files are not merged: a closer AGENTS.md replaces the root for everything under its folder rather than layering on top. That is what makes overrides work: the root sets sensible defaults, and one corner of the codebase, say a monorepo package, can ship its own AGENTS.md where it needs different rules.

  • AGENTS.md repo-wide defaults
  • Directorypackages/
    • Directoryemail-templates/
      • AGENTS.md overrides for working on email templates

The course’s stack is a single app, so the root AGENTS.md is all you need today. The day you split out a packages/ directory, each package can carry its own brief without contradicting the root.

Where AGENTS.md ends and the other docs begin

Section titled “Where AGENTS.md ends and the other docs begin”

Three sharp boundaries keep your docs from sprawling into each other.

Versus the README. The README serves first contact in the reader’s first hour; AGENTS.md serves real work in their first week. They share exactly two things, the one-paragraph overview and the local-setup commands. After that they diverge: the README links out, AGENTS.md goes deep on conventions.

Versus ADRs. This is the boundary people blur most. AGENTS.md states what the convention is: “Drizzle for all database access.” An ADR records why and what it cost: “Drizzle over Prisma, for control over the generated SQL and a smaller client, at the price of a less mature plugin ecosystem.” AGENTS.md links to the ADR; it never carries the reasoning. Write “we chose Drizzle because of these trade-offs” and you’ve bloated the conventions file with content that has a proper home.

Versus source-as-doc. Reference material lives in the source file that owns it: the env-var list in env.ts, the columns and constraints in schema.ts, the API contract in the Server Action’s TSDoc. AGENTS.md links to all of them.

Two failure modes recur. The first treats AGENTS.md as the junk drawer for everything that fits nowhere else; the file balloons and nobody reads past line 50. The second copies a reference list in instead of linking it; two copies of one truth means one is wrong within a month, and you won’t know which.

Each fact below belongs in exactly one document. Sort it into the doc that owns it — the canonical home, not just a place it could appear. Drag each item into the bucket it belongs to, then press Check.

README First contact
AGENTS.md Conventions + commands
A source file schema.ts / env.ts / the action
/docs/adr/ Why a decision was made
The exact pnpm test command
The GitHub landing description a recruiter reads first
The full list of required environment variables with their types
The reasoning behind choosing Better Auth over Clerk
The rule that Server Actions handle mutations and route handlers are only for webhooks
The invoices table’s columns and constraints
Why the app runs on the Node runtime instead of Edge
The “don’t import server modules into Client Components” convention

Signal density: cut sections, don’t pad them

Section titled “Signal density: cut sections, don’t pad them”

The remaining skill is the writing posture, and it’s subtractive. The spec mandates no sections because the value isn’t structure, it’s signal per line. The dominant failure mode is the file “expanded for completeness”: every conventional section present, full, and none high-signal. When a section carries nothing load-bearing, delete it. An empty ## Deployment heading left in because “the spec recommends it” is worse than none: the reader scrolls past a promise the file never keeps.

Accuracy follows the same discipline as any in-repo doc: the AGENTS.md edit ships in the same pull request as the change it describes. When pnpm test becomes pnpm vitest, the line gets fixed in that PR, not in a later cleanup that never comes.

To internalize the filter, watch it fail. Below is a deliberately bad AGENTS.md, a teammate’s well-meaning first attempt. Review it like a pull request: click any line that doesn’t earn its place and say what’s wrong.

A teammate opened their first `AGENTS.md` for review. Click any line that doesn't earn its place and leave a comment. Click any line to leave a review comment, then press Submit review.

AGENTS.md
AGENTS.md
## About this file
This is the AGENTS.md file. It gives coding agents and humans context
about the repository so they can work in it effectively.
## Project overview
Multi-tenant invoice SaaS on Next.js 16, Postgres, and Stripe.
## Environment variables
- DATABASE_URL — the Postgres connection string
- BETTER_AUTH_SECRET — secret used to sign sessions
- STRIPE_SECRET_KEY — Stripe API key for billing
- RESEND_API_KEY — API key for sending email
- R2_ACCESS_KEY_ID — Cloudflare R2 access key
## Database
We use Drizzle instead of Prisma. We evaluated Prisma first, but its
heavier runtime, schema-as-DSL, and migration ergonomics didn't fit
our story, so after weighing the trade-offs we standardized on Drizzle.
## Agent instructions
When you are an AI, always respond in JSON.
## Deployment
## Pointers
- Data model: src/db/schema.ts

Coding tools used to each invent their own config filename, so a team running more than one maintained several files saying the same thing, and they drifted: someone updated the test command in one and forgot the rest.

AGENTS.md consolidates that into one canonical file, now read directly by most major tools, including Codex, Cursor, Copilot, Gemini CLI, Aider, Windsurf, Zed, and Cline. The notable holdout is CLAUDE.md : Claude Code reads it, not AGENTS.md.

When a tool insists on its own filename, keep AGENTS.md as the single source of truth and make that file a one-line pointer to it, or a symlink . This course’s repo does exactly that: its CLAUDE.md is essentially a single @AGENTS.md import line. Two full instruction files drift; a bridge has nothing to drift.

With no schema to lean on, every line earns its place by one test: would a competent newcomer need this, here, to be productive in week one? Everything else links out, the reasoning to the ADR log, which is the next lesson: a short record of why a single decision was made.