Agent skill for migrating Tableau, Looker, and Power BI dashboards to Hex apps: extract specs, rebuild with Hex AI, verify parity
# Add to your Claude Code skills
git clone https://github.com/Humphrey4data/tableau-to-hex-skillGuides for using ai agents skills like tableau-to-hex-skill.
tableau-to-hex-skill is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by Humphrey4data. Agent skill for migrating Tableau, Looker, and Power BI dashboards to Hex apps: extract specs, rebuild with Hex AI, verify parity. It has 53 GitHub stars.
tableau-to-hex-skill's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/Humphrey4data/tableau-to-hex-skill" and add it to your Claude Code skills directory (see the Installation section above). tableau-to-hex-skill ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh tableau-to-hex-skill against similar tools.
No comments yet. Be the first to share your thoughts!
Unlocks once the catalog security scan passes (runs nightly).
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
Given a Tableau, Looker, or Power BI dashboard URL, produce a Hex app that is a faithful copy: same underlying queries and numbers, same filters, same colors/layout/labels, same tooltips. The build is driven by the Hex agent (hex thread CLI); source logic is extracted from the ground truth — Tableau's workbook file (.twbx), Looker's dashboard + query APIs, or Power BI's report definition (PBIR) + semantic model (TMDL/DAX) — and validated against the same warehouse Hex queries before it is handed to Hex.
All command snippets referenced below live in reference.md — read it before starting Phase 1.
Adaptivity rule: the procedure is environment-agnostic; only the leaf commands vary. Phase 0 pins four environment choices — source BI (Tableau / Looker / Power BI), warehouse engine (Snowflake / BigQuery / Databricks / Postgres-Redshift), source hosting (Tableau Cloud vs self-hosted Server; Looker-hosted vs customer-hosted; Power BI capacity/licensing tier), and OS (macOS vs Linux) — and every snippet in reference.md is either shared or labeled per branch. Follow your branch exactly; don't mix branches mid-run. (The Tableau + Snowflake + macOS branch is the most battle-tested; the others are documented equivalents.) Phases 2–5 are source-agnostic: Phase 1 is the source adapter, and whatever the source, it must produce the same artifact — a written spec with ground-truth queries, calc definitions, parameters/filters, colors, layout, and a rendered PNG.
Confirm these before Phase 1, and tell the user about anything that applies:
Works for: dashboards of charts, tables, and heatmaps whose datasources are reachable from a Hex warehouse connection (Tableau custom SQL or published sources; Looker explores/Looks on a warehouse connection Hex can also reach; Power BI DirectQuery/composite models over such a warehouse, or import models whose upstream source tables live in one), with quick filters and parameters (Looker: dashboard filters and listen-mapped tiles; Power BI: slicers and report/page/visual-level filters).
Power BI hard blockers — check before promising anything: (a) an import-mode model with no reachable upstream warehouse (data lives only inside the pbix/semantic model) has nothing for Hex to query — stop and tell the user; (b) measures are DAX, not SQL — every measure must be translated to warehouse SQL and proven equivalent, which makes Phase 2 the dominant cost and complex DAX (nested CALCULATE context transitions, heavy time intelligence) a per-measure judgment call; (c) licensing walls — XMLA read and export-to-file need Fabric capacity or Premium-Per-User, and the executeQueries REST API needs its tenant setting enabled. Budget roughly 2× the Tableau/Looker correction rounds.
Hex workspace requirements: the CLI must be enabled for the workspace, and Phase 3 depends on the Hex agent (threads) feature, which consumes the workspace's AI credits and may be disabled or exhausted on some plans. Verify early — hex thread create failing with a credits/feature error means the whole build path is blocked until the workspace admin enables it (there is no free fallback with comparable quality).
No clean Hex equivalent — agree a fallback with the user up front: Tableau Stories; dashboard actions (filter/highlight/URL/set actions); row-level security and user filters; user-specific data blending.
Out of scope: refresh schedules, permission migration, publishing/embedding, alert subscriptions. The deliverable is a draft Hex app at visual/numeric parity (Phase 5 evidence pack).
A container usually holds several dashboards (a Tableau workbook, a Power BI report's pages, a Looker folder). After Phase 1 lists them, ask the user two scope questions and record the answers in the spec:
Whatever the grouping, never mix panels from a dashboard that isn't in scope into another dashboard's tab.
- [ ] Phase 0: Setup + ground rules (tooling, fidelity policy, run manifest)
- [ ] Phase 1: Extract the source spec (Tableau: twbx + XML; Looker: dashboard/query APIs; Power BI: PBIR + TMDL/DAX; + rendered PNGs)
- [ ] Phase 2: Validate every query against the warehouse (the source's numbers as truth)
- [ ] Phase 3: Build the Hex app with `hex thread create --new-project`
- [ ] Phase 4: Self-check loop (screenshot diff, YAML, numbers) until parity
- [ ] Phase 5: Deliver the parity evidence pack, get user sign-off
Check what's installed; install only what's missing.
hex --version || curl -fsSL https://hex.tech/install.sh | bash
hex auth status || hex auth login # opens browser; confirms workspace + user
env block already has SERVER, SITE_NAME, PAT_NAME, PAT_VALUE — reuse those values for the REST API (config locations per agent: reference.md §5). Otherwise ask the user for a PAT (Tableau: My Account Settings → Personal Access Tokens). PATs expire after ~15 days of non-use. If sign-in returns 401001 (also shows up as the Tableau MCP server stuck in an error state), the PAT is dead — recovery steps: reference.md §1.POST /api/4.0/login (reference.md §11). If a Looker MCP is configured (managed MCP server or the MCP Toolbox --prebuilt looker), its env already has LOOKER_BASE_URL/LOOKER_CLIENT_ID/LOOKER_CLIENT_SECRET — reuse those. The API user needs access to the dashboard's folder and explores (API calls inherit their content permissions — a tile you can't see in the UI won't return SQL either).az login --allow-no-subscriptions then az account get-access-token per API audience (reference.md §12); a service principal also works but skips row-level security, which changes the numbers you validate against. The user needs Build permission on the semantic model and read+write on the report (the Fabric getDefinition API oddly requires write). Also verify the licensing preconditions above now, and check whether Microsoft's Power BI MCP servers are available (remote Fabric-hosted or local @microsoft/powerbi-modeling-mcp) — useful accelerators, not required (reference.md §12).WH branch and smoke-test it (reference.md preamble): Snowflake snow, BigQuery bq, Databricks dbsqlcli/Statement API, Postgres/Redshift psql. For any other engine (Trino, Athena, DuckDB/MotherDuck, ...), follow the same pattern: any CLI that takes a SQL string and returns JSON rows plugs into §8/§9 unchanged. All validation SQL and everything you feed the Hex prompt must be in this engine's dialect — Tableau's twb custom SQL and Looker's generated SQL already are (they ran against that warehouse), so never translate those; Power BI DAX is the exception that must be translated into this dialect (Phase 2). Never validate against a different engine than the one Hex will query.thread_id/project_id must not live only in shell scrollback. Use a stable run directory that survives reboots — ~/.tab2hex/<dashboard-slug>/ (not /tmp) — for the spec, saved SQL, PNGs, and a manifest.json with: dashboard URL, source IDs (Tableau: workbook + view LUIDs; Looker: dashboard ID + tile query IDs; Power BI: workspace + report + semantic-model IDs), spec path, source PNG paths, thread_id, project_id, Hex draft URL, and the environment branch (source BI, WH engine + connection name, hosting flavor, OS). Resuming a partial migration = read the manifest, re-pin the same environment branch, hex project export to see current state, continue the existing thread with hex thread continue — never start a second thread for the same app.curl via a payload file, not inline -d (terminal transcripts capture full command text; reference.md §1, §11). Remind the user to revoke/rotate it when the migration is done.curl, python3, unzip, hex, the warehouse CLI (snow / bq / dbsqlcli / psql), and az (Power BI branch). Where: Cursor → Settings → Terminal command allowlist (or per-prompt "Always allow" for that command); Claude Code → /permissions → add allow rules like Bash(hex:*), Bash(curl:*) (or permissions.allow in .claude/settings.json); Codex → approvals policy in config. Scope rules to these specific binaries — do NOT suggest blanket auto-approval of all commands, and never allowlist destructive ones (rm, git push, etc.).hex auth login (browser), the one-time headed browser login for the Hex screenshot profile (§10), az login (Power BI), and any MCP OAuth flows. Each is once-per-machine; batching them here means Phases 1–4 run unattended instead of stalling on a surprise pop-up. macOS only: the first hex call may trigger a Keychain prompt — have the user click "Always Allow" so it doesn't recur.Goal: a written spec of every section before any Hex work. Do not build from a screenshot alone — the calculation logic must come from the source's ground truth (Tableau: the workbook file; Looker: the query APIs; Power BI: the PBIR report definition + TMDL model). Every branch ends at the same final spec step.
https://<server>/#/site/<site>/views/<WorkbookContentUrl>/<ViewName> → workbook content URL + view name. (Tableau Server default site URLs omit the /site/<site> segment: https://<server>/#/views/<WorkbookContentUrl>/<ViewName> — use an empty TAB_SITE.) The view name may be a dashboard or a single worksheet — after downloading the workbook, list its dashboards (reference.md §3), show the list to the user and confirm the scope (all / the linked one / a subset — see Preconditions), then scope strictly to the chosen dashboards' worksheets./workbooks/<luid>/content). Unzip it; the .twb inside is XML. (Commands: reference.md §1–2.)<relation type='text'> is a full SQL query. Save each to the run directory — these are the ground-truth queries.<column> elements with <calculation formula='...'>. Worksheet-level table calcs (RUNNING_SUM, WINDOW_*) also live here — this is the only place to see them; the Metadata API does not expose them.Parameters datasource and no quick filters, that is a legitimate outcome: record "no filters/parameters" in the spec and skip Phase 4 step 3 / reference.md §9 entirely.(Commands: reference.md §11. Looker's API hands you more than Tableau's — the exact generated warehouse SQL per tile — so extraction is mechanically easier, but table calcs hide in a different place; see step 3.)
https://<instance>/dashboards/<id> — a numeric <id> is a user-defined dashboard (UDD); <model>::<slug> is a LookML dashboard (element addressing differs; both work with the same endpoints). /looks/<id> is a single saved Look — treat it as a one-tile dashboard.POST /api/4.0/login with the API3 credentials, then GET /dashboards/<id> → title, dashboard_filters (name, dimension, default value, per-tile listen mapping), dashboard_layouts (grid position/size per tile), and dashboard_elements (title, query_id, look_id, merge_result_id, vis_config).GET /queries/<query_id>/run/sql returns the exact warehouse SQL Looker runs — already in the warehouse's dialect, exclusions and calendar logic included. Save one file per tile; these go into Phase 2 verbatim.dynamic_fields JSON), pivots, or row totals — Looker applies those client-side after the SQL returns, exactly like Tableau's worksheet table calcs. Read GET /queries/<query_id> and translate every dynamic_fields entry into SQL (window functions) or a Hex-side transform; a tile whose SQL matches but whose table calc was dropped will pass the number check and fail the screenshot diff.GET /queries/<query_id>/run/json — the rows Looker actually displays; this is the comparison target for Phase 2 (no OCR from screenshots needed).merge_result_id set, query_id null): fetch the merge query to get the source query IDs, extract each, and replicate the join in SQL for Hex.vis_config on each element holds the chart type, series colors, axis options, and value formats — the Looker equivalent of the twb's color encodings.POST /render_tasks/dashboards/<id>/png), poll until success, download the PNG (reference.md §11). Ask the user for hover-tooltip screenshots — same rule as Tableau, tooltips render client-side.get_dimensions/get_measures). Business calendars, bake lags, and metric bundling live there.(Commands: reference.md §12. No .pbix binary parsing needed — the Fabric APIs return the report as readable JSON and the model as TMDL. The extraction is easy; the DAX→SQL translation in Phase 2 is where the work lives.)
https://app.powerbi.com/groups/<workspaceId>/reports/<reportId>/<pageId> → workspace + report IDs (groups/me = My workspace — ask the user to publish to a real workspace first). The URL pins one page, but a report usually has several — after step 2 lists them, ask the user which pages to migrate (all / the linked one / a subset — see Preconditions).POST .../reports/<id>/getDefinition → base64 parts in PBIR format (definition/pages/*/visuals/*/visual.json — visual type, bound fields/measures, formatting incl. colors; page-order and filter files) or PBIR-Legacy (one report.json; each visual's config is a JSON string inside the JSON — double-parse it). This is the twb-XML equivalent: sections, visuals, slicers, report/page/visual-level filters, bookmarks, custom tooltip pages.definition.pbir names its semantic model → export it as TMDL (Fabric semantic-model getDefinition, the local modeling MCP, or XMLA via Tabular Editor CLI). From TMDL pull: every DAX measure, calculated columns, relationships, RLS roles, and each table's M partition source — the M source reveals the upstream warehouse table (and for import mode, whether one exists at all — the hard-blocker check).EVALUATE SUMMARIZECOLUMNS(...) via the executeQueries REST API (tenant setting required; 100k-row cap) or the MCP Execute-Query tool. These rows are the Phase 2 comparison target — the displayed truth, RLS included.exportToFile (PNG/PDF) if the workspace is on capacity; otherwise screenshot app.powerbi.com with the §10 Playwright machinery (one-time login, persistent profile — same as the Hex side). Ask the user for hover-tooltip screenshots; also extract any tooltip pages (hidden pages flagged as tooltips in PBIR) — those are replicable from the definition, unlike hover defaults.Write the spec to the run directory: one block per dashboard section with exact title, subtitle, metrics + formulas, filter wiring, colors (hex codes from the twb / vis_config / visual.json), tooltip fields and labels, time ranges, number formats, and "as of" date logic. Record the fidelity-policy decision and any preconditions flagged above.
For every section, before telling Hex anything:
dynamic_fields layers (table calcs, custom fields) into it, not reconstructing the query. Power BI branch — this is the expensive phase: translate each DAX measure to warehouse SQL measure by measure (filter context → WHERE/GROUP BY; CALCULATE modifiers → scoped CTEs; time intelligence → date-table joins), validating each against its executeQueries result before composing visuals from them. Never batch-translate: one wrong measure poisons every visual built on it.run/json rows saved in Phase 1; Power BI: the executeQueries rows saved in Phase 1. Match to within rounding.MAX() not SUM();latest_snapshot - N;Use the Hex agent, not manual cell authoring (browser automation into Hex's editors is unreliable and hex project import regenerates all cell IDs).
Target app type: Hex Generative app (App builder → Generative app — a React app Hex renders client-side), NOT a classic notebook+app. Generative apps give pixel-level layout control (custom KPI cards, formula rows, hover tooltips, side-by-side groupings) that classic app builder cells cannot express, and the whole Phase 4 loop (genAppFiles code reads, iframe screenshots) assumes one. hex thread create has no app-type flag — the build prompt's wording is the only control, so the prompt must demand a Generative app explicitly (the template in reference.md §6 opens with this) and Step 4 below verifies Hex complied.
hex thread create --new-project "$(cat prompt.txt)" --json → record thread_id and project URL in the manifest.sleep 250 then hex thread get <thread_id> --json, repeat. Runs take 4–10 minutes. Do not background the poll — backgrounded hex calls lose PATH, and on macOS also Keychain auth.hex project export <project_id> -o app.yaml and check for a non-empty genAppFiles list. If it's missing, Hex built a classic notebook+app — do NOT start Phase 4 on it. Send hex thread continue <thread_id> "You built this as a classic notebook app. Rebuild it as a Generative app (App builder → Generative app): move the entire dashboard into the generative app, keeping the SQL cells as data sources. Do not change any queries." and re-verify after the thread goes IDLE.ERROR: the run died partway. Send hex thread continue <thread_id> "Your previous run errored partway. Verify and complete: <bullet list>" — partial changes usually landed.The loop is driven by the screenshot diff and runs without user involvement:
repeat:
1. screenshot Hex (headless, reference.md §10) + re-download the source PNG (Tableau view-image / Looker render task / Power BI exportToFile-or-Playwright)
2. diff the image pairs panel-by-panel (+ steps 1–3 below: YAML, numbers, filters)
3. zero discrepancies (or only documented trade-offs)? -> exit loop, go to Phase 5
4. send ONE fix batch via `hex thread continue`, wait for the thread to go IDLE
(re-screenshot — never assume a fix landed; verify it in pixels)
hex project export <project_id> -o app.yaml. Grep/py-inspect the YAML for: exact titles and labels, color codes, SQL logic, filter options, tooltip fields, formatting helpers. This is faster and more reliable than browser inspection.{{ param }} only proves it's present, not that it works. Run a functional diff for each one (harness: reference.md §9):
{% if %} blocks, that silently corrupts the query), run both via the warehouse CLI, and assert the output changes in the right direction: a narrower filter returns fewer rows, and every returned row matches the filter. If (a) and (b) are identical, the param is dead — send a fix.{% if param == '...' %} branches, so confirm each option renders a different, valid query (different group-by column or measure) — not just the default branch. Test at least two option values per toggle.hex project run <project_id> -i <param>=<value> --json runs the logic with an overridden input; use it to confirm the app materializes without error under a non-default filter.
Watch for the classic failure: a display-only dropdown that drives nothing, or a panel pointed at a source that lacks the filter's column (it silently won't respond)./views/<luid>/image?resolution=high&maxAge=1); Looker — dashboard render task (reference.md §11); Power BI — exportToFile or Playwright (reference.md §12). Same call as Phase 1's visual-reference step; re-download fresh each round.~/.hex-screenshot-profile); browser per OS: macOS system Chrome via channel="chrome", Linux plain Chromium (reference.md §10). The FIRST time, launch headed and have the user log in once; the session persists, so every later round is fully headless and automatic. (Headless-only host, e.g. remote Linux/CI: do the one-time login on a machine with a display and copy the profile directory over — reference.md §10.) If the app is unpublished, the /app/<id>/latest URL shows a lock screen — open the draft editor URL and click App builder instead. Capture the FULL app as one stitched PNG: scroll the app's inner scroll container inside its iframe in overlapping steps, crop each tile to the iframe (drops the editor chrome), drop the sticky header band from tiles 2+, and stitch — viewport screenfuls are NOT enough (panels hide at screenful boundaries and get mis-flagged as missing; full_page=True and giant viewports both fail on Hex). Multi-tab apps: repeat the capture once per tab (click the tab inside the iframe first), and pair each tab's PNG with its own source dashboard's PNG (each Tableau tab is its own view LUID; each Power BI page exports separately).genAppFiles code read (React App.js, lib/theme.js) is still useful to trace WHICH cell/column a wrong panel reads once the screenshot shows a discrepancy — but it is a debugging aid, not the verification.hex thread continue <thread_id> "<numbered fix list>". Keep each round to one coherent batch; be surgical ("change X to Y", with exact strings/colors/values) and always end with "do not change anything else".resultVariableName is no longer referenced and have the agent delete them.Discrepancy scan (run this every round, then auto-fix what doesn't match). The scan and the parity checklist below say "Tableau" / "worksheet" / "twb" for brevity — read them as your source / its panels (tiles, visuals) / its ground-truth artifact (twb XML, Looker query body + vis_config, PBIR visual.json). Explicitly walk each axis below, list the deltas, and — unless a delta is a deliberate, documented trade-off or covered by the fidelity policy — send the fix to the agent automatically rather than asking the user:
- Layout: is every Tableau worksheet on the dashboard present, in the same section order and side-by-side grouping? (Compare the screenshot pairs; agents commonly DEFER dense panels — cohort triangles, latency matrices — silently.)
- Plotted series — CHART panels (judge ONLY from what is rendered, never from titles/labels/annotations): for EVERY chart, extract these fields from BOTH screenshots and require them to be equal:
1. chart type + marks (line/area/bar, stacking, reference lines, negative handling)
2. x-axis: dimension, range, and tick format (calendar date vs day-of-month vs cohort index vs category)
3. y-axis: measure unit and tick range ($ vs % vs count vs ratio)
4. series: count, grouping/legend, and per-series color
5. curve geometry: monotonic climb / flat oscillation / smoothness / boundedness / resets — same data under a different transform (raw, cumulative, rolling, share-of-total, index, %-change) produces visibly different geometry, and the axes + geometry are the only proof of which one is plotted
6. headline + endpoint values
Titles, legends, and static annotations can all match Tableau while the plotted series is wrong — any field above differing means the panel needs a fix, even if every label agrees.
- Tabular & heatmap panels (tables, crosstabs, highlight tables, cohort triangles) — the parallel checklist:
1. fill scale: stepped vs continuous, number of steps, palette, and direction
2. scale domain and centre (diverging midpoint at 0? at the mean? fixed range?)
3. text-colour rules on filled cells, incl. auto-inversion on dark fills (unreadable text = fail)
4. row/column headers verbatim, and their sort order
5. empty vs null vs zero cell treatment (blank / '-' / 0 render differently and mean different things)
6. shape: cohort triangle vs full rectangle (future/unbaked cells blanked, not zero-filled)
7. all columns visible without horizontal scrolling at app width
8. cell number formats and totals/subtotal rows
- Numbers: re-run each panel's SQL and compare to Tableau; flag any metric-column mismatch, denominator scope, or timezone (UTC vs local) that shifts values.
- Parameters: does every Tableau parameter exist as a Hex input with the same options/default? (Agents drop toggles like dimension selectors and measure switches — rebuild them and wire them, don't hardcode one branch.) Skip if Phase 1 recorded none.
- Filters: is each filter actually wired into EVERY panel Tableau applies it to? Watch for a panel pointed at a source that lacks the filter's dimension (it silently won't respond) — reconcile or document the trade-off. Skip if Phase 1 recorded none.
- Unbaked periods: does the app exclude today / partial days and apply each metric's bake lag exactly as Tableau does?
- Formats & colors: number formats and hex codes match the twb (don't eyeball).
Parity checklist to satisfy before sign-off:
- [ ] Every number matches Tableau (same filters applied) to within rounding
- [ ] Every chart passes the plotted-series checklist (marks, x-axis, y-axis unit, series, curve geometry, endpoints)
- [ ] Every table/heatmap passes the tabular checklist (fill scale, domain/centre, text colour, headers, empty-cell treatment, shape, column fit)
- [ ] All filters/parameters present, same options, same defaults, actually wired into every panel they drive (or "none" recorded in the spec)
- [ ] Every Tableau worksheet is present (no silently deferred panels), same section order/grouping
- [ ] Section titles, subtitles, axis labels, legends match verbatim
- [ ] Colors match (pull exact codes from the twb, don't eyeball)
- [ ] Tooltips exist wherever Tableau has them, same fields and labels
- [ ] Time ranges and "as of" dates match, incl. per-metric bake lags and Exclude-Today
- [ ] Number formats match
- [ ] Layout matches: section order, side-by-side groupings, alignment
- [ ] Multi-tab scope: every tab present, names and order verbatim, each tab diffed against its own source dashboard
- [ ] Readable in both dark and light Hex themes
Done is defined by user acceptance of an evidence pack, not by the agent declaring victory. Assemble and present:
project_id, thread_id (for future fix rounds), and the run-manifest path.Hex tooling
hex thread create/app run can't force it), so it must be in the prompt text, and genAppFiles in the export YAML is the proof it happened (Phase 3 step 4).hex thread continue; work usually partially landed. Export the YAML to see what survived.hex project import regenerates every cell ID — never cache cell IDs across an import; re-list with hex cell list.hex calls lose PATH, and on macOS also Keychain access (auth silently fails).channel="chrome"; on Linux, plain Chromium works (same scroll-and-stitch logic). Hex keeps websockets open so networkidle never fires — use domcontentloaded + poll the canvas/svg count. full_page=True captures only the viewport and a giant viewport renders just a spinner — scroll the iframe's inner container and stitch tiles (handle the sticky header, it overwrites content at stitch boundaries). Unpublished apps show a lock screen on /app/<id>/latest — use the draft editor + App builder tab.Tableau extraction
serverinfo endpoint (reference.md preamble).Looker extraction
run/sql omits the post-SQL layers: table calcs and custom fields (dynamic_fields), pivots, and row totals are applied client-side after the SQL returns. A tile can pass the SQL/number check and still render wrong — always fold dynamic_fields into the Hex SQL (window functions) or chart config, and let the screenshot diff catch what slipped.model::slug — same endpoints, different element addressing. A LookML dashboard's source of truth is a YAML file in the LookML repo; prefer reading it when you have repo access.merge_result_id instead of query_id — extract every source query and replicate the merge as a SQL join./render_tasks/<id> until success → download /results. A 202 from /results means still rendering, not failure.Power BI extraction
executeQueries; a measure that's "roughly right" fails silently in every visual that uses it.report.json where each visual's config is a JSON string inside the JSON — parse twice.visual.json's field bindings, not TMDL.LocalDateTable_* hierarchies make Year/Quarter/Month drill-downs work without a date table — replicate with date functions, don't hunt for a table that doesn't exist.executeQueries under a user enforces row-level security; under a service principal it doesn't. Validate under the same identity whose numbers the user expects, and record which.getDefinition needs report write permission; XMLA/exportToFile need capacity/PPU; executeQueries needs its tenant setting. Hit them in Phase 0, not Phase 4 — and the Playwright screenshot path (§10) is the capacity-free fallback for visual reference.Data semantics
MAX() not SUM() — cross-check magnitudes against Tableau before shipping.An agent skill that migrates a Tableau, Looker, or Power BI dashboard to a Hex app with matching queries, numbers, filters, colors, layout, and tooltips — then proves the match with an automated screenshot-diff loop.
This file is for humans. The agent follows SKILL.md (the procedure) and reference.md (the commands).
Source support:
| Source | Status | Notes |
|---|---|---|
| Tableau (Cloud + self-hosted Server) | Battle-tested | Proven on a production migration |
| Looker (Looker-hosted + customer-hosted) | Documented equivalent | Easier in one way: the API returns the exact warehouse SQL behind every tile |
| Power BI (Fabric / Premium / PPU best) | Documented equivalent, hardest source | Extraction is clean (PBIR + TMDL via APIs), but DAX measures must be translated to warehouse SQL and proven equivalent; import-mode models without a reachable upstream warehouse can't be migrated at all |
| Phase | What happens | Your involvement |
|---|---|---|
| 0. Setup | Installs/verifies the Hex CLI and a warehouse CLI, checks source BI access, asks the fidelity policy, suggests command allowlisting, front-loads the one-time logins, creates a resumable run manifest | Provide source credentials (Tableau PAT / Looker API key / Entra login) if not configured; answer one policy question; do the logins |
| 1. Extract | Pulls the ground truth from the source — Tableau: downloads the workbook (.twbx) and parses its XML for custom SQL, calculated fields, parameters, filters, layout, colors; Looker: fetches the dashboard, per-tile generated SQL, table calcs, filters, and vis config via the API; Power BI: fetches the report definition (PBIR) and semantic model (TMDL: DAX measures, relationships, M sources) via the Fabric APIs — and grabs rendered PNGs | Send screenshots of hover tooltips (they're invisible to every API); confirm the scope — which dashboards/pages to migrate, and whether tabbed siblings become one Hex app with tabs (default) or separate apps |
| 2. Validate | Re-runs every section's logic against the warehouse and reconciles with the source's numbers before building anything (for Power BI this includes the DAX-to-SQL translation) | None (agent asks only if the source is ambiguous) |
| 3. Build | Sends one comprehensive prompt to the Hex agent (hex thread create --new-project) with the validated SQL and exact styling spec, targeting a Generative app (not a classic notebook app, with tab navigation when the scope covers multiple dashboards) and verifying Hex complied |
None — takes ~10–15 min |
| 4. Self-check loop | Screenshots the Hex app headlessly, diffs it panel-by-panel against the source PNG, re-verifies numbers and that every filter actually changes results, sends fix batches to the Hex agent; repeats until parity | One-time browser login for screenshots; expect 3+ rounds |
| 5. Sign-off | Presents an evidence pack: side-by-side images, per-panel parity table, explicit approximations, open questions | You accept or request changes |
The skill activates when you paste a Tableau view link, a Looker dashboard link, or a Power BI report link and ask for a migration. In a chat with your agent (Cursor, Claude Code, Codex, etc.):
Migrate this Tableau dashboard to Hex:
https://<pod>.online.tableau.com/#/site/<site>/views/<Workbook>/<Dashboard>
Migrate this Looker dashboard to Hex:
https://<instance>.cloud.looker.com/dashboards/<id>
Migrate this Power BI report to Hex:
https://app.powerbi.com/groups/<workspace>/reports/<report>/<page>
Phrases like "migrate/replicate/copy/rebuild this dashboard in Hex", "Tableau to Hex", "Looker to Hex", or "Power BI to Hex" all work. You can also invoke it explicitly (e.g. @tableau-to-hex-migration in Cursor).
Copy this folder into your project's skills directory:
.cursor/skills/tableau-to-hex-migration/ (project) or ~/.cursor/skills/... (personal).claude/skills/tableau-to-hex-migration/.codex/skills/tableau-to-hex-migration/ (project) or ~/.codex/skills/... (personal)executeQueries tenant setting enabled; Fabric capacity or Premium-Per-User strongly recommended (needed for TMDL export via API/XMLA and PNG export — without it the skill falls back to browser screenshots). DirectQuery/composite models migrate best; import-only models need their upstream warehouse tables to exist.snow, BigQuery bq, Databricks dbsqlcli, Postgres/Redshift psql (any JSON-emitting SQL CLI can be plugged in).playwright, pillow, pyyaml, jinja2 for the self-check loop.curl, python3, hex, your warehouse CLI) to your agent's command allowlist — in Cursor via the terminal allowlist settings, in Claude Code via /permissions allow rules — and (b) doing all one-time browser logins (Hex CLI, screenshot profile, az login) back-to-back in Phase 0. Ten minutes of setup buys hours of unattended running; only allowlist these specific read/query commands, never destructive ones.~/.tab2hex/<dashboard-slug>/, so an interrupted migration is resumable.