Turbine for AI agents
A coding agent can use Turbine correctly on a schema it has never seen. It gets a tested query skill, a read-only MCP server, a generated client whose types catch bad queries, and stable error codes to branch on.
The installable skill#
npx turbine skillThis writes a query-writing skill to .claude/skills/turbine-orm/SKILL.md. It covers what agents get wrong on an unfamiliar schema: with instead of Prisma's include, how relation names are derived, what select may name, WHERE operators, relation filters, having, JSON paths, and the error each mistake produces.
| Command | What it does |
|---|---|
npx turbine skill --print | Write the skill to stdout. |
npx turbine skill --agents | Print a short block for AGENTS.md / CLAUDE.md. |
npx turbine skill --dir <path> | Install under another skills root. |
Every factual claim in the skill is run as a query against a live database before each release, so it cannot drift from the ORM.
On a held-out schema, across 240 scored attempts, first-try pass rates were:
| Schema only | + the skill | + a live MCP connection | |
|---|---|---|---|
claude-sonnet-5 | 73% | 97% | 93% |
claude-haiku-4-5 | 60% | 97% | 90% |
Run-to-run variance is about 7 points. Twelve of the twenty schema-only failures were one mistake: a relation named inside select. The method and raw records are in evals/ in the repository.
The MCP server#
npx turbine mcp gives an agent eleven read-only tools over your live database. The most useful on an unfamiliar schema:
relation_graph: every relation name with its cardinality and keys, the exact nameswithaccepts.find_join_path: the relation chain from one table to another, with thewithclause to write.table_stats: the row estimate and indexes, so the agent checks size before writing a scan.compile_query: the SQL a read query compiles to, without running it.explain_error: a Turbine error code's cause and fix, without a database.
Setup and the full tool list: MCP Server.
llms.txt#
| File | What it is |
|---|---|
| turbineorm.dev/llms.txt | A short index: what Turbine is, key facts, and a link to each docs page. |
| turbineorm.dev/llms-full.txt | A condensed API reference: reads, writes, WHERE operators, transactions, errors, engines. |
Both follow the llms.txt convention. Give an agent the URLs, or reference them in your instructions file.
Generate, then typecheck#
npx turbine init # writes turbine.config.ts and a turbine/ directory
npx turbine generate # introspect the live database, emit a typed clientgenerate writes types.ts, metadata.ts and index.ts to generated/turbine/. Column names, relation names and types are in the emitted code, so the agent never guesses them. After a schema change, it re-runs generate and the type checker points at every call site to update. Code-first projects loop edit turbine/schema.ts, turbine push, turbine generate (see Schema).
const users = await db.users.findMany({
where: { orgId: 1 },
with: { posts: { with: { comments: { with: { author: true } } } } },
});
users[0].posts[0].comments[0].author.name; // inferred end to end, no manual casts- A misspelled field, an unknown operator or an unknown relation in
withis a type error, sotscreviews the query before it runs. withresults are typed at any depth.- Every value is a bound parameter, in the builder and in
db.raw/db.sql<T>alike.
Coded errors#
Every error carries a stable code (TURBINE_E001 to TURBINE_E020), so an agent branches on the class or code instead of parsing text.
import { SerializationFailureError, UniqueConstraintError } from 'turbine-orm';
try {
await db.orders.create({ data });
} catch (err) {
if (err instanceof UniqueConstraintError) {
// E008, duplicate; surface a clean conflict
} else if (err instanceof SerializationFailureError && err.isRetryable) {
// E013, retry the transaction; the flag is a typed const, so the compiler knows it is safe
}
}DeadlockError (E012) and SerializationFailureError (E013) have isRetryable: true as a const. See Typed Errors.
Drop-in agent instructions#
Paste this into your project's CLAUDE.md or AGENTS.md, trimmed to what you use.
## Database: Turbine ORM
This project uses turbine-orm (Postgres-first, Prisma-like API). Reference:
https://turbineorm.dev/llms.txt and https://turbineorm.dev/llms-full.txt.
Client
- Import the generated client: `import { turbine } from '@/generated/turbine'`.
- `const db = turbine()` reads DATABASE_URL. Call `db.disconnect()` on shutdown.
- Never hand-write entity types; run `npx turbine generate` after any schema change
and import types from `generated/turbine`.
Reads
- `db.<table>.findMany({ where, with, orderBy, limit, offset, cursor, select, omit })`.
`take`/`skip` are accepted as aliases for `limit`/`offset`.
- Nested relations go in `with`, never Prisma's `include`, and never as N+1 loops or
manual joins. An unrecognized option is IGNORED, so an `include` returns rows with
the relation missing. `with` results are fully typed at any depth.
- `select` and `omit` name columns only. A relation named in `select` throws (E003);
relations carry their own nested `select`.
- Under `relationLoadStrategy: 'join'` the whole `with` tree resolves in ONE SQL
statement (json_agg). The default is `'auto'`, which keeps that plan but may move
an individual relation (or `_count`) to one flat follow-up statement when the join
would be slower, e.g. an unindexed correlation column. The returned rows are
identical either way, so do not assert on statement counts unless the query pins
a strategy.
- `findUnique` needs a `where` that identifies one row (PK, a unique column, or every
column of a compound unique) and returns `T | null`; anything else throws E003.
`findUniqueOrThrow` throws NotFoundError (E001) on a miss. Use `findFirst` for
"any row matching a filter", with an `orderBy` if which row matters.
Writes
- `create`, `createMany`, `update`, `updateMany`, `delete`, `deleteMany`, `upsert`.
- For counters use atomic operators: `data: { viewCount: { increment: 1 } }`.
- `update`/`delete` with an empty `where` throws ValidationError (E003) by design.
- Relation ops inside `data` (connect/create/disconnect/set/update/upsert) run in one tx.
Safety
- Every value is parameterized ($1, $2, ...); never string-interpolate SQL. Use `db.raw`
or `db.sql<T>` (both bind `${value}` as params) only when the builder can't express it.
- Wrap multi-step writes in `db.$transaction(async (tx) => { ... })`.
Errors
- Catch typed errors and branch on the class/code (TURBINE_E001..E020), not the message.
- Retry only when `err.isRetryable` is true (DeadlockError E012, SerializationFailureError E013).
Migrations
- Local iteration: edit `turbine/schema.ts` (defineSchema), then `turbine push`, then
`turbine generate`. Production: SQL migrations via `turbine migrate` (never edit an
applied migration file, checksums are validated).See also#
- MCP Server: the eleven read-only tools.
- Quick Start: from install to a first typed query.
- Reading and writing: every method and option the snippet refers to.
- Typed Errors: every error code.