SDK reference
npm i @persql/sdknew PerSQL(options)
Section titled “new PerSQL(options)”import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN!, baseURL: "https://api.persql.com", // optional, this is the default});| Option | Type | Default | Notes |
|---|---|---|---|
token | string | required (unless local) | psql_live_… bearer |
baseURL | string | https://api.persql.com | Override for self-hosted / staging |
fetch | typeof fetch | globalThis.fetch | Inject your own fetch (useful in Node 18-, edge runtimes) |
local | string | — | Path to a SQLite file or ":memory:". Routes query / batch / tables / explain / schema through better-sqlite3 (lazy-loaded optional peer dep) instead of HTTP. See Local testing. |
persql.database(path)
Section titled “persql.database(path)”const db = persql.database("acme/orders");// orconst db = persql.database("acme", "orders");Returns a PerSQLDatabase.
db.query(sql, params?, options?)
Section titled “db.query(sql, params?, options?)”const result = await db.query<{ id: number; email: string }>( "SELECT id, email FROM customers WHERE id = ?", [42], { idempotencyKey: "fetch-customer-42-once" });
console.log(result.data); // [{ id: 42, email: "ada@example.com" }]console.log(result.columns); // ["id", "email"]console.log(result.rows); // [[42, "ada@example.com"]]console.log(result.rowsRead); // 1db.batch(statements, options?)
Section titled “db.batch(statements, options?)”const results = await db.batch([ { sql: "INSERT INTO orders (id, total) VALUES (?, ?)", params: ["o1", 99.5] }, { sql: "INSERT INTO orders (id, total) VALUES (?, ?)", params: ["o2", 12.0] }, { sql: "SELECT COUNT(*) AS n FROM orders" },]);console.log(results[2].data); // [{ n: 2 }]options.transaction: true wraps the batch — first error rolls back
everything.
db.transaction(statements, options?)
Section titled “db.transaction(statements, options?)”Sugar for db.batch(stmts, { transaction: true }). See
Batch and transactions.
db.tables()
Section titled “db.tables()”const tables = await db.tables();// [{ name: "orders", rowCount: 1284 }, ...]Discoverability — describe, search, doctor
Section titled “Discoverability — describe, search, doctor”The three calls an agent should make on a fresh database, in order.
db.describe()
Section titled “db.describe()”Returns the entire schema graph plus any stored semantic descriptions in one round-trip — designed to be JSON-stringified straight into an LLM prompt.
const schema = await db.describe();// {// databaseDescription: "Order data for acme",// tables: [// {// table: "orders",// description: "One row per checkout",// columns: [// { name: "id", type: "INTEGER", pk: true, notNull: true, description: "" },// { name: "customer_id", type: "INTEGER", pk: false, notNull: true,// description: "FK → customers.id" },// ],// foreignKeys: [{ from: "customer_id", toTable: "customers", toColumn: "id" }],// },// ],// }db.setDescription(input)
Section titled “db.setDescription(input)”Persist semantic docs (admin token role required). Pass any subset.
await db.setDescription({ databaseDescription: "Order data for acme", tables: [ { table: "orders", description: "One row per checkout", columns: [ { column: "customer_id", description: "FK → customers.id" }, ], }, ],});db.search(query, options?)
Section titled “db.search(query, options?)”Natural-language ranked search over table names, column names, and stored descriptions. Use when you don’t know the schema cold.
const { hits } = await db.search("customer billing addresses");// [{ kind: "table", table: "addresses", description: "...", score: 6 }, ...]| Option | Type | Default | Notes |
|---|---|---|---|
limit | number | 25 | 1–100 |
db.doctor()
Section titled “db.doctor()”Lints the schema for issues that hurt LLM consumption. Read-only — emits suggestions, never modifies anything.
const { findings } = await db.doctor();// [// { code: "missing_pk", severity: "warning", table: "events", column: null,// message: "Table \"events\" has no primary key.",// suggestion: "Add an integer PRIMARY KEY..." },// { code: "ambiguous_column_name", severity: "info", ... },// ]Pair with CI: fail the build if any severity === "error" finding appears.
db.queryLog(options?)
Section titled “db.queryLog(options?)”Engine telemetry — one entry per /v1/query and /v1/batch call. The
same data also lives inside the database as _persql_meta_query_log, so
you can JOIN against it from your own SQL.
const { data } = await db.queryLog({ since: new Date(Date.now() - 60 * 60 * 1000), status: "error",});| Option | Type | Notes |
|---|---|---|
since / until | Date | string | Filter by timestamp |
tokenId | string | Per-token cost partitioning |
status | ”ok” | “error” | |
cursor / pageSize | string / number | Pagination |
db.branches.claim(options?)
Section titled “db.branches.claim(options?)”One-shot lease: create-or-reset a branch with a TTL and mint a scoped token in the same call. See Claim a branch.
const lease = await db.branches.claim({ purpose: "agent-run-fix-issue-742", ttlSec: 3600, // default role: "write", // default});// lease.branchRef, lease.token, lease.expiresAtdb.proposals.propose(sql, options?) / db.proposals.apply(token)
Section titled “db.proposals.propose(sql, options?) / db.proposals.apply(token)”Pre-flight a write. propose() validates SQL via EXPLAIN, estimates
affected rows, and returns a single-use executionToken. apply()
runs it. See Propose / apply.
const plan = await db.proposals.propose( "UPDATE users SET tier = ? WHERE company_id = ?", { params: ["enterprise", 42] });// plan.estimatedAffectedRows, plan.plan, plan.executionTokenconst result = await db.proposals.apply(plan.executionToken);db.approvals
Section titled “db.approvals”Approvals are a first-class primitive — get status without consuming, wait for a reviewer, or subscribe to changes. See Approval rules.
// Throws ApprovalRequiredError carrying approvalToken + approvalUrl.try { await db.query("UPDATE production_users SET …");} catch (e) { if (e instanceof ApprovalRequiredError) { // 1) Look up status without redeeming: const status = await db.approvals.get(e.approvalToken); // status.status: "pending" | "approved" | "denied"
// 2) Block until a reviewer decides (or expiry): const final = await db.approvals.poll(e.approvalToken, { intervalMs: 2000, timeoutMs: 10 * 60 * 1000, });
// 3) Once approved, run the original write: if (final.status === "approved") { const result = await db.approvals.redeem(e.approvalToken); } }}
// Poll-based push for clients that can't host a webhook:const sub = db.approvals.subscribe([token1, token2], { onApprovalResolved: ({ approvalToken, status }) => { /* … */ },});// sub.stop() to cancelFor push-style delivery, prefer the approval_required /
approval_resolved webhook events (see Webhooks).
db.approvalRules
Section titled “db.approvalRules”Manage require_approval / deny rules from code. List is open to
any bearer with read access; create and delete require an
admin-role token.
const rules = await db.approvalRules.list();
const rule = await db.approvalRules.create({ tableGlob: "production_*", action: "require_approval", note: "All production tables need human sign-off",});
await db.approvalRules.delete(rule.id);db.branches.pin(ref, options?)
Section titled “db.branches.pin(ref, options?)”Mint a single-use handoff token (phand_…) pinned to one (database,
branch, role). Used by orchestrator agents to delegate to sub-agents.
See the sub-agent handoff recipe.
const { token } = await db.branches.pin("pr-42", { role: "write", // "read" | "write" | "admin" ttlSec: 900, // 60–86400, default 900});PerSQL.fromHandoff(token, options?)
Section titled “PerSQL.fromHandoff(token, options?)”Sub-agent side. Redeems a phand_… for a regular psql_live_… token
scoped exactly to the pinned (database, branch, role).
const sub = await PerSQL.fromHandoff(handoffToken);const db = sub.database(sub.handedOff.namespaceSlug, sub.handedOff.databaseSlug);db.asTool(name?)
Section titled “db.asTool(name?)”One fat SQL tool definition for every major function-calling API. See Tool use.
const tool = db.asTool("query_orders");// tool.anthropic — Anthropic-shaped tool// tool.openai — OpenAI Chat Completions-shaped tool// tool.aiSdk() — Vercel AI SDK { [name]: { description, inputSchema, execute } }// tool.mastra() — Mastra { [name]: { id, description, inputSchema, execute } }// tool.langchain() — LangChain / LangGraph [{ name, description, schema, invoke }]// tool.openaiAgents() — OpenAI Agents SDK [{ type, name, description, parameters, invoke }]Same shapes as db.asTools(), just one tool instead of a per-table
bundle.
db.asTools()
Section titled “db.asTools()”Full agent-surface tool bundle. Per table: select_/count_/describe_/insert_/update_/delete_<table>.
Discovery: describe_database, search_schema, schema_doctor.
Safety: propose_mutation, apply_mutation. Telemetry: recent_queries.
Branches: claim_branch, branches_list/create/delete/preview_merge/merge.
Fallback: sql_query.
The runtime dispatcher (run(name, input)) handles all of them. See
Tool use → Typed per-table tools.
db.runTool(input)
Section titled “db.runTool(input)”Execute a { sql, params? } payload returned from a model. Internally
calls db.query.
Errors
Section titled “Errors”import { PerSQLError, RateLimitError } from "@persql/sdk";
try { await db.query("SELECT 1");} catch (e) { if (e instanceof RateLimitError) { await new Promise((r) => setTimeout(r, e.retryAfterSeconds * 1000)); } else if (e instanceof PerSQLError) { console.error(e.status, e.message); }}| Class | Extends | Fields |
|---|---|---|
PerSQLError | Error | status: number, detail?: SqlErrorDetail |
RateLimitError | PerSQLError | retryAfterSeconds: number |
ApprovalRequiredError | PerSQLError | approvalToken, approvalUrl, hits, expiresAt |
PerSQLError.detail carries a structured envelope on /v1/query
and /v1/batch failures — branch on detail.kind (e.g.
"unique_violation") instead of regexing the message. See
Structured SQL errors.
ApprovalRequiredError is thrown when a write hits a
require_approval rule. See Approval rules.
Edge / serverless usage
Section titled “Edge / serverless usage”The SDK is fetch-based, has no Node-only dependencies, and runs on
Cloudflare Workers, Vercel Edge, Deno, Bun, and the browser. In
environments without a global fetch, pass one explicitly:
import { PerSQL } from "@persql/sdk";import nodeFetch from "node-fetch";
const persql = new PerSQL({ token, fetch: nodeFetch as typeof fetch });