Getting Started
Install Text2SQL and run your first natural language query
This guide walks you through installing Text2SQL and running your first natural language to SQL query.
Installation
npm install @deepagents/text2sqlDatabase-specific drivers or client libraries are required. Install the ones that match your database:
npm install pg # PostgreSQL
npm install mssql # SQL Server
npm install mysql2 # MySQL / MariaDB
npm install @google-cloud/bigquery # BigQuerySQLite uses the built-in node:sqlite module and requires no additional driver.
Basic Setup
Text2SQL requires four components:
- Adapter - Connects to your database (PostgreSQL, SQLite, SQL Server, MySQL/MariaDB, or BigQuery)
- Grounding - Configures database introspection (see Grounding for advanced options)
- Model - Any AI SDK model compatible with
@deepagents/agent - Version - A string used to manage cache invalidation (bump when schema changes)
Start by creating the shared model:
import { groq } from '@ai-sdk/groq';
const model = groq('openai/gpt-oss-20b');Reuse model in the adapter-specific examples below. The adapter-map key
(main in these examples) becomes the required second argument to
text2sql.toSql(..., 'main') and the <db> selector in
sql validate <db> "..." / sql run <db> "..." when the package CLI is
installed inside the sandbox.
sql validate enforces read-only access: one SELECT/WITH statement is
allowed (including leading whitespace or SQL comments), while writes,
comment-only input, and multi-statement batches are rejected.
Adapter names must match /^[A-Za-z_][A-Za-z0-9_]*$/. If you construct the
adapter map dynamically, validate the keys first with
isValidAdapterName(name) or validateAdapterNames(names).
import { DatabaseSync } from 'node:sqlite';
import { Text2Sql } from '@deepagents/text2sql';
import { Sqlite } from '@deepagents/text2sql/sqlite';
import * as sqlite from '@deepagents/text2sql/sqlite';
// Connect to your database
const db = new DatabaseSync('./your-database.db', { readOnly: true });
const adapter = new Sqlite({
execute: (sql) => db.prepare(sql).all(),
grounding: [sqlite.tables(), sqlite.info()],
});
// Create the Text2SQL instance
const text2sql = new Text2Sql({
version: 'v1', // Bump this when your schema changes
model,
adapters: { main: adapter },
});import pg from 'pg';
import { Text2Sql } from '@deepagents/text2sql';
import { Postgres } from '@deepagents/text2sql/postgres';
import * as postgres from '@deepagents/text2sql/postgres';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
const adapter = new Postgres({
execute: async (sql) => {
const result = await pool.query(sql);
return result.rows;
},
grounding: [postgres.tables(), postgres.info()],
});
const text2sql = new Text2Sql({
version: 'v1', // Bump this when your schema changes
model,
adapters: { main: adapter },
});import sql from 'mssql';
import { Text2Sql } from '@deepagents/text2sql';
import { SqlServer } from '@deepagents/text2sql/sqlserver';
import * as sqlserver from '@deepagents/text2sql/sqlserver';
const pool = await sql.connect({
server: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
options: { encrypt: true, trustServerCertificate: true },
});
const adapter = new SqlServer({
execute: async (query) => {
const result = await pool.request().query(query);
return result.recordset;
},
grounding: [sqlserver.tables(), sqlserver.info()],
});
const text2sql = new Text2Sql({
version: 'v1', // Bump this when your schema changes
model,
adapters: { main: adapter },
});import mysql from 'mysql2/promise';
import { Text2Sql } from '@deepagents/text2sql';
import { Mysql } from '@deepagents/text2sql/mysql';
import * as mysqlGrounding from '@deepagents/text2sql/mysql';
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const adapter = new Mysql({
execute: async (sql) => {
const [rows] = await pool.query(sql);
return rows;
},
grounding: [mysqlGrounding.tables(), mysqlGrounding.info()],
});
const text2sql = new Text2Sql({
version: 'v1', // Bump this when your schema changes
model,
adapters: { main: adapter },
});import { BigQuery as BigQueryClient } from '@google-cloud/bigquery';
import { Text2Sql } from '@deepagents/text2sql';
import { BigQuery } from '@deepagents/text2sql/bigquery';
import * as bigquery from '@deepagents/text2sql/bigquery';
const projectId = process.env.BQ_PROJECT_ID!;
const client = new BigQueryClient({ projectId });
const adapter = new BigQuery({
projectId,
datasets: ['analytics'],
execute: async (sql) => {
const [job] = await client.createQueryJob({ query: sql });
const [rows] = await job.getQueryResults();
return rows;
},
validate: async (sql) => {
await client.createQueryJob({
query: sql,
dryRun: true,
useQueryCache: false,
});
},
grounding: [bigquery.tables(), bigquery.info()],
});
const text2sql = new Text2Sql({
version: 'v1', // Bump this when your schema changes
model,
adapters: { main: adapter },
});Your First Query
Generate SQL Only
Use toSql() to generate SQL without executing it:
const sql = await text2sql.toSql(
'Show me the top 10 customers by total purchases',
'main',
);
console.log(sql);
// SELECT c.CustomerId, c.FirstName, c.LastName, SUM(i.Total) as TotalPurchases
// FROM Customer c
// JOIN Invoice i ON c.CustomerId = i.CustomerId
// GROUP BY c.CustomerId
// ORDER BY TotalPurchases DESC
// LIMIT 10Chat with Streaming
For streaming, build the chat agent yourself with primitives from
@deepagents/context and feed it the Text2SQL index plus instructions:
import {
ContextEngine,
InMemoryContextStore,
agent,
chat,
createBashTool,
createDockerSandbox,
errorRecoveryGuardrail,
npm,
user,
} from '@deepagents/context';
import { createSqlCommandHooks, instructions } from '@deepagents/text2sql';
const store = new InMemoryContextStore();
const context = new ContextEngine({
store,
chatId: 'chat-123',
userId: 'user-456',
});
const backend = await createDockerSandbox({
installers: [npm('@deepagents/text2sql', { ensureRuntime: true })],
volumes: [
{
type: 'bind',
hostPath: process.cwd(),
containerPath: '/workspace',
readOnly: true,
},
],
env: {
TEXT2SQL_ADAPTERS: '/workspace/text2sql-adapters.ts',
},
});
const sandbox = await createBashTool({
sandbox: backend,
...createSqlCommandHooks({ adapters: { main: adapter } }),
});
const indexResult = await sandbox.sandbox.executeCommand('sql index');
if (indexResult.exitCode !== 0) throw new Error(indexResult.stderr);
const manifest = JSON.parse(indexResult.stdout) as { fragmentsPath: string };
const fragments = JSON.parse(
await sandbox.sandbox.readFile(manifest.fragmentsPath),
);
context.set(...instructions(), ...fragments);
const ai = agent({
name: 'sql-assistant',
sandbox,
model,
context,
guardrails: [errorRecoveryGuardrail],
maxGuardrailRetries: 3,
});
await context.continue(user('What are the most popular genres?'));
const stream = await chat(ai);
for await (const chunk of stream) {
// render
}The /workspace/text2sql-adapters.ts module must exist inside the sandbox and
default-export the adapter map the CLI should load. If you are mounting your
app source into the container, point TEXT2SQL_ADAPTERS at that mounted module;
otherwise upload or write the file before calling chat().
toSql() is stateless. It reads cached schema fragments, but it does not write
messages, titles, or usage into your ContextStore. Use the chat() helper
above when you want multi-turn persistence. See
Build Conversations for the full pattern.
sql index options
sql index indexes all configured adapters by default (same behavior as
sql index --all). Pass names to target a subset, for example:
sql index main analytics.
Use --verbose when you want index progress mirrored to stderr while keeping
the JSON manifest on stdout:
const indexResult = await sandbox.sandbox.executeCommand(
'sql index --verbose pretty --out-dir ./sql',
);--verbose prettyprints human-readable progress.--verbose jsonprints one JSON progress event per line.--out-dir <path>writes artifacts under that path (default:$TEXT2SQL_OUT_DIRor./sql).- The manifest always includes
fragmentsPathandeventsPath.
Set TEXT2SQL_INDEX_VERSION to control index-cache invalidation across runs.
Cache keys are index-<version>-<adapter>, so bump the version when your
schema changes.
Set TEXT2SQL_INDEX_EVENTS_PATH to write the NDJSON progress stream to a
fixed path instead of the auto-generated index-<id>.events.ndjson. The
manifest still reports the resolved path as eventsPath. Relative paths
resolve against the current working directory; an empty value falls back to
the default. This is the simplest way to live-tail indexing from another
process:
mkfifo /tmp/sql.events
tail -f /tmp/sql.events & tail_pid=$!
trap "kill $tail_pid 2>/dev/null; rm -f /tmp/sql.events" EXIT
TEXT2SQL_INDEX_EVENTS_PATH=/tmp/sql.events sql index --allWhen the path is a FIFO, opening the writer blocks until a reader attaches.
If no reader is attached at startup, sql index prints
waiting for reader on FIFO <path>... to stderr and then waits — so the
symptom is recognizable rather than a silent hang.
Understanding the Response
When Text2SQL processes a query, it:
- Runs grounding - Introspects your database schema (tables, columns, relationships, indexes, constraints).
- Builds SQL context -
toSql()uses cached schema fragments. Chat flows use fragments fromAdapterIndexerorsql index, then resolve the stored conversation thread from your context store. - Generates SQL - The model maps the request to the available schema.
- Validates - The adapter validates the generated SQL before it is returned or executed.
- Returns results -
toSql()returns the SQL string; thechat()helper streams an end-user answer from indexed schema fragments, Text2SQL instructions, and sandbox query results.
The chat() helper also injects Text2SQL's built-in SQL workflow guidance and
any extra fragments you add to your ContextEngine.
Environment Variables
The examples in this guide use Groq. If you follow them as written, set your API key:
export GROQ_API_KEY=your-api-keyNext Steps
- Grounding - Configure database introspection for richer context
- PostgreSQL Adapter - PostgreSQL-specific features
- BigQuery Adapter - BigQuery-specific features
- SQLite Adapter - SQLite-specific features
- SQL Server Adapter - SQL Server-specific features
- MySQL/MariaDB Adapter - MySQL and MariaDB-specific features
- Generate SQL - Deep dive into SQL generation
- Teach the System - Add domain knowledge