Data Infrastructure·8 min read·Mar 12, 2026

Migrating Tableau Calculated Fields to dbt: What Nobody Tells You

The real pain of inheriting a Tableau workbook with 100+ calculated fields — and what I built to make the migration actually manageable.

You've been handed a Tableau workbook. It powers the company's weekly revenue report, the sales team's pipeline view, and three dashboards the CFO looks at every Monday. There are 140 calculated fields. No documentation. The person who built it left six months ago.

Your job: migrate this to dbt. Make it version-controlled, testable, documented, and reusable. Don't break anything.

This is not a hypothetical. It's a pattern I see constantly working with small data teams trying to modernize their stack. And the migration always hits the same wall: the calculated fields.

The Wall Everyone Hits

Moving tables and data sources to dbt is straightforward. You write staging models, define your sources, run dbt test. That part is well-documented and there are good patterns to follow.

The calculated fields are where it falls apart. Tableau has its own formula language — IIF() instead of IFF(), date literals wrapped in # signs, DATETRUNC with different argument orders depending on your warehouse, and fields referenced by internal IDs like Calculation_1634806716618883078 that mean nothing without the ID map.

Then there are LOD expressions. {FIXED [Customer ID] : SUM([Revenue])} is Tableau's way of saying "aggregate at a different grain than the view." In SQL, that's a CTE — but which CTE, at what grain, joined on what key, that all has to be worked out manually for each one.

With 140 fields, that's not a morning's work. It's weeks. And most of that time isn't thinking — it's translating syntax, looking up ID mappings, and reconstructing intent from formula names like "% Attainment YTD v2 FINAL."

What Makes It Harder Than It Should Be

01

Aggregates and row-level fields are mixed together

Tableau doesn't force you to separate SUM(Revenue) from a row-level string concatenation. They live in the same list. In dbt, they can't — aggregates need a GROUP BY in a fct_ model, row-level expressions go in a dim_ model. Sorting them manually across 100+ fields is tedious and error-prone.

02

Internal field IDs are everywhere

Tableau auto-generates IDs like Calculation_1634806716618883078 for fields referenced by other fields. The formula you're reading says [Calculation_1634806716618883078] / [Calculation_9182736450192837465]. You have no idea what those are without building the ID map yourself from the XML.

03

Table calculations simply don't translate

RUNNING_SUM, RANK, INDEX, WINDOW_AVG — these are Tableau table calculations. They don't have a direct SQL equivalent. The right answer is a window function, but which PARTITION BY and ORDER BY columns requires understanding what the original visualization was doing. That context is usually gone.

04

You're migrating logic you didn't write

The hardest part isn't the SQL. It's not knowing what the field was supposed to mean. Was that IIF() a business rule or a data quality patch? Is that date filter intentional or a quirk of the original dataset? There's no one to ask.

What I Built

I've been working on a tool called tableautodbt.com to automate the parts of this migration that are mechanical — so you can spend your time on the parts that actually require judgment.

You upload a .twb file. The tool parses every calculated field, resolves the internal ID map, classifies each field by complexity, runs a rule-based translation pass (Snowflake or BigQuery dialect), then sends the complex ones through an AI pass. What comes out is a structured dbt package:

# What's in the zip
models/staging/stg_{datasource}.sql
models/marts/fct_{datasource}.sql
models/marts/dim_{datasource}.sql
metrics.yml # MetricFlow semantic layer
schema.yml # not_null tests, descriptions
sources.yml
translation_report.md
SETUP.md

The key design decision: aggregates and row-level expressions are automatically split into separate models. LOD expressions generate CTE templates that are injected directly into the WITH clause of your fct_ model. Table calculations get window function hints in the translation report. You're not starting from scratch — you're starting from 80% done, with a clear list of what still needs your judgment.

Who This Is For

This tool is most useful for small data teams — one to three analytics engineers — who are setting up dbt for the first time and need to migrate existing Tableau workbooks as part of that effort. If you're starting fresh and don't have a semantic layer yet, this gets you most of the way there without months of manual work.

It's less useful if:

  • You already have a mature dbt project and just need to add a few fields (faster to write them manually)
  • Your workbooks are mostly table calculations (those still require manual window function rewrites)
  • You need Redshift or DuckDB (Snowflake and BigQuery only right now)

Honest Limitations

I want to be straightforward about what this tool doesn't do well yet, because I think the "AI writes all your code" framing does a disservice to AEs doing real migrations.

Table calculations

RUNNING_SUM, RANK, INDEX and similar Tableau table calcs are classified as untranslatable. The tool generates window function hints and SQL templates in the report, but the PARTITION BY and ORDER BY columns have to be filled in manually. There's no way around this — the context for those choices lives in the visualization, not the formula.

LOD inner expressions

FIXED LOD CTEs are generated rule-based and injected into your model, but the source table reference and join key are placeholders. You'll need to wire those up to your actual staging model and primary key. The CTE structure is correct; the plumbing is a TODO.

Parameters

Tableau parameters are annotated with a comment in the SQL but not resolved. Whether they should become dbt variables, seeds, or hardcoded values is a judgment call that depends on how the parameter was used in the workbook.

MetricFlow entity setup

The metrics.yml generates correct semantic model structure, but TODO_primary_entity and TODO_ID_COLUMN are placeholders. Primary key auto-detection from workbook XML join relationships isn't built yet — it's on the roadmap.

Multi-datasource workbooks

Fields that reference columns from a different datasource than their own may produce incorrect source() references. Multi-datasource workbooks need a manual review pass on the staging models.

Currently Building

This Is a Work in Progress

I'm actively building this and there's a lot still to improve. The rule-based translation handles common Tableau patterns well, but real workbooks are messy — unusual formulas, nested LODs, heavy parameter dependencies. The AI pass catches a lot of it but not everything.

What's next on the roadmap: Redshift dialect, primary key auto-detection from workbook join relationships, better LOD INCLUDE handling, and eventually a Tableau Server API mode so you can pull workbooks directly without uploading a file.

If you try it on a real workbook and find something that doesn't translate correctly, I genuinely want to know. The tool gets better with real migration feedback. You can reach me at justin@klardata.com.

Try it on your workbook

Free to use right now while I'm collecting feedback. Upload a .twb and see what comes out.

tableautodbt.com →
Justin Leu

Justin Leu

Data & BI Consultant · San Francisco

17+ years helping companies like Google, Pinterest, Salesforce, and United Healthgroup turn raw data into actionable business intelligence. I write about BI strategy, data infrastructure, and the practical side of analytics.

Work with me →