Support
Log In

Migrate from Power BI

Convert your Power BI tabular model and DAX measures into governed Malloy semantic models

Your Power BI semantic model holds the definitions your business runs on — tables, relationships, and DAX measures. Credible reads the model definition, re-expresses its DAX logic as Malloy, and — where you connect it — validates each measure against Power BI's own query engine before you cut over.

What Credible Reads

Your tabular model — tables, columns, relationships, and DAX measures — from its text definition in TMDL (Tabular Model Definition Language, the folder-based format inside a PBIP project) or the older TMSL. Hand the agent those files and it can translate. Connecting the official Power BI Modeling MCP server (which loads TMDL) or the XMLA endpoint is optional: it lets the agent load the model live and run DAX for validation.

What Comes Across

The everyday modeling carries over. Here's how the bigger pieces land — and where they get better:

In Power BIIn Credible
Tabular model, tables, relationshipsMalloy sources and joins
DAX measures & calculated columnsMeasures and dimensions — CALCULATE's filter-context gymnastics become plain, readable filtered aggregates
Time-intelligence (TOTALYTD, …)Rebuilt as explicit time-grain truncation and window views
RLS rolesFine-grained access control in the model, versioned and enforced on every surface
Descriptions#(doc) / #(index), indexed by the AI Analytics Engine
Reports & dashboardsRebuilt as data apps or notebooks

The Migration Flow

Credible reads the TMDL, translates tables and relationships to Malloy sources and joins and DAX measures to Malloy measures, enriches with #(doc)/#(index) tags, and — where you connect it — validates row-by-row using the ExecuteQueries API to run the original DAX and diff it against the migrated result.

What Credible Handles

  • Filter context and CALCULATE — a simple CALCULATE([Total], Status="shipped") is an equivalent filtered aggregate in Malloy. DAX that removes or overrides context (ALL, REMOVEFILTERS) has no ambient equivalent and is re-modeled as an all()/level-of-detail aggregate or a separate query — explicitly, never guessed.
  • Time-intelligence (TOTALYTD, SAMEPERIODLASTYEAR) depends on a marked date table and the visual's current filter. Malloy has no ambient filter context, so year-to-date is not a measure — it becomes an explicit cumulative view driven by the query's date grain.
  • Implicit measures — auto-aggregations that were never stored as named measures are materialized explicitly, so parity checks don't miss them.

Before & After

A TMDL table (relationships live in a separate relationships.tmdl):

table Sales
	column OrderID
		dataType: int64
		isKey
		sourceColumn: order_id

	/// Date the order was placed
	column OrderDate
		dataType: dateTime
		sourceColumn: created_at

	column Status
		dataType: string
		sourceColumn: order_status

	/// Total revenue in USD
	measure 'Total Revenue' = SUM('Sales'[Amount])
		formatString: \$#,0.00

	/// Revenue from shipped orders only (filter context)
	measure 'Shipped Revenue' =
			CALCULATE ( [Total Revenue], 'Sales'[Status] = "shipped" )

	/// Share of revenue that shipped
	measure 'Shipped Revenue %' =
			DIVIDE ( [Shipped Revenue], [Total Revenue] )
		formatString: 0.0%

	/// Year-to-date revenue
	measure 'Revenue YTD' = TOTALYTD ( [Total Revenue], 'Calendar'[Date] )
source: orders is conn.table('sales.orders') extend {
  primary_key: order_id
  join_one: customers is conn.table('sales.customers') on customer_id

  dimension:
    #(doc) Date the order was placed
    order_date is created_at::date

    #(doc) Order fulfillment status
    #(index)
    status is order_status

  measure:
    #(doc) Total revenue in USD
    # currency
    total_revenue is sum(amount)

    #(doc) Revenue from shipped orders only
    # currency
    shipped_revenue is sum(amount) { where: status = 'shipped' }

    #(doc) Share of revenue that shipped
    # percent
    shipped_revenue_pct is shipped_revenue / total_revenue

  view:
    #(doc) Year-to-date revenue — cumulative by month
    revenue_ytd is {
      group_by: order_date.month
      aggregate: total_revenue
      calculate: running_total is sum_cumulative(total_revenue) {
        partition_by: order_date.year
        order_by: order_date.month asc
      }
    }
}

CALCULATE with a simple predicate becomes a { where: … } filtered measure; TOTALYTD — which relied on ambient filter context — becomes an explicit cumulative view. The formatString masks map to # currency/# percent.

More than a reformat. The migration leaves DAX filter-context complexity behind for aggregates that are correct by construction, and your logic becomes portable, AI-discoverable code instead of a Power BI artifact. See what you gain →

Next Steps

On this page