Documentation

ask() — natural language to SQL

The Node SDK’s ask() call is the headless path: your API sends a question and tenantId, QueryPanel returns SQL, params, rows, and a chart spec.

qp.ask() is the headless path: your backend sends a natural-language question plus tenantId, and QueryPanel returns SQL, bound params, rows, a chart spec, and a client-safe rationale. SQL runs with your database driver. QueryPanel does not store warehouse passwords.

Quickstart

Attach a database once, sync schema in a setup job (not on the request path), then call ask() from an API route.

attach databases, then ask()
import { QueryPanelSdkAPI } from "@querypanel/node-sdk";
import { Pool } from "pg";

const qp = new QueryPanelSdkAPI(
  process.env.QUERYPANEL_URL!,
  process.env.PRIVATE_KEY!,
  process.env.QUERYPANEL_WORKSPACE_ID!,
);

const pool = new Pool({ connectionString: process.env.POSTGRES_URL });

const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
  const client = await pool.connect();
  try {
    const result = await client.query(sql, params);
    return {
      rows: result.rows,
      fields: result.fields.map((field) => ({ name: field.name })),
    };
  } finally {
    client.release();
  }
};

qp.attachPostgres("pg_demo", createPostgresClient(), {
  database: "pg_demo",
  description: "PostgreSQL demo database",
  tenantFieldName: "tenant_id",
  enforceTenantIsolation: true,
  allowedTables: ["orders"],
});

const response = await qp.ask("Top countries by revenue", {
  tenantId: "tenant_123",
  database: "pg_demo",
});

console.log(response.sql);
console.log(response.params);
console.table(response.rows);

Custom system prompt

Pass systemPrompt on ask() for per-customer policies QueryPanel does not know about — retention windows, excluded statuses, max date range. It does not replace tenant isolation. Max length is 8,000 characters.

systemPrompt from client config
const response = await qp.ask("Revenue by product over all time", {
  tenantId: client.tenantId,
  database: "analytics",
  systemPrompt: [
    `Data retention: only query rows from the last ${client.retentionDays} days.`,
    "Never return a date range longer than that window, even if the user asks for all time.",
  ].join(" "),
});

Follow-up questions

Reuse querypanelSessionIdfrom the previous response so "filter that to Europe" keeps context.

const first = await qp.ask("Revenue by country", {
  tenantId: "tenant_123",
  database: "analytics",
});

const followUp = await qp.ask("Now filter that to Europe", {
  tenantId: "tenant_123",
  database: "analytics",
  querypanelSessionId: first.querypanelSessionId,
});

Related