npx skills add ...
npx skills add exploreomni/omni-agent-skills --skill omni-to-databricks-metric-view
npx skills add exploreomni/omni-agent-skills --skill omni-to-databricks-metric-view
Convert an Omni Analytics topic into a Databricks Metric View definition in Unity Catalog. Use this skill whenever someone wants to export Omni metrics to Databricks, create a Metric View from an Omni topic, harden BI metrics into Unity Catalog, or bridge Omni's semantic layer with Databricks AI/BI dashboards and Genie spaces.
Converts an Omni topic into a Databricks Metric View by exploring the Omni model via API, translating its field definitions into the Databricks Metric View embedded YAML format, and executing via the Databricks CLI.
See FIELD-MAPPING.md for full before/after translation examples and YAML-REFERENCE.md for the complete YAML structure, aggregate type, and format mapping tables.
Auth: a profile authenticates with an API key or OAuth. If
whoami(or any call) returns 401, hand off — ask the user to run! omni config login <profile>(OAuth 2.1 browser flow; it blocks ~2 min on the browser). Don't runconfig loginyourself in a headless/CI session (no browser → timeout); on a local interactive machine you may. See theomni-api-conventionsrule for profile setup (omni config init --auth oauth) and discovering request-body shapes with--schema.
Tip: Use
-o jsonto force structured output for programmatic parsing, or-o humanfor readable tables. The default isauto(human in a TTY, JSON when piped).
Ask the user:
orders)catalog.schema) (e.g., main.sales)databricks sql warehouses list to find it)catalog.schema.[topic_name]_mv?⚠️ STOP — Confirm all answers before proceeding. The metric view will be named
[topic_name]_mvby default.
Identify the Shared Model and note its id. Always prefer the Shared Model over Schema or Workbook models.
From the topic file extract: base_view, joins, fields, always_filter, ai_context, sample_queries.
For every view in base_view and joins:
If a view is prefixed with
omni_dbt_, fetch the file starting withomni_dbt_. Skip any view backed byderived_table.sql— it has no physical table.
Map view names to fully-qualified Databricks table references (catalog.schema.table):
| Omni view name | Databricks table |
|---|---|
ecomm__order_items | catalog.ecomm.order_items |
omni_dbt_ecomm__order_items | catalog.ecomm.order_items (strip omni_dbt_) |
The __ separator maps to schema (left) and table (right). Confirm the catalog prefix with the user.
The joins indentation defines the join chain — a view indented beneath another joins into its parent:
Find the dimension with primary_key: true in each view — list it first among that table's dimensions.
✋ STOP — Confirm the full table list and join hierarchy with the user before continuing.
| Syntax | Meaning |
|---|---|
(no fields parameter) | Include all fields from all views |
all_views.* / view.* | Include all fields from all views / named view |
tag:<value> | Include all fields with this tag |
view.field | Include this specific field |
-view.field | Exclude this field (always wins over wildcard inclusions) |
Process inclusions first, then apply exclusions. Also remove any field with hidden: true unless explicitly included by name.
Using the hierarchy from Step 3 and relationships.yaml, extract join columns from on_sql and build the on: clause. Use the view name as the join name.
Star schema (single-level):
Snowflake schema (multi-hop):
⚠️
onis a YAML 1.1 reserved word — always single-quote the key as'on':. Columns from nested (2+ level) joins cannot be used inexpr— flatten them through a denormalized direct join instead.
For each field that survived Step 4, translate it using the rules below. See FIELD-MAPPING.md for full examples.
Dimension quick reference:
| Omni field type | Databricks translation |
|---|---|
| Standard string/number | expr: COLUMN |
type: time (no timeframes) | Single timestamp dimension |
type: time + timeframes | One DATE_TRUNC(...) dimension per timeframe |
groups: | CASE WHEN ... END expression |
bin_boundaries: | CASE WHEN range expression |
duration: | DATEDIFF(unit, start, end) expression |
type: yesno | BOOLEAN dimension (not a filter; omit data_type) |
Measure quick reference:
| Omni measure type | Databricks translation |
|---|---|
aggregate_type: sum/avg/max/min | SUM(col) / AVG(col) / etc. |
aggregate_type: count | COUNT(*) |
aggregate_type: count_distinct | COUNT(DISTINCT col) |
| Derived (refs other measures) | MEASURE(measure_a) op MEASURE(measure_b) — define atomics first |
filters: on a measure | AGG(col) FILTER (WHERE condition) |
Strip Omni's ${view.column} refs to bare column names (or join_name.column for joined fields). Use display_name for the Omni label, comment for description, and carry synonyms directly. See YAML-REFERENCE.md for format and aggregate type mapping tables.
If the topic has ai_context, carry it into the metric view's top-level comment.
✋ STOP — Review all dimensions, measures, and join definitions with the user before generating the final output.
CREATE OR REPLACE VIEW ... WITH METRICSALTER VIEW ... AS $$ ... $$Write the SQL to a temp file:
Execute via the SQL Statements API (databricks sql execute does not exist in CLI v0.295.0+).
Write the request body as a JSON file (with the SQL embedded as a properly-quoted string) and pass it via --json @file. This avoids shell substitution of arbitrary SQL content into the command line:
Check the response for "state": "SUCCEEDED". If "state": "FAILED", read status.error.message and see the Troubleshooting section below.
✋ STOP — Confirm which group or user should receive access before running the GRANT. This is a permission change visible to others.
Grant access:
When the SQL Statements API returns "state": "FAILED", read status.error.message:
| Error message contains | Likely cause | Fix |
|---|---|---|
METRIC_VIEW_INVALID_VIEW_DEFINITION | Invalid YAML field or value | Check the field name against the valid keys (name, expr, display_name, comment, synonyms, format). Common mistakes: using description instead of comment, unsupported decimal_places. |
warehouse not running / RESOURCE_DOES_NOT_EXIST | Warehouse is stopped or wrong ID | Start the warehouse in the Databricks UI or verify the ID with databricks api get /api/2.0/sql/warehouses. |
PERMISSION_DENIED | The CLI profile lacks privileges | Check the profile's permissions on the catalog/schema with databricks api get /api/2.0/unity-catalog/permissions/.... |
TABLE_OR_VIEW_NOT_FOUND | A source or join table doesn't exist in Unity Catalog | Verify each table reference with SHOW TABLES IN <catalog>.<schema>. |
on parse error / unexpected key | on: not quoted | Always write 'on': (single-quoted) — it is a YAML 1.1 reserved word. |
wait_timeout value error | Timeout out of range | wait_timeout must be between 5s and 50s. |
If the error message is truncated, run the same statement with "wait_timeout": "5s" to get the full synchronous error response.
[topic_name]_mv (snake_case, lowercase)CREATE OR REPLACE for new, ALTER VIEW for existingversion: 1.1 (requires Databricks Runtime 17.2+)derived_table.sql have no physical table — skip and warn the usertype: yesno as BOOLEAN dimensions — not filters. data_type is not a valid field — omit itMEASURE() syntax; define atomic measures before composed oneson is a YAML 1.1 reserved word — always write 'on': (single-quoted)MAP type columns — not supportedexpr. Flatten snowflake schema joins through a denormalized direct join-view.field always overrides any wildcard inclusionnumber, currency, date, date_time, percentage, bytetype: date and type: date_time both require date_formatcurrency_code: USD not iso_code: USDdecimal_places unsupported: Omit it entirely — causes a parse errordatabricks api post /api/2.0/sql/statements; wait_timeout must be 5s–50s--filename (not --file-name)comment: not description: — description is not a recognized field and causes a parse error# Databricks CLI — verify installed and list configured profile names
databricks --version
databricks auth profilesomni models list --modelkind SHAREDomni models yaml-get <modelId> --filename <topic_name>.topicomni models yaml-get <modelId> --filename relationshipsomni models yaml-get <modelId> --filename <view_name>.viewjoins:
user_order_facts: {} # skip — derived CTE
ecomm__users: {} # joins to base_view
ecomm__inventory_items: # joins to base_view
ecomm__products: # joins to inventory_itemsjoins:
- name: ecomm__users
source: catalog.ecomm.users
'on': source.user_id = ecomm__users.idjoins:
- name: ecomm__inventory_items
source: catalog.ecomm.inventory_items
'on': source.inventory_item_id = ecomm__inventory_items.id
joins:
- name: ecomm__products
source: catalog.ecomm.products
'on': ecomm__inventory_items.product_id = ecomm__products.iddatabricks api post /api/2.0/sql/statements \
--json "{\"warehouse_id\": \"<WAREHOUSE_ID>\", \"statement\": \"SHOW VIEWS IN <catalog>.<schema> LIKE '%_mv'\", \"wait_timeout\": \"30s\", \"catalog\": \"<CATALOG>\", \"schema\": \"<SCHEMA>\"}"-- CREATE (new view)
CREATE OR REPLACE VIEW catalog.schema.orders_mv
WITH METRICS
LANGUAGE YAML
AS $$
version: 1.1
comment: "..."
source: catalog.ecomm.order_items
joins:
- name: ecomm__users
source: catalog.ecomm.users
'on': source.user_id = ecomm__users.id
dimensions:
- name: id
expr: id
display_name: "Order ID"
- name: status
expr: status
display_name: "Order Status"
measures:
- name: order_count
expr: COUNT(*)
display_name: "Order Count"
- name: total_sale_price
expr: SUM(sale_price)
display_name: "Total Sale Price"
format:
type: currency
currency_code: USD
$$-- ALTER (existing view)
ALTER VIEW catalog.schema.orders_mv AS $$
version: 1.1
...
$$# /tmp/orders_mv.payload.json
{
"warehouse_id": "<WAREHOUSE_ID>",
"statement": "<SQL with newlines as \\n and quotes as \\\">",
"wait_timeout": "50s",
"catalog": "<CATALOG>",
"schema": "<SCHEMA>"
}databricks api post /api/2.0/sql/statements --json @/tmp/orders_mv.payload.jsondatabricks api post /api/2.0/sql/statements \
--json "{\"warehouse_id\": \"<WAREHOUSE_ID>\", \"statement\": \"GRANT SELECT ON VIEW catalog.schema.orders_mv TO \`group_name\`\", \"wait_timeout\": \"30s\", \"catalog\": \"<CATALOG>\", \"schema\": \"<SCHEMA>\"}"