Migrate from Omni
Convert your Omni shared model, topics, and views into governed Malloy data models
Omni is a BI app, and its model belongs to it: .view and .topic files in Omni's own YAML, spanning a shared model every workbook inherits, per-workbook extensions, and branches in between. Credible reads across all of them, reconciles the definitions into one canonical set, and rebuilds your analytical domain as governed Malloy in your own repository — where the model is the input to an engine rather than configuration inside a product.
Still weighing the move? Credible vs. Omni sets the two side by side, and The Omni Migration Guide walks the method end to end, including the layer reconciliation.
What Credible Reads
Your Omni model — .view files (dimensions and measures over tables), .topic files (views gathered into queryable units, with AI context and default filters), and the relationships file, where the global joins live. All YAML. The agent works from those files directly, inventorying workbook-level extensions too — logic often lives there, not just in the shared model. Connecting Omni's MCP server or Model API is optional and adds live validation (and can round-trip the model files).
What Comes Across
The everyday modeling carries over. The bigger pieces land like this:
| In Omni | In Credible |
|---|---|
| Shared model, topics, views | Malloy sources (base + joined); import/export curate exposure |
relationships file — join_from_view, join_to_view, on_sql | join_one / join_many on the source, with the on_sql condition as the join expression |
relationship_type: many_to_one / one_to_many | join_one / join_many — the cardinality is the keyword |
relationship_type: assumed_many_to_one | Flagged, not converted: an inferred join nobody declared is a decision, not a fact |
reversible: true | Two named joins, one per direction, rather than one join used both ways |
join_from_view_as aliases | Separate named joins on the same source |
primary_key: true dimension | primary_key: on the source — what keeps symmetric aggregates correct on both sides |
| Dimensions & measures | Carried over, filtered and ratio measures included |
Field-level sql: with ${…} refs | Resolved into Malloy expressions with explicit types |
| Logic split across shared / branch / workbook layers | Reconciled into one canonical definition per field |
access_filters on a topic | An #(access_filter) rule reading the same column — the rows a caller may see |
required_access_grants | An #(authorize) lock, gating the whole source: every row, or none |
hidden_unless_access_grants | Flagged, not converted — hiding a field per caller is a different layer from filtering rows, and the difference is easy to lose |
ai_context and descriptions | #(doc) / #(index), compressed into the engine's concept index |
| Workbooks & dashboards | Rebuilt as data apps or notebooks |
The Migration Flow
Credible reads the shared model, topics, and workbook layers, translates views to sources and topics to joined sources, enriches with #(doc)/#(index) tags (seeded from ai_context), and — where you connect it — validates against Omni's query engine.
What Credible Handles
- Logic split across layers — the "real" definition of a field may live in an un-promoted workbook model, not the shared model. Credible reconciles the shared, branch, and workbook layers into one canonical Malloy definition, resolving promotion lineage as it goes.
- Field-level inline SQL — Omni encourages
sql:with${…}references and implicit typing. Credible resolves the references and makes Malloy types explicit. - Removable default filters —
default_filterson a topic is a starting point a user can clear, so it belongs in the query rather than the model: it becomes part of a named view, not a source-level filter. - Filters that cannot be removed —
always_where_sql,always_where_filters,always_having_sqlandalways_having_filtersare different in kind. A user cannot clear them, and the file does not say whether one is a data-quality rule, a business convention or a security restriction somebody depends on. Each is written down and classified by a person: a rule that restricts becomes an#(authorize)lock or#(access_filter)row filter, whichever the restriction shape calls for, a convention becomes part of a named view, and a data-quality filter usually belongs upstream. None is migrated on sight — reading one as a filter can silently drop a restriction, and dropping one moves every number the topic produces. - Assumed joins — Omni infers relationships it was never told about and marks them
assumed_many_to_one. An inferred join is a guess that has happened to work, which is not the same as a fact, so each one is flagged for a person to confirm. - Reversible joins —
reversible: truelets one relationship be explored in both directions. Malloy makes direction explicit, so a reversible relationship becomes two named joins, with the fan-out direction named. - Topic-scoped overrides — a topic's
views:block customises a view inside that topic only, andextendslets topics inherit from one another. A field can therefore differ by layer and by topic, so reconciliation reads both before deciding what it means. join_type—always_left,innerandfull_outerdecide whether unmatched rows survive. Each is carried over explicitly. Turning an inner join into a left join changes totals without changing a single definition, and the parity check would report that difference without explaining it.sum_distinct_onand friends —sum_distinct_on,average_distinct_on,median_distinct_onandpercentile_distinct_ontake acustom_primary_key_sqlfor materialized or unnested tables. The custom key is what makes the aggregate correct, so it travels with the measure.- Primary keys — every Omni view needs a
primary_key: truedimension for symmetric aggregates to hold. Those carry straight across to Malloy'sprimary_key:, and a view that never had one is flagged, because the aggregates that depended on it were not protected in Omni either.
Before & After
An Omni .view and .topic:
# order_items.view
views:
- name: order_items
schema: PUBLIC
table_name: order_items
dimensions:
order_item_id:
primary_key: true
status:
type: string
sql: ${TABLE}.order_status
value_tier:
type: string
sql: |
CASE WHEN ${TABLE}.sale_price >= 100 THEN 'High'
WHEN ${TABLE}.sale_price >= 25 THEN 'Medium'
ELSE 'Low' END
measures:
total_revenue:
description: Gross merchandise value across all order items
sql: ${TABLE}.sale_price
aggregate_type: sum
completed_revenue:
sql: ${TABLE}.sale_price
aggregate_type: sum
filters: { status: { is: Complete } }
order_count:
sql: ${TABLE}.order_id
aggregate_type: count_distinct# order_items.topic
topic:
base_view: order_items
label: Order Analysis
ai_context: |
Order-item level revenue and fulfillment. Use total_revenue for GMV and
completed_revenue for recognized revenue; join users for customer demographics.
joins:
users:
# a removable, query-time default — not baked into the model
filters:
order_items.status:
is: Completesource: order_items is conn.table('analytics.public.order_items') extend {
primary_key: order_item_id
join_one: users is conn.table('analytics.public.users') on user_id = users.id
dimension:
#(doc) Order item status
#(index)
status is order_status
#(doc) Sale-price value bucket
value_tier is
pick 'High' when sale_price >= 100
pick 'Medium' when sale_price >= 25
else 'Low'
measure:
#(doc) Gross merchandise value across all order items
# currency
total_revenue is sum(sale_price)
#(doc) Recognized revenue from completed items
# currency
completed_revenue is sum(sale_price) { where: status = 'Complete' }
#(doc) Distinct orders
order_count is count(order_id)
#(doc) Average order value
# currency
avg_order_value is total_revenue / order_count
view:
#(doc) Revenue by month
revenue_by_month is {
group_by: created_at.month
aggregate: total_revenue
}
}The topic's ai_context becomes #(doc) intent on the source and its views; the same physical view joined multiple ways in a topic becomes multiple named joins in Malloy.
More than a reformat. Definitions once split across shared, branch, and workbook layers collapse into one canonical, AI-discoverable model that composes into new questions. See what you gain →