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.
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.
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
- Tenant isolation — attach the tenant column before you ask.
- Node SDK reference — schema sync, saved charts, modifyChart, retries.
- React embed — use the headful workspace when you do not want to own the UI.