Support
Log In

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 OmniIn Credible
Shared model, topics, viewsMalloy sources (base + joined); import/export curate exposure
relationships file — join_from_view, join_to_view, on_sqljoin_one / join_many on the source, with the on_sql condition as the join expression
relationship_type: many_to_one / one_to_manyjoin_one / join_many — the cardinality is the keyword
relationship_type: assumed_many_to_oneFlagged, not converted: an inferred join nobody declared is a decision, not a fact
reversible: trueTwo named joins, one per direction, rather than one join used both ways
join_from_view_as aliasesSeparate named joins on the same source
primary_key: true dimensionprimary_key: on the source — what keeps symmetric aggregates correct on both sides
Dimensions & measuresCarried over, filtered and ratio measures included
Field-level sql: with ${…} refsResolved into Malloy expressions with explicit types
Logic split across shared / branch / workbook layersReconciled into one canonical definition per field
access_filters on a topicAn #(access_filter) rule reading the same column — the rows a caller may see
required_access_grantsAn #(authorize) lock, gating the whole source: every row, or none
hidden_unless_access_grantsFlagged, 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 & dashboardsRebuilt 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_filters on 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_sql and always_having_filters are 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: true lets 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, and extends lets 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, inner and full_outer decide 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_on and friends — sum_distinct_on, average_distinct_on, median_distinct_on and percentile_distinct_on take a custom_primary_key_sql for 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: true dimension for symmetric aggregates to hold. Those carry straight across to Malloy's primary_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: Complete
source: 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 →

Next Steps

On this page