Skip to content

Import from any other tool

dct import has built-in importers for Looker, Omni, Sigma, and Hex. Everything else still works, because the agent that writes the board reads a source bundle, not the tool. Give it a folder in the bundle layout and it migrates a Tableau workbook exactly as it migrates a Looker dashboard.

You have three ways to get there, from least to most work:

  1. Screenshots only. Fast, good for a handful of tiles.
  2. A hand-assembled bundle. Copy the SQL and numbers out of the tool by hand.
  3. A small fetcher script. Worth it when you have many dashboards to move, and worth sharing when you're done.

Option 1: screenshots

Make a folder with a png/ directory and a source.json:

tableau-sales/
  source.json
  png/
    01-overview.png
    02-regions.png
{
  "tool": "tableau",
  "url": "https://tableau.acme.com/#/views/Sales/Overview",
  "fetched_at": "2026-11-12T09:00:00Z"
}

Then ask your agent:

Migrate ./tableau-sales into a dbt Charts board. The data is in the
analytics.orders and analytics.customers models.

The agent reads each image, infers the queries from your description of the data, and leaves a note on any chart whose query it had to guess. Expect to check numbers by hand: a screenshot has no SQL to copy.

Option 2: a hand-assembled bundle

Adding a few more files turns guesses into copies. The full layout:

Path What to put there
outline.md Each page, then each tile in reading order: type (bar, line, KPI, table, text), title, grid position, fields, filters, number formats. Plus every control: its label, default, and which tiles it filters
source.json tool, url, fetched_at
queries/<tile>.sql The SQL each tile runs. Most tools show it under "View SQL", "Show query", or a query log
kpi/<tile>.csv For each single-number tile, a header row and the one row it shows, headline column first. These are your parity numbers
png/ A screenshot of each page
raw definition Optional: whatever export the tool offers (.twb, dashboard JSON)

A minimal outline.md:

# Sales overview

## Page: Overview

- **kpi** total_revenue "Total revenue" (row 1, col 1): sum of amount, currency
- **bar** revenue_by_region "Revenue by region" (row 1, cols 2-3): x=region, y=sum(amount), sorted descending
- **line** monthly_orders "Orders per month" (row 2, cols 1-3): x=month(order_date), y=count(*)

## Controls

- **date range** "Order date", default last 12 months; filters every tile

The agent treats the outline as a digest and your SQL as the truth, so a rough outline is fine as long as queries/ is right.

Option 3: write a fetcher

A fetcher is a script that calls the tool's API and writes the bundle. The built-in importers run a few hundred lines each per tool. Start from this shape:

"""Fetch one <tool> dashboard into a dbt Charts source bundle."""

import json
import os
import sys
from datetime import UTC, datetime
from pathlib import Path

import requests

BASE = os.environ["MYTOOL_BASE_URL"]
TOKEN = os.environ["MYTOOL_API_TOKEN"]  # never write this to disk


def get(path: str) -> requests.Response:
    reply = requests.get(
        f"{BASE}{path}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=120
    )
    reply.raise_for_status()
    return reply


def main(dashboard_id: str) -> None:
    out = Path(f"mytool-{dashboard_id}")
    for sub in ("queries", "kpi", "png"):
        (out / sub).mkdir(parents=True, exist_ok=True)

    raw = get(f"/dashboards/{dashboard_id}").text
    (out / "dashboard.json").write_text(raw)
    dashboard = json.loads(raw)

    outline = [f"# {dashboard['name']}", ""]
    for tile in dashboard["tiles"]:
        outline.append(f"- **{tile['type']}** {tile['id']} \"{tile['title']}\"")
        (out / "queries" / f"{tile['id']}.sql").write_text(tile["sql"])
    (out / "outline.md").write_text("\n".join(outline) + "\n")

    (out / "png" / "dashboard.png").write_bytes(
        get(f"/dashboards/{dashboard_id}/image").content
    )
    (out / "source.json").write_text(
        json.dumps(
            {
                "tool": "mytool",
                "url": f"{BASE}/dashboards/{dashboard_id}",
                "fetched_at": datetime.now(UTC).isoformat(timespec="seconds"),
            },
            indent=2,
        )
    )
    print(f"Wrote {out}. Start with outline.md")


if __name__ == "__main__":
    main(sys.argv[1])

The endpoints and field names above are placeholders; every tool names them differently. Where to find each piece:

Tool Definition and SQL Values Images
Tableau REST API for workbooks and views; the Metadata API (GraphQL) for sheets, fields, and custom SQL. A downloaded .twb is XML with the same View data export views/{id}/image
Metabase Dashboard and card APIs; each card has a native SQL or MBQL query card/{id}/query/csv Screenshots
Mode Report, query (with SQL), and chart APIs Report run exports Screenshots
Power BI REST API for reports and datasets; a .pbix or .pbit holds the model DAX through the dataset's executeQueries Report export
Superset Dashboard, chart, and saved query APIs, with SQL Chart data API Screenshots
Redash Dashboard and query APIs, with SQL Query results API Screenshots
Grafana The dashboard JSON API, with each panel's query Panel query API Rendering API
Looker Studio No definition API Export from the underlying source Screenshots

A few rules the built-in importers follow, and yours should too:

  • Read credentials from environment variables only. Never write a token into the bundle, the outline, or source.json. Strip tokens from URLs before you record them.
  • Copy, don't interpret. Write the tool's raw definition byte for byte and the SQL exactly as the tool compiled it. The agent does the interpreting.
  • Fail loudly on the dashboard, record gaps on tiles. If the dashboard itself cannot be fetched, exit with an error. If one tile's SQL or one image cannot, finish the bundle and list it in outline.md under a ## Not fetched heading with the tool's error.

Share your importer

If you write a fetcher for a tool we don't support, please share it. A working importer for Tableau, Metabase, Power BI, Mode, Superset, or anything else saves the next team the same afternoon, and it is the fastest way for a new dct import <tool> command to exist.

  • Publish it. Put the script in a public repository or gist with a short README: which tool and version it was tested against, which environment variables it reads, and any API permissions it needs.
  • Tell us about it. Open an issue on dbt-labs/dbt-charts titled Importer: <tool> with a link to your script. A sample bundle helps even more; strip anything sensitive first. The public repository is a read-only mirror today, so issues, not pull requests, are how importers reach us. We use shared importers as the starting point for built-in dct import commands and credit the people who wrote them.
  • Report gaps in the built-in importers the same way. A tile type the Looker, Omni, Sigma, or Hex importer misses, or an API change that breaks one, is worth an issue with the outline.md line it got wrong.

Even a rough script that handles only the tiles your dashboards use is worth sharing. The bundle layout is the contract, so anyone can build on it.