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:
- Screenshots only. Fast, good for a handful of tiles.
- A hand-assembled bundle. Copy the SQL and numbers out of the tool by hand.
- 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:
{
"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.mdunder a## Not fetchedheading 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-indct importcommands 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.mdline 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.