dbt MetricFlow metrics
Connect Qyra to your dbt MetricFlow semantic layer metrics
Beta Behavior and supported features may change as we iterate on MetricFlow support. What Beta means.
If your dbt project defines metrics using MetricFlow (dbt's semantic layer), Qyra can translate them into Qyra metrics automatically. You keep a single source of truth for metric definitions in MetricFlow format, and Qyra reads them directly from your dbt project — you don't need to duplicate the definitions.
This works with every way of connecting a dbt project:
- Qyra CLI (
qyra deploy/compile/preview) — reads your compiledmanifest.json. No dbt Cloud subscription or dbt Semantic Layer API needed. - Git connections (GitHub, GitLab, Azure DevOps, Bitbucket) — the same translation runs when Qyra compiles your project server-side.
- dbt Cloud connection — Qyra pulls your semantic models and metrics from the dbt Cloud Discovery API when you refresh the project. See Using the dbt Cloud connection for details and limitations.
How it works
When you run qyra deploy (or qyra compile / qyra preview), the CLI reads your project's compiled manifest.json, which contains everything MetricFlow knows about your project: the semantic_models (entities, dimensions, measures) and the metrics defined on top of them. Projects connected via git are compiled the same way server-side when you refresh dbt in Qyra.
These metrics can then be used everywhere Qyra metrics work (charts, dashboards, the API, AI agents).
dbt parse # or dbt compile — writes target/manifest.json
qyra deploy # translates MetricFlow metrics as part of the deployIf you define a metric with the same name in both MetricFlow and your model's meta.metrics, the meta.metrics (Qyra YAML) definition will take precedence — so you can always override a translated metric by hand.
Requirements
- dbt Core 1.6 or later (manifest schema
v10, the first version to include MetricFlow's semantic layer). The legacy metrics format used by pre-1.6 dbt (calculation_method/expression/time_grains) is no longer read.
Use the latest MetricFlow spec
Qyra supports the latest and legacy spec, though it's recommended to use the latest spec.
- Legacy spec (dbt Core 1.6+): top-level
semantic_models:andmetrics:blocks withtype_params. - Latest spec (dbt Fusion engine, dbt platform, dbt Core 1.12+):
semantic_model:enabled inline on the model, with entities/dimensions on columns and metrics using top-levelagg/exprkeys.
Supported features
simple metrics, and measures flagged create_metric: true, translate into Qyra:
| MetricFlow | Qyra metric type |
|---|---|
agg: sum | sum |
agg: count | count |
agg: count_distinct | count_distinct |
agg: average | average |
agg: median | median |
agg: min / agg: max | min / max |
agg: percentile | percentile |
agg: sum_boolean | sum over CASE WHEN bool THEN 1 ELSE 0 END |
Also carried over:
- Measure
expr: bare column references become the metric's SQL, qualified with${TABLE}(for example,expr: amountbecomessql: ${TABLE}.amount). Full SQL expressions are used verbatim. - Labels and descriptions: from the metric, falling back to the measure's.
- Percentile value: MetricFlow's legacy spec authors percentile as a
0–1fraction (percentile: 0.95) and the latest spec authors it as0–100(percentile: 95). Both are normalized to Qyra's0–100scale automatically. config.meta.hiddenandconfig.meta.group_label: read from the metric first, falling back to the measure. Use them to hide translated helper metrics from the explore sidebar or group them alongside your other Qyra metrics. Unknown keys underconfig.metaare ignored.
Filters
Metric-level and measure-level filter: templates translate when every reference is a {{ Dimension('entity__dimension') }} that resolves on the metric's own semantic model. The filter is compiled into the metric SQL:
- name: completed_revenue
type: simple
type_params:
measure: total_revenue
filter: |
{{ Dimension('order__status') }} = 'completed'becomes a Qyra sum metric with SQL CASE WHEN ("orders".status = 'completed') THEN ("orders".amount) END. The dimension's expr is used when it isn't a plain column.
Filters referencing dimensions on other semantic models, or using other template functions (TimeDimension(), Entity(), Metric()), are skipped with a warning.
Ratio metrics
ratio metrics translate to a non-aggregate Qyra number metric dividing the two input metrics, when the numerator and denominator both live on the same model:
- name: revenue_per_order
type: ratio
type_params:
numerator: total_revenue
denominator: order_countbecomes (${total_revenue} * 1.0) / NULLIF(${order_count}, 0) — the * 1.0 avoids integer division on warehouses that truncate, and NULLIF avoids division-by-zero errors.
Filtered inputs work too: a numerator with its own filter: (e.g. completion rate = completed orders ÷ all orders) compiles the filter into a hidden helper metric that the visible ratio references. A ratio-level filter: applies to both inputs.
Derived metrics
derived metrics translate to a number metric with the expression rewritten over the input metrics (aliases supported), again when all inputs live on the same model:
- name: revenue_per_customer
type: derived
type_params:
expr: total_revenue / unique_customers
metrics:
- name: total_revenue
- name: unique_customersbecomes ${total_revenue} / ${unique_customers}. Inputs using offset_window or offset_to_grain (time-shifted metrics) are skipped with a warning.
Why same-model only? MetricFlow computes each input metric in its own aggregation subquery — even across unrelated tables — and joins the already-aggregated results. Qyra compiles one query per explore, so ratio/derived metrics translate faithfully only when all inputs resolve to metrics on the same dbt model. Cross-model inputs are skipped with a warning naming the models involved.
Using the dbt Cloud connection
Projects connected through the dbt Cloud connection type don't have a local manifest.json — instead, Qyra fetches your semantic models and metrics from the dbt Cloud Discovery API alongside your models when you refresh the project. Translation then works exactly as described above, with no extra setup:
- Your service token's existing 'Metadata Only' permission covers the semantic layer queries — no dbt Semantic Layer configuration is required.
- Definitions come from the environment's latest successful run, like the rest of your dbt Cloud metadata: run a job that parses your project (any
dbt build/dbt runjob) and refresh dbt in Qyra.
The Discovery API doesn't expose every field that's in the manifest, so a few things behave differently compared to the CLI or git connections:
- Percentile measures are skipped (with a warning) — the API doesn't return
agg_params.percentile, and translating without the percentile value would silently compute a median. - Measure-level
labelandconfig.meta(e.g.hiddenorgroup_labelset on a measure) aren't available. Set these on the metric instead of the measure — metric-level values come through fine. - Dimension
exprisn't available, so metricfilter:references resolve against the dimension name. If a filtered dimension's name doesn't match a real column on your model, define the filter on the metric in Qyra YAML instead.
What's not supported yet
These are skipped with a warning on deploy (details under --verbose):
| MetricFlow feature | Notes |
|---|---|
cumulative metrics | require time-spine semantics |
conversion metrics | require entity-journey semantics |
Cross-model ratio / derived inputs | inputs must resolve to metrics on the same model (see above) |
offset_window / offset_to_grain inputs | time-shifted inputs have no Qyra metric equivalent |
Cross-model or non-Dimension() filter: templates | only same-model {{ Dimension('entity__dim') }} references translate |
agg: percentile without a numeric percentile value | skipped rather than defaulted — set type_params.measure.agg_params.percentile (legacy) or agg_params.percentile (latest) |
percentile_type: discrete | Qyra percentiles always compile to PERCENTILE_CONT |
join_to_timespine, fill_nulls_with, non_additive_dimension | no equivalents |
And these parts of the semantic model are currently skipped:
- Entities / joins. MetricFlow joins semantic models implicitly at query time through shared entity keys. Qyra joins are explicit and authored per-explore (joining tables).
- Dimensions and
agg_time_dimension. Qyra generates dimensions from your model's real columns, so all of your columns are already available as dimensions, and metrics can be grouped by any of them — the MetricFlow dimension definitions aren't needed. (Dimensionexpris honored when resolving filter references.)
Example
A complete working example (both specs, with a reproducible test) lives in the Qyra repo under examples/metricflow-demo — it exercises every supported shape, including filtered, ratio, derived, and sum_boolean metrics. The short version, in the legacy spec:
semantic_models:
- name: orders
model: ref('orders')
defaults:
agg_time_dimension: ordered_at
entities:
- name: order
type: primary
expr: order_id
dimensions:
- name: ordered_at
type: time
type_params:
time_granularity: day
- name: status
type: categorical
measures:
- name: total_revenue
agg: sum
expr: amount
- name: order_count
agg: count
expr: order_id
# create_metric: true auto-creates a metric — also translated
- name: unique_customers
agg: count_distinct
expr: customer_id
create_metric: true
metrics:
- name: total_revenue
label: Total revenue
type: simple
type_params:
measure: total_revenue
# config.meta carries over to the translated Qyra metric
config:
meta:
group_label: Order metrics
# Same-model filter → CASE WHEN in the metric SQL
- name: completed_revenue
label: Completed revenue
type: simple
type_params:
measure: total_revenue
filter: |
{{ Dimension('order__status') }} = 'completed'
# Same-model ratio → Qyra `number` metric
- name: revenue_per_order
label: Revenue per order
type: ratio
type_params:
numerator: total_revenue
denominator: order_countDeploying this project produces four metrics on the orders explore: total_revenue (sum on ${TABLE}.amount, grouped under "Order metrics"), unique_customers (count_distinct on ${TABLE}.customer_id, from the create_metric: true measure), completed_revenue (a sum over CASE WHEN ${TABLE}.status = 'completed' THEN ${TABLE}.amount END), and revenue_per_order (a number metric of (${total_revenue} * 1.0) / NULLIF(${order_count}, 0)) — all with no Qyra-specific YAML.