Skip to content
Superset + AI · Technical

Adding natural-language questions to Apache Superset

An architecture walkthrough for a text-to-SQL layer on Superset: schema context, prompt design, SQL validation, execution through Superset and choosing the chart.

For the complete AI-readable content index, see /llms.txt.

DashboardGPT TeamUpdated 5 min read
On this page
  1. The architecture
  2. Stage 1: Build the schema context
  3. Stage 2: Write the prompt
  4. Stage 3: Validate the SQL
  5. Stage 4: Execute through Superset
  6. Stage 5: Choose the chart and explain it
  7. Testing the layer
  8. Or use a layer that already exists
  9. FAQ

Key takeaways

  • A natural-language layer on Superset has five parts: schema context, prompt, validation, execution and chart selection.
  • Schema context matters more than the model. Describe columns, grains and business definitions explicitly.
  • Validate every generated query before it runs: read-only, known tables only, a row limit and a timeout.
  • Run queries through Superset so existing database connections and permissions still apply.

The architecture

A natural-language layer on Apache Superset sits between the user and Superset's query engine. The request flows through five stages:

  1. Schema context: collect a compact description of the tables and columns the user may query.
  2. Prompt: send the question plus that context to a language model and ask for SQL.
  3. Validation: parse the SQL and reject anything unsafe or invalid before it runs.
  4. Execution: run the query through Superset against the database.
  5. Presentation: choose a chart for the result and write a short explanation.

Each stage can fail on its own, so build and test them separately. The code below is illustrative Python; adapt it to your stack and your Superset version.

Stage 1: Build the schema context

The model only knows what you tell it. Raw information_schema output is too noisy and too thin at the same time: it lists every column but explains none.

Build a compact profile per table instead:

table: subscriptions
grain: one row per subscription change
columns:
  - name: customer_id      role: id        note: join key to customers.id
  - name: plan             role: category  values: [starter, team, enterprise]
  - name: mrr_usd          role: measure   unit: USD per month
  - name: started_at       role: time      primary: true  tz: UTC
  - name: canceled_at      role: time      note: null if active
definitions:
  active_subscription: canceled_at IS NULL
  churned_in_month: date_trunc('month', canceled_at) = :month

Three parts of this matter most:

  • Grain: what one row represents. Most wrong totals come from getting this wrong.
  • Roles: which columns are measures, categories, times and IDs.
  • Definitions: the business terms people actually use, written once as SQL.

In DashboardGPT this profile is generated automatically when a file is uploaded or a source is connected, then refined as columns are confirmed.

Stage 2: Write the prompt

Keep the prompt structured and strict. A good pattern:

You write SQL for {dialect}. Use only the tables and columns below.
Return one SELECT statement and nothing else.

Schema:
{schema_profile}

Rules:
- Use the primary time column for "last N days/months" unless told otherwise.
- Aggregate to the grain the user asks for before averaging.
- Use the definitions section for business terms.
- If the question is ambiguous, return: AMBIGUOUS: <one short question>.
- Always include LIMIT {row_limit}.

Question: {question}

The AMBIGUOUS escape hatch matters. A model that asks "Gross or net revenue?" is far more useful than one that guesses.

For follow-up questions, include the previous question and its SQL in the context, so "now only enterprise" modifies the last query instead of starting over.

Stage 3: Validate the SQL

Never run generated SQL directly. Parse it and check it first. With the sqlglot library:

import sqlglot
from sqlglot import exp

ALLOWED_TABLES = {"subscriptions", "customers", "invoices"}

def validate(sql: str, dialect: str, row_limit: int = 5000) -> str:
    statements = sqlglot.parse(sql, read=dialect)
    if len(statements) != 1:
        raise ValueError("Exactly one statement is allowed")
    tree = statements[0]
    if not isinstance(tree, exp.Select):
        raise ValueError("Only SELECT queries are allowed")
    tables = {t.name for t in tree.find_all(exp.Table)}
    unknown = tables - ALLOWED_TABLES
    if unknown:
        raise ValueError(f"Unknown tables: {sorted(unknown)}")
    if not tree.args.get("limit"):
        tree = tree.limit(row_limit)
    return tree.sql(dialect=dialect)

Add checks for your environment: known columns only, no cross-database references, and a cap on the date range for very large tables. Also connect with a read-only database user, so a validation gap can never become a write.

Stage 4: Execute through Superset

Running queries through Superset, rather than directly against the database, means you reuse the database connections, drivers and access rules your team already maintains.

Superset exposes a REST API. A typical flow authenticates, then submits SQL for a database:

import requests

BASE = "https://superset.example.com"

def superset_session(username: str, password: str) -> requests.Session:
    s = requests.Session()
    r = s.post(f"{BASE}/api/v1/security/login",
               json={"username": username, "password": password,
                     "provider": "db", "refresh": True})
    r.raise_for_status()
    s.headers["Authorization"] = f"Bearer {r.json()['access_token']}"
    csrf = s.get(f"{BASE}/api/v1/security/csrf_token/").json()["result"]
    s.headers["X-CSRFToken"] = csrf
    return s

def run_sql(s: requests.Session, database_id: int, sql: str) -> dict:
    r = s.post(f"{BASE}/api/v1/sqllab/execute/",
               json={"database_id": database_id, "sql": sql,
                     "runAsync": False, "queryLimit": 5000})
    r.raise_for_status()
    return r.json()

Endpoints and payloads vary between Superset versions, so check the API reference for yours. Use a dedicated service account with access only to the databases and schemas the natural-language layer should reach, and set query timeouts on the database connection.

Stage 5: Choose the chart and explain it

Pick the visual from the shape of the result, not from the question text:

Result shapeChart
One row, one numberMetric card
A time column plus one measureLine chart
A time column plus a category plus a measureMulti-line or stacked bars
A category plus a measure, up to about 15 rowsBar chart, sorted
A category plus a measure, parts of a whole, up to 5 rowsShare chart
Many columns or many rowsTable

Then write a short explanation from the result itself: the largest value, the biggest change and anything unusual. Pass the aggregated rows to the model, not raw data, and ask it to cite the figures it uses. Show the SQL next to the chart so anyone can check it.

Testing the layer

Build an evaluation set before you ship:

  1. Collect 50 to 100 real questions from the people who will use it.
  2. For each, write the correct SQL or the correct answer.
  3. Run the set after every change to the prompt, the schema profile or the model.
  4. Track three rates: correct, ambiguous (the model asked a question) and wrong.

A falling "wrong" rate matters more than a rising "correct" rate. An answer that asks for clarification is safe; a confident wrong number is not.

Or use a layer that already exists

Building this well takes profiling, prompts, validation, execution, chart selection, a UI for follow-ups and continuous evaluation. DashboardGPT provides these on top of Apache Superset, with an infinite canvas for follow-ups and bring-your-own-key for the model.

Frequently asked questions

Superset executes SQL and renders charts. Natural-language questions come from a layer on top that turns a question into validated SQL and runs it through Superset.

Most current large models write good SQL when the schema context is clear. Test two or three on your own evaluation set; schema quality usually matters more than the model choice.

Allow only single SELECT statements, check tables and columns against an allowlist, add a row limit, set a timeout and use a read-only database user.

Through Superset if you already use it, so the same connections, drivers and access rules apply and results can be rendered as Superset charts.

Include the previous question and its SQL in the prompt, so the model modifies the last query instead of writing a new one from scratch.

Ask your data a question.

Open the playground and ask about revenue, churn or campaign spend. Charts, metrics and written insights appear on an infinite canvas.

  • No account, no install
  • Free plan with CSV and Excel uploads
  • Bring your own AI key