Turbine for AI agents
Turbine is built to be legible to AI coding agents, not just to people. Five properties do the work: an installable skill (npx turbine skill) whose every claim is executed against a live database before release, a read-only MCP server with eleven tools that ground the agent in your live schema, a deterministic codegen flow it can run and re-run, fully typed query arguments that turn an invalid query into a compile error it can read, and stable coded errors it can branch on instead of parsing prose. This page shows how to point an agent at Turbine and gives you a snippet to drop into your project so the agent uses it correctly.
Point an agent at Turbine: llms.txt#
Turbine publishes an llms.txt at the site root, following the llms.txt convention: a concise description of the library, the key facts, and links to every docs page with a one-line summary. There is also a fuller llms-full.txt with a condensed API reference distilled from these docs.
| File | URL | What it is |
|---|---|---|
llms.txt | turbineorm.dev/llms.txt | Short index: description, key facts, and links to each docs page. |
llms-full.txt | turbineorm.dev/llms-full.txt | Condensed API reference: reads, writes, WHERE operators, transactions, errors, engines. |
To ground an agent, hand it one or both URLs, or paste them into your project's agent-instructions file (see the snippet below). Both are static text: fetch them, cache them, feed them into context.
The installable skill#
npx turbine skillwrites a query-writing skill to .claude/skills/turbine-orm/SKILL.md. It covers
the constructs an agent gets wrong on a schema it has not seen: with versus
Prisma's include, how relation names are derived, what select may and may not
name, the WHERE operator set, relation filters, per-relation options, having,
JSON paths, and which errors each mistake produces.
Two variants for other setups:
| command | what it does |
|---|---|
npx turbine skill --print | writes the skill to stdout, so you can pipe it anywhere |
npx turbine skill --agents | prints a short instructions block for AGENTS.md / CLAUDE.md |
npx turbine skill --dir <path> | installs under a different skills root |
Every factual claim in the skill is executed against a live database before
release. The repository's eval harness runs each sentence as a query and fails
the build if the answer changed, so the file cannot drift from the ORM the way
documentation usually does. That check exists because the previous version of
this skill carried a table saying a snake_case column name was rejected in
orderBy, which was true when it was written and false one release later.
What it is worth, measured#
On a held-out schema the models had never seen, across 240 scored attempts:
| cold, 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 on this harness measures about 7 points, so the skill's
contribution is well outside noise, and on both models it matched or beat the
live tool connection. Twelve of the twenty cold failures were the same mistake,
a relation named inside select, which is the fourth thing the skill says.
Method, raw records and what the numbers do not show: evals/ in the
repository.
The MCP server#
For an agent that speaks MCP, npx turbine mcp exposes the live database through eleven read-only tools, every one inside BEGIN READ ONLY, with PII-tagged columns redacted before rows reach the model. The four that matter most on an unfamiliar schema:
relation_graph: every relation name with its cardinality, target, and keys. These are the exact names awithclause accepts, so the agent reads them instead of guessing from column names.find_join_path: "how do I get fromcommentstoorgs" answered with the relation chain and thewithclause to write, as code.table_stats: the planner's row estimate and index list, so the agent checks size before writing a query that would scan fifty million rows.explain_error: a Turbine error code mapped to cause, fix, and docs link, with no database read at all.
Plus compile_query (the exact SQL a read query compiles to, without executing it), schema_overview, table_detail, sample_rows, explain_query, migrate_status, and doctor_report. Setup and the full tool table: MCP Server.
The deterministic codegen flow#
An agent works best when the same commands always produce the same typed surface. Turbine's flow is exactly that: introspect → generate → typed client.
npx turbine init # writes turbine.config.ts and a turbine/ directory
npx turbine generate # introspect the live database, emit a typed clientgenerate reads the database (or your code-first defineSchema) and writes three files to generated/turbine/:
types.ts, entity interfaces andCreate/Updateinput typesmetadata.ts, runtime schema metadataindex.ts, a typedTurbineClientsubclass withdb.<table>accessors and aturbine()factory
Because the output is generated from the schema, an agent never has to guess column names, relation names, or types, they are all present in the emitted .ts, and they move in lockstep with the database. When the schema changes, the agent re-runs generate and the type checker immediately surfaces every call site that needs updating. For code-first projects the loop is edit turbine/schema.ts → turbine push → turbine generate (see Schema & Migrations).
Why typed query args suit agents#
Turbine's query arguments are fully typed against the generated schema, so a wrong query fails at compile time with a message the agent can act on, not at runtime in production.
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- Invalid columns, operators, and relations are type errors. A misspelled field in
where, an operator that does not exist, or an unknown relation inwithwill not typecheck. An agent that runstscgets a precise pointer instead of a silent wrong result. withresults are inferred at any depth. The agent does not annotate nested shapes by hand;findManyreturns the exact nested type for thewithclause it passed.- Parameterization is guaranteed. Every user value becomes a bound
$1, $2, ...parameter;contains/startsWith/endsWithescape LIKE wildcards automatically. An agent cannot accidentally build a string-interpolated query with the typed API, there is no code path that concatenates a value into SQL. Even the raw escape hatches (db.rawanddb.sql<T>) bind every${value}as a parameter.
The practical effect: an agent can propose a query, run the type checker, and know it is at least structurally correct before it ever touches the database.
Why coded errors suit agents#
Every Turbine error extends TurbineError and carries a stable code (TURBINE_E001 through E018). Agents branch on the class or code, which is stable across versions, instead of scraping a driver's error 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
}
}Retryable failures (DeadlockError E012, SerializationFailureError E013) expose isRetryable = true as a const, so an agent can write a correct, type-checked retry loop. See Typed Errors for the full table.
Drop-in agent instructions#
Paste this into your project's CLAUDE.md or AGENTS.md so any agent working in the repo uses Turbine correctly. Trim it to what your project uses.
## 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..E018), 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, a read-only MCP server that grounds an agent in your live schema, plans, and sample rows.
- Quick Start, the fastest path from install to first typed query.
- API Reference, every method, operator, and option the snippet above refers to.
- Typed Errors, the full E001-E018 table and retry patterns.
- Schema & Migrations, code-first schemas and the migration workflow.