Data & analytics
Data Catalog & SQL
Everything tabular: uploaded files, 22 databases and warehouses, 17 app sources, the catalog that describes them, and the workbench that queries them.
Open Data & BI → Data Catalog.
Which kind of source do you want?#
There are four ways to get data in here and they are not interchangeable. Picking the wrong one is recoverable but tedious — a CSV uploaded monthly by hand is a warehouse connection nobody made, and a warehouse connected for a one-off question is a credential you now have to look after.
| Where the data lives | Freshness | Reach for it when | |
|---|---|---|---|
| Upload a file | Copied in, as a local dataset | Frozen at upload — re-upload to update | A one-off analysis, a hand-maintained list, or anything that has no system behind it. Fastest path from nothing to a chart. |
| Connect a database | Stays where it is; queried in place | Live — every query hits the source | The data already lives in a system of record and must not go stale. This is the default for anything a business actually runs on. |
| Connect an app | Pulled into local datasets on a schedule | As fresh as the sync interval | Stripe, Shopify, HubSpot, Salesforce, Jira, Zendesk, Google Sheets — SaaS tools with no query language of their own. |
| Crawl a bucket | Stays in object storage; read per query | Live, per file | Files someone else drops into S3-compatible storage — exports, logs, partitioned Parquet. |
Queried in place versus pulled in is the real distinction
| Also worth knowing | |
|---|---|
| Uploads have a ceiling | 500,000 rows per dataset (UPLOAD_MAX_ROWS). Past that you want a database connection rather than a bigger file. |
| Connections are read-only | The drivers accept a single SELECT-shaped statement and reject writes and DDL before execution. Still connect with a read-only account — the warehouse's own permissions are the real boundary, and this is the belt to that braces. |
| Bucket queries are not pushed down | Files are read up to 50,000 rows each and the query runs here, so a filter does not reduce what is fetched. Fine for exports; not a substitute for a warehouse. |
| You can mix them | The catalog describes all four the same way, and the SQL workbench and agents treat a synced app table exactly like an uploaded one. Start with an upload to prove the question is worth answering, then connect the real source. |
Local tables#
- 1
Upload
Drag in a.csvor.xlsx, or paste rows. The first row is treated as headers. - 2
Check the inferred column types
A column of2024-03-01must be date, not text, or date filters and time-series charts will not work on it later. Fix it now — it is far more annoying after dashboards are built. - 3
Name it for the model
monthly_revenueis chosen correctly by agents far more often thansheet1. Lower case, underscores, no spaces.
Why it works this way
External connections — every field#
Configure these under Integrations → Data Sources → Add connection. Every connector has a Name (your label) and a Test connection button; the last test result and its error are kept on the connection so you can see when it started failing. All secrets are encrypted at rest and never returned to the browser.
Note
PostgreSQL#
| Field | Required | Example / notes |
|---|---|---|
host | Yes | db.example.com |
port | No | Defaults to 5432 |
database | Yes | analytics |
username | Yes | Create a read-only role for this |
password | Yes | — |
ssl | No | Set to require to enable TLS. Needed by most managed hosts (RDS, Cloud SQL, Neon, Supabase). |
MySQL / MariaDB#
| Field | Required | Example / notes |
|---|---|---|
host | Yes | db.example.com |
port | No | Defaults to 3306 |
database | Yes | analytics |
username | Yes | — |
password | Yes | — |
ssl | No | Set to require for TLS. |
Ten more databases use exactly the fields above
- PostgreSQL fields — CockroachDB, TimescaleDB, AlloyDB, Greenplum, YugabyteDB
- MySQL fields — MariaDB, SingleStore, StarRocks, Apache Doris, PlanetScale
Microsoft SQL Server / Azure SQL#
Speaks TDS through the tedious driver, so it needs a Node or Docker deployment — there is no REST SQL API to fall back on.
| Field | Required | Example / notes |
|---|---|---|
host | Yes | sql.example.com or acme.database.windows.net |
port | No | Defaults to 1433. Leave blank if using a named instance. |
database | Yes | analytics |
username | Yes | — |
password | Yes | — |
instance_name | No | A named instance such as SQLEXPRESS. Mutually exclusive with a port — send both and the instance is ignored, which silently connects you to the wrong server. |
trust_server_certificate | No | Set true for an on-prem server with a self-signed certificate. Leave off for Azure SQL, which never needs it. |
ClickHouse#
Uses the HTTP interface, so it works on any deployment.
| Field | Required | Example / notes |
|---|---|---|
url | Yes | Base URL of the HTTP interface, e.g. https://abc.clickhouse.cloud:8443 |
username | Yes | default |
password | Yes | — |
database | No | Scopes table browsing |
Snowflake#
Uses a programmatic access token (PAT), not a password — generate one in Snowflake under your user's settings.
| Field | Required | Example / notes |
|---|---|---|
account | Yes | Account identifier: xy12345.eu-west-1 or myorg-myaccount |
token | Yes | The PAT |
warehouse | Yes | COMPUTE_WH — the compute that runs the query |
database | Yes | ANALYTICS |
schema | No | PUBLIC — scopes table browsing |
role | No | Defaults to the user's default role. Set it explicitly for least privilege. |
Databricks SQL#
The warehouse_id comes from the SQL warehouse's Connection Details tab.
| Field | Required | Example / notes |
|---|---|---|
host | Yes | https://dbc-xxxx.cloud.databricks.com |
warehouse_id | Yes | From Connection Details |
token | Yes | Personal access token |
catalog | No | Unity Catalog catalog name |
schema | No | Default schema |
Google BigQuery#
| Field | Required | Example / notes |
|---|---|---|
project_id | Yes | my-gcp-project |
service_account_json | Yes | The FULL service-account key JSON, pasted as a string. Grant it BigQuery Data Viewer + Job User. |
location | No | US, EU or a region like us-central1. Used for jobs and region-wide table listing. |
dataset | No | Restricts browsing to one dataset |
Amazon Redshift#
Two shapes: Serverless (set workgroup_name) or provisioned (set cluster_identifier and db_user). Fill one pair, not both.
| Field | Required | Example / notes |
|---|---|---|
region | Yes | us-east-1 |
access_key_id | Yes | — |
secret_access_key | Yes | — |
database | Yes | dev |
workgroup_name | Serverless | Serverless workgroup name |
cluster_identifier | Provisioned | Cluster id |
db_user | Provisioned | Database user for the cluster |
Azure Synapse (dedicated SQL pool)#
| Field | Required | Example / notes |
|---|---|---|
server | Yes | myworkspace.sql.azuresynapse.net |
database | Yes | Pool name |
username | Yes | — |
password | Yes | — |
Trino / Starburst / Presto#
| Field | Required | Example / notes |
|---|---|---|
host | Yes | trino.example.com — hostname only, no scheme |
port | No | Defaults to 443 with TLS, 8080 plain |
username | Yes | Required by the protocol (sent as X-Trino-User) |
password | No | Basic auth; optional on anonymous coordinators |
access_token | No | JWT/OAuth2 bearer — takes precedence over password when set |
catalog | No | iceberg, hive, delta … |
schema | No | Default schema |
ssl | No | Set to disable for plain HTTP; anything else is HTTPS (the default). |
Amazon Athena#
| Field | Required | Example / notes |
|---|---|---|
region | Yes | us-east-1 |
access_key_id | Yes | — |
secret_access_key | Yes | — |
session_token | No | For temporary STS credentials |
database | No | Glue database queried by default; also scopes browsing |
catalog | No | Defaults to AwsDataCatalog |
workgroup | No | Defaults to primary |
output_location | Usually | s3://bucket/prefix/ for query results. Required unless the workgroup already sets one — the most common cause of a failing Athena connection. |
Oracle Database / Autonomous DB#
Connects over ORDS — plain HTTPS, so no wallet and no Instant Client. Autonomous Database ships ORDS enabled.
| Field | Required | Example / notes |
|---|---|---|
ords_url | Yes | Base URL from Database Actions, e.g. https://<id>-<db>.adb.<region>.oraclecloudapps.com/ords |
username | Yes | DB user, HTTP Basic against the REST-enabled schema |
password | Yes | — |
schema | No | URL schema-alias segment from ORDS.ENABLE_SCHEMA. Defaults to the lower-cased username, which is ORDS's own default. |
Object stores and external table catalogs#
Three different things are called a catalog
LAKEHOUSE_CATALOG_URL) is the Postgres the built-in lakehouse keeps its own table manifests and snapshots in; it is machinery, not an inventory, and you never browse it. The first describes data for people; the third is what makes the Parquet in your bucket queryable at all.S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2, GCS over its S3 API, any other S3-compatible endpoint, and Azure Blob Storage / ADLS Gen2 (account key or SAS token) are added as catalog sources through Data Catalog → Add source. A crawl lists the bucket, groups a folder of same-format files into one dataset, and records each file’s columns. Iceberg REST and Unity Catalog are connected for metadata only.
Parquet, CSV, JSON, NDJSON and ORC files are queryable. Press Query data on a file in the catalog and it opens in the Workbench with the bucket selected as the engine. The SQL name is the file’s basename without its extension, so data/orders.parquet is orders; a partitioned folder sales/*.parquet is sales. Files in the same bucket can be joined, including across formats — a Parquet fact table against a CSV lookup is an ordinary query.
| Format | Schema | Query | Notes |
|---|---|---|---|
| Parquet | Yes | Yes | Read in place; schema and row count from the footer |
| CSV / TSV | Yes | Yes | Schema and profile stats from a head-of-file sample |
| JSON / NDJSON | Yes | Yes | — |
| ORC | Yes | Yes | Downloaded whole and read in a separate process — see below. Capped by ORC_MAX_DOWNLOAD_BYTES (256 MB). |
| Avro | No | No | Cataloged with its name and size; there is no reader for it |
ORC is read differently, and sometimes cannot be read at all
read_orc cannot open s3:// — on the same connection, read_parquet('s3://…') works and read_orc('s3://…') reports “no files found” — so the object is downloaded whole before it is read, which is what the size cap bounds. Second, the ORC extension can crash the process on files with nested STRUCT/LIST/MAP columns, including conformance files published by the Apache ORC project. It therefore always runs in a child process: the read fails with a message saying the reader crashed, and your server keeps serving. Flat ORC files read normally, and schemas are read for all of them — a nested file is still cataloged with its columns even though it cannot be queried.Why Avro is listed but cannot be opened
.avro files, the catalog lists them with their name and size and says why they have no columns. If a build appears, enabling it is a one-line change.Where a Parquet schema comes from
Bucket queries read up to 50,000 rows per file
Why bucket SQL does not run against the bucket
s3:// needs network access, and DuckDB offers no setting that grants that while denying the local filesystem — so an engine able to read your bucket could also read the server’s own files. Your SQL therefore runs in the sandboxed engine over rows fetched for it, and only queries the platform composes itself ever reach the networked one. That is also why a query naming a file the catalog has not crawled is refused by name rather than attempted.Create a read-only user
SELECT, so a read-only role loses you nothing and bounds the blast radius of a leaked secret.Direct query vs import#
| Mode | Freshness | Cost | Use for |
|---|---|---|---|
| Direct query | Always current | A round trip to the source per query | Operational checks; data that changes minute to minute |
| Import (snapshot) | As of the last refresh | Cheap and fast to re-query | Dashboards many people open; anything charted repeatedly |
Sharing a connection with your team#
A connection belongs to whoever created it. Rather than every analyst creating their own — several copies of one credential, each rotated separately, each a place it can leak — a superadmin can share it under Admin → IAM → Access, to a user or a group, as either a Database / warehouse connection or an App source.
A shared connection runs as its owner. The credential is the connection — a grantee has none of their own — so the owner's credential is decrypted server-side and the query runs against the owner's warehouse. A grantee can query it, test it and see its health; they cannot see the credential, edit it or delete it. {{secret:NAME}} references resolve as the owner, never against the grantee's own vault.
Revocation takes effect on the next use
A shared app source syncs as its owner, into the owner's datasets — so a teammate who notices stale data can re-run it and refresh the real datasets rather than building a parallel copy under their own account. The audit entry records both who triggered it and whose data moved. Sharing the source lets someone keep it healthy; to let them read the resulting data, share those datasets too.
Apps — SaaS sources#
Databases are queried in place. Apps have no query language, so they are pulled into datasets instead: Integrations → Apps → connect, discover what is in there, choose what to sync. Each stream becomes its own dataset and is then indistinguishable from an uploaded CSV — same type inference, same version history, same use in BI, prep flows and the semantic layer.
| App | Auth | Streams |
|---|---|---|
| Google Sheets | Service-account JSON | One dataset per worksheet. Share the sheet with the key's client_email, or Google returns 403 however valid the key is. |
| Stripe | Secret or restricted key | Charges, customers, invoices, subscriptions, payment intents, products, prices, refunds, payouts, balance transactions |
| Shopify | Admin API access token | Orders, customers, products, draft orders, price rules |
| HubSpot | Private app token | Contacts, companies, deals, tickets, line items, products |
| Salesforce | Connected app (client credentials) | Accounts, contacts, leads, opportunities, cases, campaigns, users |
Auth is a pasted credential, never OAuth: a redirect flow needs a public callback URL that a self-hosted deployment behind a firewall may not have, so each connector uses the vendor's server-to-server credential instead.
Syncs run on demand or hourly / daily / weekly, and you are notified if one fails or comes back partial.
Following a source instead of re-reading it#
A sync does one of two things, and the Streams button on a connection says which for every stream it syncs.
Full refresh re-reads the source and replaces the dataset — the right semantic where rows are edited and deleted in place with nothing to filter on, because an append would resurrect deleted rows for ever. The previous contents are snapshotted as a restorable version first, so a sync that pulls a truncated source is recoverable.
Incremental asks the API for records changed since the last sync and folds them into the dataset by key. Salesforce, Shopify, HubSpot, Jira, ServiceNow and GitHub follow every stream they offer, as do Linear and Asana; Stripe follows its six immutable object types; Zendesk follows tickets and users; Intercom follows contacts and conversations; Freshdesk follows tickets and contacts; Klaviyo and Notion follow everything they offer, and GA4 follows every report on its date. Everything else is a full refresh — Zendesk has no incremental export for organizations, Intercom none for admins, Freshdesk none for companies or agents, and neither a Google Sheets worksheet nor an Airtable table has a timestamp to follow at all.
GA4 is measured, not fetched
Each API is asked in its own dialect
Why some Stripe objects are deliberately not followed
customers, subscriptions, products and prices are edited in place while their created never moves, so following it would miss every edit. They are few enough that re-reading costs little, and correctness is worth more than the saving. Salesforce follows SystemModstamp rather than LastModifiedDate for the same reason: LastModifiedDate reflects user edits only, while SystemModstamp also moves on a merge, a cascade or a bulk update.The first pass has no high-water mark, so it reads everything and replaces: merging a full read into a stale dataset would leave rows the source has since deleted, for ever. The mark is written only after the rows are committed — advanced first and then lost to a failed sync, the next run would skip that whole window and nothing would say so.
Starting a stream over#
Streams → Start over forgets the high-water mark, so the next sync reads that stream in full. It is the escape hatch for what a cursor cannot see: records the API changed without moving their cursor field, or a backfill predating the connection.
Starting over is the owner's to decide
Staying connected#
Three things run underneath every connection without being asked for. All are tunable by whoever runs the instance — see self-hosting.
| What happens | What you see | |
|---|---|---|
| Health checks | Every connection is re-tested on a schedule with the same probe the Test button uses. A warehouse password that expires on your company’s rotation policy is found by us, not by a dashboard erroring in front of a customer. | A Failing badge in Integrations, one notification when it breaks and one when it recovers — not one per check. |
| Credential age | How long ago the stored secret was entered. Re-saving a connection resets it; a health check does not. | An “N d old” badge once it passes the policy age (90 days by default). Advisory — nothing expires or stops working. |
| Retries | A rate limit or a brief outage from a provider is retried with backoff rather than failed. Retries are always reads, so nothing can be double-written. | Nothing — that is the point. A tile that would have errored simply loads. |
Behind a corporate proxy?
HTTPS_PROXY and NO_PROXY and every connector follows them. Without it, reaching Snowflake or Stripe fails as a connection timeout rather than anything that names the real cause — so it is worth checking first if a connector that should work does not.The catalog#
| Feature | What it gives you |
|---|---|
| Column profiles | Row counts, null rates, distinct values, min/max per column — the fastest way to spot a column that is 90% empty before charting it. |
| AI descriptions | Generated plain-English descriptions for tables and columns. Agents read these too, so a described catalog measurably improves tool choice. |
| Lineage | What a dataset came from and what depends on it — prep flows, dashboards, metrics — down to the column: which source columns fed each column of a pipeline's target or a SQL model, with steps the tracer cannot read marked. Check before changing or deleting anything. |
| Business glossary | Define terms once ("active customer") and attach them to columns so the definition travels with the data. |
| Column tags | Tag a column pii or a table restricted in the asset drawer, and a lakehouse tag policy masks or filters it everywhere the tag appears — one rule, not one per table. |
| Change detection | Scheduled crawls report new, changed and removed columns, so an altered upstream schema surfaces as a change rather than a broken dashboard. |
| Owner & status | Who owns it and when it was last crawled. |
Query data on any dataset opens it in the workbench with the table loaded.
SQL workbench#
Write SQL against anything connected. Results can be charted, added to a dashboard, or exported to CSV/Excel.
-- Monthly revenue and order count for the last 12 months
SELECT date_trunc('month', o.created_at) AS month,
COUNT(*) AS orders,
SUM(o.amount) AS revenue
FROM orders o
WHERE o.status = 'settled'
AND o.created_at >= current_date - INTERVAL '12 months'
GROUP BY 1
ORDER BY 1;- Ask in English — describe the question and the assistant writes the SQL. Read it before running: it shows you exactly which join and filter the answer depends on.
- Results are capped for display, with the true match count shown, so a runaway query cannot hang the page.
Which engine runs your SQL#
Queries against a connected warehouse run on that warehouse. Queries against uploaded tables and prepared datasets run on DuckDB — in your browser for the workbench and Ask AI, and on the server for scheduled refreshes, prep flows and agent tools.
Why it works this way
The first query in a session is slower#
The engine is WebAssembly — roughly 8 MB, fetched once and then cached by your browser for every later visit. You will see “Starting the SQL engine…” with a progress bar while that happens.
- It starts as soon as you open a data page, not when you press Run, so it usually finishes while you are still choosing a table.
- It downloads once per browser, not per query, per dataset or per tab.
- It is served from your own deployment, never a CDN — so an air-gapped install works, and no third party sees that you loaded it.
If it never finishes
Giving an agent access#
- 1
Open the agent → Tools
Enablesql_query. Also enablecalculator— see the warning below. - 2
Set Allowed tables
Only tables in this list are visible to the agent. Leaving it empty means sql_query has nothing to query. - 3
Mention the data in the system prompt
e.g. "Use sql_query for anything involving counts, totals or dates. Never estimate a number you could compute." - 4
Test with a counting question
Ask for a total. Check the sources under the answer name the table — if they name a document instead, it answered from prose.
At run time the agent sees each table's name, columns and a small sample of rows, writes a SELECT, and gets real rows back.
Read-only, and scoped to you
SELECT is accepted — writes and DDL are rejected before execution. Queries run under your identity and row-level security, so an agent cannot read a table you cannot. When an agent runs for an anonymous embed visitor it is explicitly scoped to the owner's data.Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| "Table does not exist" | Not in Allowed tables, or the agent guessed a name | Add it to Allowed tables; rename cryptic tables; add AI descriptions. |
| It queried the wrong table | Two similar names, no descriptions | Generate AI descriptions — they are what the model disambiguates on. |
| Date filters do nothing | Column imported as text | Change the column type on the table to date. |
| Athena connection fails | No query result location | Set output_location to an S3 prefix, or configure one on the workgroup. |
| Managed Postgres refuses to connect | TLS not enabled | Set ssl to require. |
| Snowflake auth fails | Using a password | This connector expects a programmatic access token (PAT), not your account password. |
| Connector test fails on a private host | Outbound guard | Requests to private and link-local addresses are refused. The database must be reachable from wherever the app runs — see Install & deploy. |
| Agent totals are wrong | It did arithmetic itself | Enable the calculator tool and set temperature to 0. |
Use cases#
The production warehouse, handed to the team#
- 1
Integrations → Data Sources → new connection
Name it, pick the provider, enter a read-only login, test. The credential is encrypted at rest underPROVIDER_CREDS_SECRETand never shown again. - 2
Admin → IAM → Access → share it with a group
The connection runs as its owner; grantees query without ever holding the credential. Rows returned to an agent are capped, so a runaway SELECT * cannot flood a context window.
Catalog an Azure container#
- 1
Data catalog → add a source → Azure Blob Storage / ADLS Gen2
Container, storage account name, and an account key or SAS token. - 2
Read in place, or mount
Files are read with DuckDB overaz://; mount the container into the lakehouse as a read-only source when agents should query it with SQL.
Ask an agent about Jira, or Zendesk#
- 1
Integrations → Apps → Jira
Site URL, account email, API token, optional project keys. Each project becomes a stream (issues:KEY) that syncs into a local table on a schedule. Zendesk works the same way and exposes tickets, users and organizations. - 2
Give an agent the synced tables as a source
Which open bugs in PROJ are older than thirty days? The answer comes from your copy of the data, on your schedule, with the same provenance as any other read.