Skip to content

AI agents ​

Coding agents follow d1-record’s conventions best with its guide at hand and its rules in context. Use any of these, or all three.

Giving your agent the docs ​

ResourceWhat it is
llms.txtAn index of the guide, each page linked as Markdown, following llmstxt.org. Start here when an agent can fetch pages.
llms-full.txtThe whole guide in one Markdown file, about 20K tokens.
Any page + .mdEvery page as Markdown: /guide/associations.md.
node_modules/@forestfuture/d1-record/llms-full.txtThe same guide, shipped in the package. It matches the version you’ve installed, and an agent can read it without fetching anything.

Installing the agent skill ​

Install d1-record’s agent skill: its rules, the shapes of a Model and a Worker, and the mistakes to watch for, with pointers into the guide. Agents that support skills, such as Claude Code, Cursor, Codex, and others, load it when they work on d1-record code:

sh
npx skills add forestfuture/d1-record

To install the copy shipped with your installed version, sync skills from node_modules:

sh
npx skills experimental_sync

TIP

The skills CLI marks experimental_sync as experimental, so its name may change.

Adding the rules to your project ​

Agents read AGENTS.md, or CLAUDE.md for Claude Code, at the start of every task. Paste this block into yours:

md

d1-record (`@forestfuture/d1-record`) is a Rails-style Active Record for Cloudflare D1. Its guide for the installed version is `node_modules/@forestfuture/d1-record/llms-full.txt`: read the part for the task before writing code with it.

- Create tables with the generators, never by hand-declaring attributes: `npx d1-record g model Post user:references 'title:string!'` writes the migration and the Model; `npx d1-record g migration AddSlugToPosts slug:string:unique` writes a change. Apply them with `npx wrangler d1 migrations apply <db> --local`. After a hand-written migration, run `npx d1-record schema`. Never edit the generated `src/db/schema.ts`.
- A Model extends the app's base class, naming its table: `class Post extends ApplicationRecord("posts")`. Its attributes come from the schema, so it declares only behavior. Change how a column is read with an override, `ApplicationRecord("posts", { metadata: "json" })`, never by editing the schema.
- Start queries and new records from the Model, as in Rails: `User.find(id)`, `User.where(…)`, `User.build(…)`. Each Model queries its binding (`static override binding = "DB"`, usually on the app's base class) during a request. For a D1 Session, per-request `onQuery`, or tests and scripts outside a Worker, wrap the code once in `withDatabase(connect(env.DB, { … }), fn)`. Records keep the database they came from.
- Never pass a request's whole body, form or query string to a Model: name the fields, with destructuring or `User.permit(await request.json(), "email", "name")` (it reads `FormData` and `URLSearchParams` too). Assigned values are checked against the attribute's type, but that doesn't stop a user setting `role` or `isAdmin`. Check a request value's type before querying with it: `findBy({ token: null })` matches rows whose token is NULL.
- Name attributes in camelCase everywhere: `where`, `order`, `pluck`, `foreignKey`. Column names (snake_case) appear only inside `sql` fragments.
- Write custom SQL as an `sql` tagged template, which binds every value: ``where(sql`lower(${column("email")}) = ${email}`)``.
- Bulk writes run when called: `await posts.deleteAll()` and `updateAll(…)` resolve to how many rows; `insert(…)` to D1's `meta`. Make several writes atomic with one `batch([...])` of their `to…` forms (`toInsert`, `toUpdateAll`, `toDeleteAll`, …) and records' `toSave()`/`toDestroy()`. D1 has no interactive transactions, so a read-then-write can't be atomic.
- Keep the generators' `TEXT` keys, generated in code, for records saved together (`--id integer` gives a key D1 generates instead). An owner whose key is known before its INSERT (generated in code, or already saved) is written in one atomic batch with the records built through it. A key D1 generates exists only after its own write, so each level is then written in a separate batch.
- To change how those keys are made (UUIDv7, ULID, a prefix), override `static generateId` once on the app's base class; don't give each Model's key a `default`. `generateId` sees only the attribute's name, so a value made from other attributes (a slug) goes in a callback.
- Declare associations as fields, passing the target class: `posts = hasMany(Post, { dependent: "destroy" })`, `author = belongsTo(User)`. The field's name is the association's name.
- Check what writes return. `save()` and `update()` resolve to `false` when validation fails (reasons in `record.errors`) or a callback aborts (`record.abortReason`). `destroy()` resolves to `false` when a callback aborts, or a `restrictWithError` association refuses (reason in `record.errors`). The `…OrThrow` methods throw instead. Constraint errors, such as `RecordNotUnique`, always throw.

Copy it again after upgrading d1-record, since the rules can change.