Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

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/text2sql

Database-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 # BigQuery

SQLite uses the built-in node:sqlite module and requires no additional driver.

Basic Setup

Text2SQL requires four components:

  1. Adapter - Connects to your database (PostgreSQL, SQLite, SQL Server, MySQL/MariaDB, or BigQuery)
  2. Grounding - Configures database introspection (see Grounding for advanced options)
  3. Model - Any AI SDK model compatible with @deepagents/agent
  4. 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 10

Chat 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 pretty prints human-readable progress.
  • --verbose json prints one JSON progress event per line.
  • --out-dir <path> writes artifacts under that path (default: $TEXT2SQL_OUT_DIR or ./sql).
  • The manifest always includes fragmentsPath and eventsPath.

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 --all

When 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:

  1. Runs grounding - Introspects your database schema (tables, columns, relationships, indexes, constraints).
  2. Builds SQL context - toSql() uses cached schema fragments. Chat flows use fragments from AdapterIndexer or sql index, then resolve the stored conversation thread from your context store.
  3. Generates SQL - The model maps the request to the available schema.
  4. Validates - The adapter validates the generated SQL before it is returned or executed.
  5. Returns results - toSql() returns the SQL string; the chat() 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-key

Next Steps