Migrate dbt models into Metabase Transforms automatically.
This tool reads your dbt project, compiles it with dbt compile, and creates Metabase transforms from the compiled SQL — complete with dependency ordering, schema mapping, tagging, and job scheduling.
dbt project ──► dbt compile ──► manifest.json ──► schema remapping ──► Metabase Transforms API
The tool leverages dbt's own compiler to resolve all Jinja templating (ref(), source(), var(), macros, conditionals, packages like dbt_utils, etc.) into plain SQL. This means every dbt feature works out of the box — no need for regex-based rewriting or manual macro translation.
| dbt concept | What happens |
|---|---|
| Models (table, view, incremental, ephemeral) | Converted to Metabase transforms |
| Sources | Referenced as-is in compiled SQL (no transform created) |
| Seeds | Referenced as-is in compiled SQL (no transform created) |
| Snapshots (SCD2) | Registered in dependency graph but not migrated — dbt must keep running dbt snapshot |
- Python >= 3.9
- dbt-core >= 1.4 (with your database adapter, e.g.
dbt-postgres) - git (if cloning from GitHub)
- Metabase with Transforms support (Metabase 50+)
A live database connection is required for dbt compile to resolve introspective queries. You can provide credentials inline in the config or point to an existing profiles.yml.
pip install .For development:
pip install -e ".[dev]"- Copy the example config:
cp config.example.yaml config.yaml-
Edit
config.yamlwith your dbt project source, database credentials, and Metabase connection. -
Dry run to preview the migration plan:
dbt-to-metabase plan --config config.yaml --stdout- Execute the migration:
dbt-to-metabase migrate --config config.yamlThe config file has three main sections: dbt project source, Metabase connection, and migration settings.
Three input modes are supported:
dbt:
project_path: "/path/to/your/dbt/project"
target: "prod"
profiles_dir: "~/.dbt" # OR provide inline credentials belowdbt:
github_repo: "your-org/your-dbt-repo"
github_branch: "main"
# github_token: "ghp_..." # Or set GITHUB_TOKEN env var
# github_subdirectory: "" # For monorepos
target: "prod"
db_type: "postgres"
db_host: "localhost"
db_port: 5432
db_user: "analytics"
db_name: "warehouse"
db_schema: "public"If you already run dbt compile in CI/CD, pass the manifest directly:
dbt:
manifest_path: "/path/to/target/manifest.json"Or via CLI:
dbt-to-metabase migrate --config config.yaml --manifest-path ./target/manifest.jsondbt compile requires a live database connection. You can provide credentials in two ways:
Inline credentials (generates profiles.yml automatically):
dbt:
db_type: "postgres"
db_host: "localhost"
db_port: 5432
db_user: "analytics"
# db_password: "" # Or set DBT_DB_PASSWORD env var
db_name: "warehouse"
db_schema: "public"Existing profiles.yml:
dbt:
profiles_dir: "~/.dbt"
target: "prod"dbt's compiled SQL contains literal schema references (e.g. staging.stg_orders). To avoid Metabase transforms overwriting your original dbt tables, you remap schemas to a transforms_* namespace:
transform_schema_prefix: "transforms_"
schema_remap:
staging: transforms_staging
intermediate: transforms_intermediate
marts: transforms_marts
analytics: transforms_analyticsThis rewrites SELECT * FROM staging.stg_orders to SELECT * FROM transforms_staging.stg_orders in the generated transform queries.
Note: Do not add your snapshot schema (e.g.
dbt_models_snapshot) or your database name toschema_remap. Snapshot tables are managed by dbt and should be referenced by their original schema. The database name is not a schema — adding it causes three-part references like"database"."schema"."table"to be incorrectly rewritten.
Metabase incremental query transforms use a checkpoint column to track which rows have already been processed. On each run, Metabase only processes rows where the checkpoint column value is greater than the last-seen value — similar to dbt's incremental materialization.
Since dbt compile evaluates is_incremental() as False (the model doesn't exist yet at compile time), the incremental block is compiled away and the checkpoint column cannot be auto-detected. You must declare these mappings explicitly:
checkpoint_columns:
- model: "fct_orders"
column: "updated_at"
- model: "fct_events"
column: "event_timestamp"How it works: Transforms are initially created as non-incremental and run once to bootstrap the target table. After the first successful run, you must set the checkpoint column manually in the Metabase UI (Data Studio → Transforms → transform name → Settings → "Column to check for new values"). The Metabase API for incremental strategy is currently unstable and may change — setting it via the UI is the reliable path.
dbt snapshots are SCD2 (Slowly Changing Dimension Type 2) tables that track row-level changes over time. They add dbt_valid_from, dbt_valid_to, and dbt_scd_id columns to capture historical state.
Metabase transforms cannot replicate SCD2 logic. Transforms either overwrite the entire table (full refresh) or append new rows via a checkpoint column. There is no merge/upsert capability needed for change tracking.
This tool handles snapshots by registering them in the dependency graph but skipping them during transform creation. They appear in the migration plan under skipped_models with the reason "Snapshot (SCD2) — managed by dbt, not migrated". Downstream models that ref() a snapshot will still work — the compiled SQL references the snapshot table directly (e.g. "analytics"."dbt_models_snapshot"."snapshot_account"), and the Metabase transform reads from that existing table.
Important: you still need dbt snapshot running on a schedule. Snapshots are not migrated to Metabase — dbt continues to own them. If you stop running dbt snapshot, the snapshot tables will go stale and downstream transforms will read outdated data.
A typical setup after migration:
dbt snapshot (cron) ──► snapshot tables (SCD2, managed by dbt)
│
▼
Metabase transforms ──► read from snapshot tables via SQL
Config note: do not add your snapshot schema (e.g. dbt_models_snapshot) to schema_remap. That schema should remain untouched so transforms read from the same tables that dbt writes to.
If your project uses snapshots and you want to eventually remove dbt entirely, you have a few options:
- Metabase Python transforms — write the SCD2 merge logic manually in a Python transform
- Freeze the history — stop
dbt snapshot, keep existing tables as-is, history stops accumulating - Minimal dbt — strip your dbt project down to just the
snapshots/folder and a cron job - Upstream CDC — move change tracking to your ETL tool (Fivetran, Airbyte history mode) so snapshots become unnecessary
metabase:
url: "https://metabase.example.com"
api_key: "mb_..." # Or set METABASE_API_KEY env var
# username: "admin@example.com" # Or set METABASE_USERNAME env var
# password: "..." # Or set METABASE_PASSWORD env var
database_id: 1
default_schema: "analytics"See config.example.yaml for a complete annotated configuration file.
Run the full migration pipeline:
dbt-to-metabase migrate --config config.yamlOptions:
--dry-run— generate plan without executing--output plan.json— export plan to JSON--validate— run data validation after transforms complete--manifest-path PATH— use a pre-compiled manifest.json--dbt-project-path PATH— override dbt project location--dbt-target NAME— override dbt target
Generate a migration plan without executing it:
dbt-to-metabase plan --config config.yaml --stdoutValidate configuration and connectivity:
dbt-to-metabase check --config config.yamlThis verifies that dbt can compile successfully, dependencies resolve, and Metabase is reachable.
Compare transform output tables against the original dbt tables:
dbt-to-metabase validate --config config.yamlRemap existing Metabase cards/dashboards from dbt tables to transform tables:
dbt-to-metabase remap --config config.yaml┌─────────────────────────────────────────────────────────┐
│ DbtCompiler │
│ git clone (or local path) → dbt deps → dbt compile │
│ Output: target/manifest.json │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ManifestParser │
│ Read manifest.json → DbtProject with compiled SQL │
│ (models, sources, seeds, snapshots, dependencies) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ TransformSqlAdapter │
│ Remap schema references in compiled SQL │
│ staging.orders → transforms_staging.orders │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Migrator │
│ Dependency ordering → Metabase API calls │
│ Create transforms, tags, jobs, run in order │
└─────────────────────────────────────────────────────────┘
| Variable | Description |
|---|---|
GITHUB_TOKEN |
GitHub personal access token for private repos |
DBT_DB_PASSWORD |
Database password for dbt compile |
DBT_DB_USER |
Database user for dbt compile |
DBT_DB_HOST |
Database host for dbt compile |
DBT_DB_NAME |
Database name for dbt compile |
METABASE_API_KEY |
Metabase API key |
METABASE_USERNAME |
Metabase username |
METABASE_PASSWORD |
Metabase password |
v0.1 used the github: config key and a regex-based SQL rewriter. v0.2 uses dbt compile for accurate SQL generation.
Config changes:
# v0.1 (still supported with deprecation warning)
github:
repo: "org/repo"
branch: "main"
# v0.2 (recommended)
dbt:
github_repo: "org/repo"
github_branch: "main"
target: "prod"
db_host: "localhost"
db_user: "analytics"
db_name: "warehouse"New requirements:
dbt-coremust be installed (pip install dbt-postgresor your adapter)gitmust be available if cloning from GitHub- Database credentials are needed for
dbt compile - Checkpoint columns must be declared explicitly in config (no longer auto-detected)
dbt snapshotmust continue running if your project uses dbt snapshots — they are not migrated to Metabase transforms
What you can remove:
- No more
sql_rewriterwarnings about unsupported Jinja - No more
NULLreplacements for unrecognized macros - No more passthrough queries for complex models
For a working example, see dbt-metabase-postgres-example which includes a dbt project, Postgres setup, and Docker Compose for local testing.
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Lint
ruff check .