Business Intelligence

How Do You Convert LookML to dbt MetricFlow?

How each LookML object maps to dbt MetricFlow, what we got converting Looker's own sample project with our open-source unshackle CLI, and the one choice that held back most measures.

Javier BonillaCo-founder & CTO

To convert LookML to dbt MetricFlow, turn each view into a semantic model on a dbt model, primary keys and join keys into entities, dimension groups into time dimensions, and measures into metrics. Explores disappear: MetricFlow works out joins from entities. Derived tables become dbt models. Liquid gets rewritten by hand, and access filters move out of MetricFlow altogether.

You don't have to type it all by hand. We built unshackle, a free, open-source CLI that reads a LookML project and writes MetricFlow YAML into your dbt repo. We ran it on Looker's own public sample project. Most of the hand work it left came down to one question Looker never asked: which date does each table count by?

How does each LookML object map to MetricFlow?

LookML MetricFlow What changes
View on a table (sql_table_name) Semantic model on a dbt model (model: ref('...')) One semantic model per dbt model
derived_table, including PDTs A dbt model, with a semantic model on top dbt builds the table instead of Looker
primary_key: yes dimension Primary entity
Join sql_on in an explore Foreign entity matching a primary or unique entity of the same name on the other model No join config: MetricFlow finds the path
Dimension Categorical dimension
dimension_group (type: time) Time dimension with a granularity You pick the grain at query time
Measure (sum, count_distinct, average) Simple metric
Measure with filters: Simple metric with a filter:
type: number measure over other measures Ratio or derived metric
sql_always_where, access_filter Nothing in MetricFlow Row filters live in the warehouse or the tool that queries
Liquid templating Nothing One metric per branch, or a filter
LookML dashboards Nothing Rebuilt in whatever reads the metrics

A filtered LookML measure and the MetricFlow metric it became: count_bounces, a count filtered to is_exit_event "Yes", turns into the simple metric events_count_bounces with a filter on the events__is_exit_event dimension.

The table is the easy half. A filtered measure like count_bounces (a count with filters: is_exit_event "Yes") comes out as a simple metric with filter: '{{ Dimension(''events__is_exit_event'') }} = true'. A type: number measure like bounce_rate, which divides one measure by another, comes out as a derived metric.

Can you convert LookML to MetricFlow automatically?

The YAML, yes. The decisions, no.

dbt Labs has its own converter, dbt-converter, with a convert-lookml command. Its README says it "can only help with the migration and leaves many manual steps to the user", and that it requires dbt-core 1.5. Its last commit was on February 11, 2025.

unshackle is the one we maintain. It's Apache 2.0, needs no account and no Querio, and it reads a LookML Git checkout without any Looker credentials. When your repo has a dbt_project.yml, it writes MetricFlow next to Apache Ossie files (the open semantic-layer format formerly called Open Semantic Interchange). What it can't translate faithfully, it leaves commented out under a TODO(unshackle) line with the original formula. It doesn't guess.

pip install unshackle
cd your-dbt-repo
unshackle import looker path/to/lookml
unshackle build looker

On November 2, 2026, we ran unshackle 0.1.0 on thelookevent, Looker's open-source sample e-commerce project. It wrote 34 semantic models and 112 MetricFlow measures. 14 were ready to use. The other 98 needed a person.

Why do most measures wait on a time dimension?

Why 98 of the 112 measures in Looker's sample project needed a person: 70 sit on a model with several dates and no default, 26 on a model with no date at all, and 2 for other reasons.

96 of those 98 measures were waiting on a date. 70 sat on models with several dates and no default. 26 sat on models with no date at all.

LookML never makes you choose. The order_items view has four dimension groups: created, shipped, delivered and returned. A Looker user picks one when they build a query. MetricFlow wants the choice in the file. A semantic model that defines metrics needs at least one time dimension with a granularity and an agg_time_dimension, the default date its metrics count by.

So unshackle lists the candidates and stops:

# TODO(unshackle): measure order_items.order_items_total_sale_price: the source declares no default time dimension; set defaults.agg_time_dimension to one of returned, shipped, delivered, created
#   measures:
#   - name: order_items_total_sale_price
#     agg: sum
#     expr: sale_price

