Embed AskDB in a Node app
Integrator guide
Drop @askdb/client into a TypeScript service. It reads the same askdb.config.ts the CLI and Studio use, generates validated SQL from a question, and hands it back. Your service runs it through its own connection pool against a read-only role.
Install
Section titled “Install”npm install @askdb/client @askdb/ai-openai @askdb/config pgpnpm add @askdb/client @askdb/ai-openai @askdb/config pgyarn add @askdb/client @askdb/ai-openai @askdb/config pg| Package | Why |
|---|---|
@askdb/client | createAskDb() — resolves the schema, model, and dialect from config. |
@askdb/ai-openai | The adapter your model is built from. Swap -openai for -anthropic, -google, or -azure. |
@askdb/config | Loads .env and askdb.config.ts at startup. |
pg | The Postgres driver. AskDB doesn’t open the connection; your app does. |
Wire it into a handler
Section titled “Wire it into a handler”Call createAskDb once at startup, then askdb.ask(question) per request. Schema, model, and dialect all resolve from askdb.config.ts — the same file askdb init scaffolded.
import { createAskDb } from "@askdb/client";import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";import { openaiProvider } from "@askdb/ai-openai";import { Pool } from "pg";
bootstrapAskDbEnv({ cwd: process.cwd() });
const askdb = createAskDb({ config: getAskDbRuntimeConfig(), providers: [openaiProvider], schema: { path: "./my-app.schema" }, // or set host.schemaPath in config and omit});
const pool = new Pool({ connectionString: process.env.DATABASE_URL, // Run under a read-only role for defense in depth. statement_timeout: 5000, max: 10,});
export async function askQuestion(question: string) { const { sql } = await askdb.ask(question);
// Audit hook — log every question and generated SQL. console.log({ question, sql });
const { rows } = await pool.query(sql); return { sql, rows };}./my-app.schema is a directory, not a file — <name>.schema/ is AskDB’s convention for the introspected-and-enriched artifact. The schema loads once at startup and stays in memory; call askdb.reload() if the artifact changes while the process runs.
That’s the whole pipeline. AskDB returns validated SQL — your handler logs it, runs it, and returns the result.
What the call returns
Section titled “What the call returns”askdb.ask() (and the direct ask() below) return AskPipelineResult. All fields except sql are optional:
| Field | Type | When present |
|---|---|---|
sql | string | Always. The generated, validated SQL string. |
unboundSql | string | undefined | When parameterize is on (default) and the model returned consistent extras. Driver markers. |
params | QueryParameterValue[] | undefined | Positional values for unboundSql. Prefer over tenantParams when present. |
parameters | QueryParameterBinding[] | undefined | Named bindings for form UIs (includes values). |
preparedQuery | PreparedQuery | undefined | Definitions + template for bindPreparedQuery() (no values). |
usage | AskUsage | undefined | When the model provider reports token counts: promptTokens, completionTokens, totalTokens (each number | null). Use it for cost tracking or quota enforcement. |
explain | unknown | undefined | When explain: true is passed. Heuristic guardrail metadata — which read-only rules fired and why. |
tenantGuardrail | TenantGuardrailResult | undefined | When a tenant policy is active. Whether the SQL references tenant-scoped tables correctly. |
tenantParams | unknown[] | undefined | When tenantSqlMode: "sql-params" is set. Tenant-only positional $N values. |
tenantBindings | TenantBinding[] | undefined | When tenant scope bindings were resolved. Structured binding metadata for audit logging. |
import { bindPreparedQuery } from "@askdb/core";
const result = await askdb.ask(question, { tenantScope });
await pool.query(result.sql);await pool.query(result.unboundSql!, result.params);
const rebound = bindPreparedQuery(result.preparedQuery!, { state_name: "Utah", ":tenant_agency_ids": authorizedAgencyIds,});await pool.query(rebound.sql);
if (result.usage) { console.log(`Tokens — prompt: ${result.usage.promptTokens}, completion: ${result.usage.completionTokens}`);}Every ask() is still one model call. Set { parameterize: false } to skip the extra output tokens. bindPreparedQuery does not authorize tenant IDs.
The full result and option types are in the @askdb/core API reference.
Advanced: call ask() directly
Section titled “Advanced: call ask() directly”createAskDb is a convenience layer over @askdb/core’s pure ask() — not a replacement. Drop down to ask() when you construct the model yourself, serve multiple schemas from one process, or want zero dependency on @askdb/config / @askdb/ai.
npm install @askdb/core @ai-sdk/openai pgpnpm add @askdb/core @ai-sdk/openai pgyarn add @askdb/core @ai-sdk/openai pgDialects for all four engines are built into @askdb/core — install @askdb/postgres only if this service also runs askdb introspect programmatically.
import { ask, loadSchema } from "@askdb/core";import { openai } from "@ai-sdk/openai";import { Pool } from "pg";
// loadSchema is synchronous; it autodetects the artifact directory,// a bundled .bundle.json, or a bare schema.json. Load once at startup.const schema = loadSchema("./my-app.schema");const model = openai("gpt-4o-mini");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function askQuestion(question: string) { const { sql } = await ask({ question, schema, dialect: "postgres", model, });
const { rows } = await pool.query(sql); return { sql, rows };}Every option ask() takes — modes, tenant scope, retrieval, sensitive-field handling — is in the @askdb/core API reference. The same options work as per-call overrides on askdb.ask(question, overrides).
Switching engines
Section titled “Switching engines”Same ask() call, different dialect string. Change dialect: "postgres" to "mysql", "sqlite", or "sqlserver". See Switch engines for the full migration matrix.
Complete example: Express server
Section titled “Complete example: Express server”The snippet below is a copy-paste-ready Express server. Schema, model, and dialect are fully config-driven — askdb.config.ts sets host.schemaPath and the AI provider; server.ts only handles HTTP.
The /ask endpoint accepts an optional execute flag: false (default) returns just the SQL; true also runs it through a pg.Pool and returns the rows. This keeps SQL generation and execution decoupled — your service controls both.
import { createAskDb } from "@askdb/client";import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";import { openaiProvider } from "@askdb/ai-openai";import express, { Request, Response } from "express";import pg from "pg";
bootstrapAskDbEnv({ cwd: process.cwd() });
const app = express();app.use(express.json());
const askdb = createAskDb({ config: getAskDbRuntimeConfig(), providers: [openaiProvider], // schema resolved from host.schemaPath in askdb.config.ts});
const pool = process.env.DATABASE_URL ? new pg.Pool({ connectionString: process.env.DATABASE_URL }) : null;
app.post("/ask", async (req: Request, res: Response) => { const { question, execute } = req.body as { question?: string; execute?: boolean };
if (!question || typeof question !== "string" || question.trim() === "") { res.status(400).json({ error: "question is required and must be a non-empty string" }); return; }
const shouldExecute = execute === true;
if (shouldExecute && !pool) { res.status(500).json({ error: "DATABASE_URL is not configured" }); return; }
try { const { sql } = await askdb.ask(question.trim());
console.log({ question, sql, execute: shouldExecute });
if (!shouldExecute) { res.json({ sql }); return; }
const result = await pool!.query(sql); res.json({ sql, rows: result.rows, rowCount: result.rowCount }); } catch (err) { const message = err instanceof Error ? err.message : String(err); res.status(500).json({ error: message }); }});
app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok" });});
app.listen(3000, () => console.log("listening on http://localhost:3000"));import { defineConfig, env, type AskDbConfig } from "@askdb/config";
export default defineConfig({ ai: { provider: "openai", providerConfig: { openai: { apiKey: env("OPENAI_API_KEY") } }, }, introspection: { provider: "postgres", providerConfig: { postgres: { databaseUrl: env("DATABASE_URL") } }, }, host: { schemaPath: env("ASKDB_SCHEMA_PATH"), },} satisfies AskDbConfig);The full working package (with package.json, tsconfig.json, and .env instructions) lives in examples/express-server in the repo.
Read next
Section titled “Read next”© 2026 Yahya Gilany

