nao_config.yaml at the semantic_manifest.json that dbt parse produces, and the agent gets a new tool, execute_semantic_query, that takes metrics and dimensions rather than SQL:
revenue means what metrics.yml says it means, every time, in every chat.
MetricFlow runs inside nao, as a SQL compiler only. There is no call to
dbt Cloud, no Semantic Layer plan to buy, and MetricFlow never opens a
connection to your warehouse: the SQL it produces goes through the same
execute_sql pipeline as any other query, so read-only checks, row limits,
excluded columns and strict mode apply unchanged.Why a semantic layer?
An agent writing SQL from table documentation has to re-derive your business logic every time: which status counts as completed, whether revenue includes refunds, which join gives one row per customer. It gets that right most of the time. A semantic layer makes it right by construction, because the agent no longer writes the aggregation - it names it. That changes three things in practice:- Consistency. Two users asking for “revenue by month” in two chats get the same number, and the same number as your BI tool.
- Governance. Metric definitions stay in dbt, versioned and reviewed like the rest of your project. nao reads them; it does not redefine them.
- Less context to write. Every metric and dimension is documented for the agent by
nao sync, from the manifest. You do not describe them again inRULES.md.
Setup
1
Install the semantic layer extra
MetricFlow is an optional dependency of It is included in
nao-core:nao-core[all] and in the official Docker image. When it is missing, semantic queries fail with a clear message and the rest of nao works as usual.2
Produce the manifest with dbt
In your dbt project,
dbt parse writes target/semantic_manifest.json. That file is the only thing nao needs from dbt. Regenerate it whenever you change a semantic model or a metric.3
Declare it in nao_config.yaml
The compiled SQL is rendered in the dialect of that database. Supported: BigQuery, Snowflake, Databricks, Redshift, Postgres, Trino, Athena, DuckDB and MotherDuck.
4
Sync
nao sync on its own runs it too. The provider reads the manifest, copies it under .meta/semantic_layer/ and writes the agent’s documentation in semantics/.What gets synced
metrics/<metric>.md gives the agent the definition, type, expression, input metrics, the dimensions it can be grouped or filtered by, and two ready-to-use queries. The agent is told to read these before calling the tool, so metric and dimension names come from the manifest rather than from memory.
At runtime nao compiles against the .meta/ copy, so a deployed project does not need the original dbt target/ folder - only the synced context. Do not edit the generated files: they are rewritten on every sync.
Only metrics are queryable. Measures are building blocks; expose one as a
metric explicitly, or with
create_metric: true in the semantic model, to
make it available to the agent.How the agent queries it
execute_semantic_query mirrors the flags of mf query:
One call compiles and executes. The agent receives the rows, not the SQL: it never sees the compiled query and cannot reason about it or rewrite it. The result is a normal query result with its own query id, so everything downstream works unchanged -
display_chart, stories, read_query_result, and joins against it in nao’s local DuckDB.
In the chat, the tool call shows three views: the results, the semantic definition that was sent, and the compiled SQL for anyone who wants to check what actually ran. The SQL is read-only. A semantic result cannot be edited as SQL from the side panel, and execute_sql refuses to update it; to change it, the agent calls execute_semantic_query again with different metrics, dimensions or filters. That is the point: a governed number stays governed.
Choosing how the agent uses it
Once a semantic layer is declared, admins pick a mode under Settings -> Project -> Agent -> Semantic layer.
The mode changes both the tool set and the system prompt: in Semantics only, the warehouse databases are not even listed as SQL targets, and the agent is told that metrics are the only way in.
Working with it
- Context engineering still applies.
RULES.mdis the place to say which metric to prefer when several overlap, and what “customer” means for your business. See the add-semantic-layer skill for a guided setup. - Test it. Metric questions make excellent evaluation cases, because the expected answer is unambiguous by definition.
- When a query fails to compile, MetricFlow’s error (unknown metric, unresolved dimension, bare column in a filter) is returned to the agent, which corrects the call. If a metric you know exists is reported as unknown, the manifest is stale: run
dbt parseandnao sync -p semanticsagain. - Snowflake semantic views are a different mechanism, imported as context by the databases provider. They are documentation for the agent; MetricFlow is an executable layer.
What it looks like
A user asks “how many orders do we have”. Before touching the tool, the agent does what the system prompt tells it to: it searchessemantics/metrics/ for a matching metric, reads orders.md and dimensions.md, and only then calls execute_semantic_query. The Semantic query block in the chat shows the request as it was sent - here the orders metric alone, with the default time bounds - and the icons on its right switch between the results, this definition and the compiled SQL.

orders grouped by order__status, so the count per status still comes from the governed definition. The share of total is not a metric, so it is not the layer’s job: the agent reshapes the semantic result with a short SQL query in nao’s local DuckDB, reading the previous result by its query id (FROM query_35c52ea2). The governed number is never recomputed by hand; the arithmetic on top of it is.

Repositories
Sync the dbt project itself so the agent can read the models behind the
metrics
Evaluation
Turn metric questions into test cases with unambiguous expected answers