query generation for agents and apps
Don't let the model
write SQL.
It writes a spec instead: measures, dimensions, filters, in the names your business already uses. 0sql resolves the spec against your governed model and returns one correct statement for your warehouse, or a refusal that says exactly why. It never sees a row.
0sql never connects to your warehouse, so your data never leaves your network. The same spec and model always produce the same statement, with row-level security already compiled in. That is the security, determinism and reliability your application inherits the day it stops assembling SQL by hand.
Need the whole stack?Strata is this planner with execution, caching, dashboards, self-service and AI agents already around it. strata.docurl -s https://app.0sql.io/projects/tpcds/branches/main/sql \
-H "Authorization: Bearer zqk_…" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"projections": [
{"field": "Web Net Paid"},
{"field": "Web Sold Date"},
{"field": "Net Paid"}
]
}
}' zsql sql --expr "web net paid, web sold date, net paid" const res = await fetch('https://app.0sql.io/projects/tpcds/branches/main/sql', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
spec: { projections: [{ field: 'Web Net Paid' }, { field: 'Web Sold Date' }, { field: 'Net Paid' }] },
}),
});
const { sql } = await res.json(); sql, adapter postgres, planned in 70 µs
WITH ag29130fae5cf6548d6ddb1bd37298b407 AS (
SELECT
sum(T0."ws_net_paid") AS "msr501e4a8",
T0."ws_sold_date_sk" AS "dimed56b67"
FROM
web_sales T0
GROUP BY
T0."ws_sold_date_sk"
), ag7098b0d0901f2eb16d14f9356f0bb2a0 AS (
SELECT
T0."ss_sold_date_sk" AS "dimed56b67",
sum(T0."ss_net_paid") AS "msr621f67c"
FROM
store_sales T0
GROUP BY
T0."ss_sold_date_sk"
)
SELECT
A0.msr501e4a8 AS "Web Net Paid",
COALESCE(A1.dimed56b67, A0.dimed56b67) AS "Web Sold Date",
A1.msr621f67c AS "Net Paid"
FROM
ag29130fae5cf6548d6ddb1bd37298b407 A0
FULL OUTER JOIN ag7098b0d0901f2eb16d14f9356f0bb2a0 A1
ON A1.dimed56b67 = A0.dimed56b67 Two measures from two fact tables, one conformed date. Each fact is aggregated at its own grain and stitched with a full outer join. Nothing double counts, and nobody modeled that query.
In
Business terms
Ask in the names your business already uses, plus who is asking. As JSON, or as one shorthand line.
- measures
- dimensions
- filters
- cohorts
- [Measure]@m formulas
- security context
Out
One SQL statement
In your warehouse's dialect, ready to run. Or an error that names exactly what could not be resolved.
- joins
- grain
- aggregation
- row-level security
- masks
- dialect
Never
A connection to your data
0sql holds the shape of your warehouse, never its contents. The statement runs inside your network.
- credentials
- connection
- rows
- results
- cache
example · open source
Build a custom agentic dashboard using 0sql only for query generation.
This example shows how to use 0sql, the semantic layer as a service, to build a custom analytics dashboard. The dashboard is a chat. You type a question, an AI agent writes a 0sql query spec, 0sql returns the SQL, and the app runs that SQL on its own warehouse and draws the chart. Say “pin that” and the chart becomes a tile on the dashboard.
The app, the semantic model and the warehouse are all open source. The walkthrough builds it in three steps: deploy the model, give the agent the 0sql tools, wire up the dashboard. Switch the user in the header and the same question returns different SQL, because row-level security lives in the model, not in the prompt.
one turn, under the hood
{
"projections": [
{ "field": "Call Center" },
{ "field": "Average Contact Duration Secs",
"alias": "Average Handle Time Secs", "order_by": "desc" }
]
} SELECT
T1."call_center_desc" AS "Call Center",
sum(T0."contact_duration_secs") * 1.0 / nullif(count(*), 0) AS "Average Handle Time Secs"
FROM
cs_contact_f T0
LEFT JOIN cs_call_center_d T1
ON T0.call_center_id = T1.call_center_id
GROUP BY
T1."call_center_desc"
ORDER BY
sum(T0."contact_duration_secs") * 1.0 / nullif(count(*), 0) desc The join came from the model and the average is the measure's own formula, because an average of an average is a bug and that gets settled once, in the YAML, not in every question.
Model. Deploy. Request. Run.
Four steps, three of them yours. The semantic model is YAML in git; a branch deploys as a branch.
- 01
model
Tables, fields and joins in YAML
Name a measure once. Declare joins with cardinality. The planner derives every reachable combination; you never write a query.
models/sales/tbl.web_sales.ymlname: Web Sales physical_name: web_sales datasource: warehouse cost: 100 fields: - type: measure name: Web Net Paid data_type: decimal expression: sql: sum(ws_net_paid)models/sales/rel.sales.ymldatasource: warehouse web_sales_item: left: Web Sales right: Item sql: left.ws_item_sk = right.i_item_sk cardinality: many_to_one - 02
deploy
One command, per git branch
Secrets are stripped before upload. The service validates the model, forms the universes, runs your tests and answers with counts and warnings.
terminal$ zsql deploy deploy tpcds (6120 bytes) as tpcds/main (the production branch) to https://app.0sql.io deployed tpcds/main: 6 tables, 41 fields, 7 joins, 11 paths, 1 policies PASSED category by net paid tests: 1 passed, 0 failed - 03
request
A spec, or one line of shorthand
Decorators for month grains and period-over-period, filters that land in WHERE or HAVING by themselves.
POST /projects/tpcds/branches/main/sql{ "spec": { "projections": [ {"field": "Date", "decorators": [{"type": "truncate", "grain": "month"}], "order_by": "asc"}, {"field": "Web Net Paid", "decorators": [{"type": "temporalize", "transform": "month_over_month"}]} ], "filters": [{"field": "Web Net Paid", "predicate": "greater_than", "value": "100"}] } }the same, as shorthand:
zsql sql --expr "month(date) asc, mom(web net paid), web net paid > 100" - 04
run
In your code, on your warehouse
Top five categories by revenue: a ranking CTE joined back, no window-function guesswork. Hand it to your driver.
response.sqlWITH ag5c9bb8d550197b3a219ae50f0b427905 AS ( SELECT T1."i_category" AS "dim30d09b7", sum(T0."ws_net_paid") AS "msr0c0d023" FROM web_sales T0 JOIN item T1 ON T0.ws_item_sk = T1.i_item_sk GROUP BY T1."i_category" ORDER BY sum(T0."ws_net_paid") desc LIMIT 5 ) SELECT sum(T0."ws_net_paid") AS "Web Net Paid", T1."i_category" AS "Category" FROM web_sales T0 JOIN item T1 ON T0.ws_item_sk = T1.i_item_sk INNER JOIN ag5c9bb8d550197b3a219ae50f0b427905 AS A2 ON T1."i_category" = A2.dim30d09b7 GROUP BY T1."i_category"
security
Your data never leaves your network.
0sql holds the shape of your warehouse, never its contents. The statement comes back to you and runs inside your own perimeter.
your network
- your application
- your warehouse and its rows
- your credentials
- every result
0sql
- your semantic model
- the planner
- a connection
- a single row
Specs and contexts are planned in memory, never stored or logged. What is kept.
row-level security, compiled into the statement
{
"spec": {"projections": [{"field": "Country"}]},
"context": {
"email": "tank@matrix.com",
"groups": [
{"name": "CC-TMNT", "tags": ["call_center_id:TMNT"]},
{"name": "CC-AVCL", "tags": ["call_center_id:AVCL"]}
]
}
} SELECT
T0."cc_country" AS "Country"
FROM
call_center T0
WHERE
LOWER(T0."cc_call_center_id") IN ('tmnt', 'avcl') The context comes from your auth system with every request. A branch with policies refuses to plan without one.
determinism
Same spec, same SQL. Byte for byte.
On every deploy and every replica. Join routes and merge order are settled by name, never by row id, so planning is not a coin flip.
Pin a statement in a test. A model edit that would change the SQL your application depends on fails the deploy instead of surprising you in production.
The planner is held to byte-for-byte parity with the engine inside Strata, which has run at Netflix scale for years.
name: revenue by category
projections:
- Category
- Net Paid
assert_sql: |
SELECT
T1."i_category" AS "Category",
sum(T0."ss_net_paid") AS "Net Paid"
FROM
store_sales T0
JOIN item T1
ON T0.ss_item_sk = T1.i_item_sk
GROUP BY
T1."i_category" $ zsql deploy
deploy tpcds (6120 bytes) as tpcds/main (the production branch) to https://app.0sql.io
deployed tpcds/main: 6 tables, 41 fields, 7 joins, 11 paths, 1 policies
PASSED category by net paid
tests: 1 passed, 0 failed reliability
Refusals, not wrong numbers.
A measure asked for by a dimension its fact cannot reach is an error with a class and a message, never a fan-out. A hallucinated field is a 422, not a confident wrong answer.
Misspelled fields resolve by similarity when one clearly leads, and the response says so under corrections.
- /explain
- The node tree, join routes and microsecond timings behind a statement.
- /explore
- What a query can still add and stay resolvable.
- /fields
- Fields by name, synonym or similarity, for pickers and agents.
{
"error": {
"class": "Unresolvable",
"message": "Could not find Universe for measure: Web Net Paid, id: 376"
}
} Every error class is documented, so an agent can read the message and fix the spec on its next turn. Error classes.
compared · semantic layer as a service
Everything a semantic layer should do. Nothing it should not.
0sql is a semantic layer as a service: the model, the planner and the API, without the server, the cache or the warehouse connection. Side by side with Cube and MetricFlow, from their own documentation. Every 0sql cell links to the docs page that shows the SQL.
| Querying and modeling | 0sql | Cube | MetricFlow |
|---|---|---|---|
| Automatic data blending across many domains | Measures from many facts in one statement. No fan-out. | Primary keys and views; many-to-many needs an associative cube. | Full outer join across facts. Two hops at most. |
| Standard aggregates | Any SQL aggregate. | A fixed list of aggregation types. | Simple metrics over one column expression. |
| Compound measures across facts | Formulas over measures from different facts, auto-leveled. | Number measures inside one cube or view. | Derived metrics. |
| Snapshot measures | Beginning or ending balance per period. | — | — |
| Exclusion level of detail | Percent of total and fixed denominators, in the model. | Multi-stage measures. | — |
| Inclusion level of detail | Aggregate per entity first, then re-aggregate. | Multi-stage measures. | — |
| Ratios | A compound measure, or a formula in the request. | Number measures. | Ratio metrics. |
| Running and moving windows | A decorator in the request. | Rolling-window measures, modeled. | Cumulative metrics, modeled. |
| Period over period | A decorator in the request. | Time-shift measures, modeled. | offset_window on a derived metric, modeled. |
| Top-n and ranking | A filter in the request. | Rank, as a multi-stage measure. | — |
| Conversion between events | Cohort conversion through segments. No per-event window. | — | Conversion metrics. |
| Formulas in the request | [Measure]@m over any measures. No deploy. | Modeled first. | Modeled first. |
| Cohort beside baseline | A segment on one measure or the query, with exclude mode. | Segments filter the whole query. | Per-metric filter in a derived metric. |
| Partition-aware routing | Routes to the table whose partition holds the data. | Partitions its own pre-aggregations only. | — |
| Routing across compute engines | A hot engine for the partitions it holds, colder ones for the rest. | — | — |
| Aggregate-aware routing | The cheapest table you already own: tiers, costs, aggregates. | Pre-aggregations copied into Cube's own separate store. | — |
| Row-level security and masks | Policies become WHERE or CASE masks. No context, no plan. | Access policies from a JWT context. | Not in the layer. Warehouse grants. |
| Pinned SQL, tested on deploy | assert_sql fails the deploy. | — | — |
| Built for agents | /explore, /fields, typed errors, llms.txt. | Meta API. Chat APIs in Cube Cloud. | dbt MCP server. |
| Architecture | 0sql | Cube | MetricFlow |
|---|---|---|---|
| What comes back | SQL. Nothing executes. | Rows. SQL from /v1/sql, for debugging. | Rows. --compile shows the SQL. |
| Needs your warehouse credentials | No. It never connects. | Yes. | Yes, through dbt. |
| Holds results or a cache | No. Planned in memory, never stored. | Relational cache and pre-aggregations. | Exports materialize saved queries. |
| Warehouses | 13 dialects. | 35+ data sources. | 6 platforms. |
| How you get it | Hosted API. Free today. | Apache 2.0 core, or Cube Cloud. | Apache 2.0 engine. APIs need dbt Cloud. |
Read from each project's public documentation on 6 October 2026. A dash means no documented equivalent was found. Cube and MetricFlow are trademarks of their owners. Spotted a mistake? Tell us and we will fix it.
who it's for
Any caller. One contract.
A spec is JSON. Whatever builds it gets the same joins, the same grain and the same security back, and none of it has to know SQL.
AI agents
The model picks fields, never SQL.
A combination the model cannot have comes back as an error with a reason, not a confident wrong number.
See the chat dashboardEmbedded analytics
A query builder in your product, with no SQL generator to write.
Your UI sends specs, your backend runs the statement, and the context decides which rows each tenant sees.
How the context worksBI and data apps
One definition of revenue, on every surface.
Notebooks, internal tools and new front ends ask by name and get the same statement, in every dialect.
Request cookbookPlatform teams
Model in git. Deploy per branch. Test the SQL in CI.
Hand every team a read-only query key and keep the warehouse credentials to yourself.
Deploy from CIOne model, thirteen dialects
The adapter on the datasource decides quoting, date functions, join support and CTE strategy. Hot and cold tiers, partitions and aggregate tables route each request to the cheapest table that can answer it.
- plan time
- ~70 µs
- per request, release build
- warehouses
- 13
- one model, one request, every dialect
- rows we can see
- 0
- no connection, no credentials, no logs, no cache
- measure kinds
- 5
- standard, compound, snapshot, inclusion, exclusion
Need the whole stack?
0sql is the layer. Strata is the platform.
0sql returns SQL and stops there, because you are building the product around it. If what you want is the product, Strata is the same planner with everything else already attached: query execution across federated engines, aggregate awareness and hot tiers, dashboards that are good by default, self-service for non-technical users, and delivery through exports, subscriptions and Google Sheets.
It is also where the agents already live. Strata ships AI analytics over the same governed model, so a non-technical user or an agent can ask broad, cross-domain questions and get answers that are correct by construction, without you writing the retrieval layer, the chart layer or the guardrails.
The model format is the same one, so this is not a decision you have to get right today. A semantic model you write for 0sql is a semantic model Strata can run.
Stop generating SQL. Start declaring it.
Get a key, install zsql, model three tables, deploy, send your first spec. The quickstart walks through every step with the SQL that comes back.
Early access, everything included. What that means.