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 BI | In Credible |
|---|---|
| Tabular model, tables, relationships | Malloy sources and joins |
| DAX measures & calculated columns | Measures 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 roles | Fine-grained access control in the model, versioned and enforced on every surface |
| Descriptions | #(doc) / #(index), indexed by the AI Analytics Engine |
| Reports & dashboards | Rebuilt 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 simpleCALCULATE([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 anall()/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 →