Turbine

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 skill

This 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.

CommandWhat it does
npx turbine skill --printWrite the skill to stdout.
npx turbine skill --agentsPrint 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-573%97%93%
claude-haiku-4-560%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 names with accepts.
  • find_join_path: the relation chain from one table to another, with the with clause 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#

FileWhat it is
turbineorm.dev/llms.txtA short index: what Turbine is, key facts, and a link to each docs page.
turbineorm.dev/llms-full.txtA 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 client

generate 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 with is a type error, so tsc reviews the query before it runs.
  • with results 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#