The fix is a decision, then two edits: set the default and uncomment the block.

semantic_models:
- name: order_items
  model: ref('order_items')
  defaults:
    agg_time_dimension: created
  measures:
  - name: order_items_total_sale_price
    agg: sum
    expr: sale_price

We did that for order_items, picking created. The file went from 0 live measures to 12 measures and 14 metrics, including the return_rate ratio metric. It passed dbt parse and mf validate-configs --skip-dw with no errors on dbt-core 1.12.6, once we gave one metric a unique label. The 26 measures on models without a date need a date column added to the dbt model first.

Pick the date the business means. Revenue by order date and revenue by ship date are both right, and they're different numbers. Looker let every user choose. Now one person chooses once, in review.

This YAML has measures: blocks, which dbt-core 1.12 still reads. dbt's newer format folds measures into simple metrics, and dbt-autofix deprecations --semantic-layer rewrites the files to it.

What happens to explores, joins and the rest?

Explores go away. MetricFlow joins from the many side to the one side, and it rejects joins the other way, which would fan out the rows. Looker handles that fan-out with symmetric aggregates, which are on by default in every explore where the SQL dialect supports them. In MetricFlow, entity types do that job. MetricFlow also follows at most two hops.

A dimension that reads another view doesn't carry over. In the sample, order_items.gross_margin is ${sale_price} - ${inventory_items.cost}. unshackle leaves it as a TODO: compute it in the dbt model, or group through the join.

The rest:

  • Access filters. dbt's Semantic Layer runs every request with the credentials you configure, and any access policies tied to those credentials apply. In a test project we wrote by hand, unshackle turned an access_filter and a sql_always_where into SQL row filters and an access_grant into a Cedar policy, with a TODO to map the group to your identity provider.
  • Dashboards. The sample's 10 LookML dashboards became 10 Jupyter notebooks, with SQL behind 59 of their 95 tiles.

In what order should you do it?

  1. Run unshackle in your dbt repo on a checkout of your LookML.
  2. unshackle writes each derived table's SQL to queries/. Build each one as a dbt model, under the name its semantic model already uses.
  3. Point every ref() at your real dbt model names. unshackle takes them from the LookML table names.
  4. Add a time spine model. MetricFlow needs one before dbt parse accepts the metrics, and unshackle doesn't write it.
  5. Set agg_time_dimension on every model with more than one date, and uncomment the blocks the TODOs list.
  6. Rewrite Liquid, cross-view dimensions and access rules by hand. unshackle todo lists what's left.
  7. Validate, then compare numbers with Looker before anyone switches. Start with the metrics your leadership team reads every week.

How do you check the numbers match Looker?

Run dbt parse, then mf validate-configs with open-source MetricFlow, or dbt sl validate on the dbt platform. That proves the YAML is valid. It doesn't prove the numbers.

For the numbers, run the same question both ways. unshackle verify runs the migrated SQL against your warehouse and compares it with the old tool's results. It needs a warehouse connection and a few test cases you write.

What reads the metrics once Looker is gone?

MetricFlow has no screen. Something has to query it, and Looker isn't on dbt's list of Semantic Layer integrations.

Serving metrics through dbt's APIs takes a dbt Starter, Enterprise or Enterprise+ plan. Without one, you can still define metrics and query them from the command line with mf query. Our dbt Semantic Layer guide covers what's free and what isn't.

Querio reads your dbt project's semantic_manifest.json, so your metrics stay defined once, in dbt, and we keep no second copy. Claude, ChatGPT or any agent connected through our MCP server can ask for a metric by name. It runs as the signed-in person and returns only what they're allowed to see. That call uses no Querio AI credits, and the MCP server is on every plan, Free included. Coming from Looker? Here's how Querio compares.

FAQ

Does converting change anything in Looker?

No. unshackle only reads. Your LookML, explores and dashboards stay as they are, so both can run side by side while you compare numbers.

What happens to Looks and dashboards people built in Looker?

A LookML checkout holds only what's in code: LookML dashboards come across, but dashboards and Looks people built in the Looker UI don't. To bring those, run unshackle against Looker's API with credentials, and it writes Looks as SQL queries and dashboards as notebooks.

Magic happens when people and agents work together

Get started for freeBook a demo