From Tableau → Sigma
Take inventory of a Tableau Cloud site and produce a migration-readiness readout — environment counts, licenses, datasource mix, refresh history, per-workbook usage, per-workbook complexity (via .twb gap-scan), and a value/cost-ranked migration shortlist. Use when a user wants to scope a Tableau→Sigma migration, audit BI sprawl, or pick which workbooks to convert first. Lightweight (~90s) MCP-driven pre-scoping; complements Hakkoda's deeper Assessment App rather than replacing it.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tableau-to-sigma:tableau-assessmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Surveys a Tableau Cloud site via the Tableau Admin Insights project (MCP) and the
PRIVACY.mdREADME.mdfixtures/ds-override-dataflow.jsonrefs/admin-insights-fields.mdrefs/output-shapes.mdrefs/readout-template.mdscripts/aggregate-complexity.rbscripts/analyze-datasources.rbscripts/analyze-users.rbscripts/analyze-views.rbscripts/build-shortlist.rbscripts/fetch-all-twbs.rbscripts/fetch-metadata-graph.rbscripts/get-tableau-token.shscripts/migration-plan.rbscripts/orchestrate-batch.rbscripts/probe-admin-insights.rbscripts/render-readout-html.rbscripts/render-readout.rbscripts/setup-tableau.shSurveys a Tableau Cloud site via the Tableau Admin Insights project (MCP) and the
workbook-content REST endpoint (PAT). Emits a markdown readout + JSON inventory
the user can hand to a Sigma rep, a Hakkoda engagement, or directly to the
tableau-to-sigma skill for conversion of the shortlisted workbooks.
Warehouse-agnostic. This skill (and the downstream
tableau-to-sigmaconversion skill) makes no assumption about which warehouse Sigma is reading from — BigQuery, Databricks, Snowflake, Postgres, SQL Server, Redshift, Synapse, and Oracle are all treated the same way at the Sigma API layer (connections → tables → columns → query). Worked examples in this skill use Snowflake because that's where the dev / audit fixtures live, but the Sigma-side patterns transfer to any supported warehouse. The only warehouse-specific surface is the optional--snowflake-connreconciliation flag onmigration-plan.rb(see "Multi-warehouse considerations" below for the equivalent on other warehouses).
This skill reads workbook metadata, not warehouse data. What crosses Anthropic's API on its way through Claude:
| Crosses Anthropic API | Stays local |
|---|---|
| Aggregate counts (workbook count, user count, datasource counts) | View CSVs (this skill never fetches them) |
| Workbook names, owner emails, project names | Warehouse rows (this skill never queries them) |
User License Type and login dates from Admin Insights | Customer database credentials |
| Refresh job results, durations, error messages | The customer's actual reports' values |
.twb XML for each workbook (calc-field definitions, custom SQL, layout) | .hyper extract data files (skipped on download) |
This is a weaker posture than Hakkoda's "stays in Snowflake" app. Hakkoda's Snowflake Native App keeps everything inside the customer's Snowflake account. This skill — like every other Claude Code skill — sends what it reads through the Anthropic API to Claude. The user should be told this before running.
The skill writes outputs to a local directory (/tmp/assessment-<sitename>/ by
default) and does NOT upload them anywhere. If the customer wants the readout
shared with a Sigma rep, that's a deliberate Share action, not automatic.
See PRIVACY.md for the full disclosure to share with customer privacy/legal review.
tableau-to-sigma invocation needs a Phase 0 inventory of the source siteNot for: Replacing Hakkoda's full Assessment App readout (pricing scenarios, permissions audit, dataset similarity at depth). Those still live in Hakkoda.
| Script | Purpose |
|---|---|
scripts/setup-tableau.sh | Symlink to the tableau-to-sigma PAT setup wizard |
scripts/get-tableau-token.sh | Symlink to the tableau-to-sigma token-refresh wrapper |
scripts/probe-admin-insights.rb | Confirm the Admin Insights project is visible (gates whether license/refresh/usage sections run) |
scripts/fetch-all-twbs.rb | Parallel download of all workbook .twb files via REST (PAT mode only) |
scripts/aggregate-complexity.rb | Run scan-workbook-gaps.rb (from tableau-to-sigma) against every .twb; emit complexity.json |
scripts/build-shortlist.rb | Cross-tabulate usage × complexity; rank by value / (1 + cost); emit shortlist.json |
scripts/render-readout.rb | Compose final readout.md from inventory.json + complexity.json + shortlist.json |
scripts/migration-plan.rb | Phase 6: combine shortlist + data-sources + .twb warehouse-table extraction into migration-plan.json with per-workbook recommended_path (tableau-to-sigma / vds-to-snowflake / retire / blocked), DM clusters (Jaccard ≥ 0.5 on shared warehouse tables + fact-table overlap), and a suggested first batch. Input contract for the conversion handoff. |
scripts/orchestrate-batch.rb | Phase 7 (optional): produce a batch-plan.json with wave-style scheduling for parallel tableau-to-sigma subagent execution. Cluster leaders run first to build/pick their DM; followers reuse via find-or-pick-dm.rb + inspect-dm-shape.rb. Continue-on-failure. Outputs ready-to-fire agent_brief strings for the conversation-layer to pass into Agent() calls. |
Scripts that need warehouse-table data (the MCP query-datasource calls against Admin Insights) are NOT scripts — the agent fires those directly per the recipes in this SKILL.md, because MCP tool calls only work from the agent's context.
| Mode | Setup | Coverage | Use when |
|---|---|---|---|
| MCP-only | None — just Tableau MCP loaded as Site Admin | Environment + Licenses + Datasource mix + Refresh + Usage | Quick pre-scope; customer hasn't issued a PAT |
| MCP + PAT (recommended) | ruby scripts/setup-tableau.sh once (~30s) | Adds per-workbook complexity scan + ranked migration shortlist | Real migration planning; full readout |
The user driving the skill MUST have Site Admin role in Tableau — Admin Insights is only published to that group by default. The skill probes this and surfaces a clear error if Admin Insights isn't visible.
Confirm the user has the access the skill needs. Two checks, in order:
# 0a. Tableau MCP loaded? Try a cheap call:
mcp__tableau__list-workbooks limit=1
# If "tool not found" → MCP isn't loaded. Skill cannot run.
# If 401/403 → user signed in but lacks site-level read access.
# 0b. Admin Insights visible?
ruby scripts/probe-admin-insights.rb
# Calls mcp__tableau__search-content for "Admin Insights"; reports which of the
# 10 expected datasources are reachable. Exits 1 if zero are reachable (user is
# not a Site Admin → can run only the Section 1 inventory below).
If only the basic inventory runs (Section 1 below), surface a banner in the readout:
"Run this as a Tableau Site Admin to unlock license, refresh, and usage sections. Currently running with limited access."
Every TS Events / Site Content query in this skill applies two default filters so the readout reflects current relevance, not lifetime noise:
| Filter | Default | Why | How to override |
|---|---|---|---|
| Usage window | Event Date >= today - 90 days on every TS Events query | Tableau Cloud's TS Events Admin Insights datasource only retains ~90 days anyway, but the filter makes the window explicit in the readout and lets you tighten it to 30 days for pilot-picking. Without an explicit Event Date filter, "all time" silently means whatever the customer's site retention is. | Set USAGE_DAYS=30 (or any positive int) in env before running the agent's Admin Insights queries; surface in the readout header as "Usage window: last N days". |
| Personal Space exclusion | Top Parent Project Name != "Personal Space" on Site Content workbook/datasource queries | Tableau Cloud's per-user sandbox project is full of one-off / draft / never-shared workbooks. On a 793-dashboard site, this often hides ~30-50% of the count. | Set INCLUDE_PERSONAL=1 to keep them in the inventory. |
There is no
Is Archivedfield on Tableau Cloud's Admin Insights Site Content datasource — verified againstread-metadataon a live site. The Tableau Cloud REST/workbooksendpoint already filters out truly archived /deleted workbooks server-side, so no client filter is needed for that. The "archived" concept the customer might mean is usually either (a) personal sandbox content (handled by the Personal Space exclusion above) or (b) workbooks moved to a project the customer calls "Archive" / "Old" / "Retired" — add those project names to--exclude-projectsif surfaced.
Compute the relative date in the orchestration shell, e.g.
MIN_DATE=$(date -v-90d +%Y-%m-%d) (BSD/macOS) or
MIN_DATE=$(date -d '90 days ago' +%Y-%m-%d) (GNU/Linux), then substitute into
the QUANTITATIVE_DATE filter shape shown in the queries below.
Even without Admin Insights, the skill can produce a basic environment overview
from mcp__tableau__search-content and mcp__tableau__list-workbooks. This is
the "even broken access still produces something" floor.
mcp__tableau__list-workbooks # → workbook count + sheetCount/hasExtracts per workbook
mcp__tableau__search-content filter.contentTypes=["datasource"] # → datasource count
mcp__tableau__list-views # → view count
Write the rolled-up counts to inventory.json's environment_overview key.
For each of the queries below, call mcp__tableau__query-datasource with the
listed datasourceLuid and query payload. Run queries sequentially, not in
parallel — VizQL session contention causes 401s under fan-out.
Field names matter — Admin Insights field naming is inconsistent and a typo
silently fails. See refs/admin-insights-fields.md for the verified field-name
cheat sheet. Critical: it's Event Id, not Event LUID.
inventory.environment_overview{
"datasourceLuid": "<Site Content LUID>",
"query": { "fields": [
{ "fieldCaption": "Item Type" },
{ "fieldCaption": "Item LUID", "function": "COUNTD", "fieldAlias": "n" }
]}
}
inventory.licenses{
"datasourceLuid": "<TS Users LUID>",
"query": { "fields": [
{ "fieldCaption": "User License Type" },
{ "fieldCaption": "User Site Role" },
{ "fieldCaption": "User LUID", "function": "COUNTD", "fieldAlias": "users" },
{ "fieldCaption": "Days Since Last Login", "function": "AVG", "fieldAlias": "avg_days_since_login" }
]}
}
inventory.content_ownership{
"datasourceLuid": "<Site Content LUID>",
"query": { "fields": [
{ "fieldCaption": "Item Type" },
{ "fieldCaption": "Owner Email" },
{ "fieldCaption": "Item LUID", "function": "COUNTD", "fieldAlias": "n" }
], "filters": [{
"field": { "fieldCaption": "Item Type" },
"filterType": "SET",
"values": ["Workbook", "Datasource", "Flow", "View"]
}]}
}
inventory.datasource_types{
"datasourceLuid": "<Site Content LUID>",
"query": { "fields": [
{ "fieldCaption": "Data Source Content Type" },
{ "fieldCaption": "Data Source Database Type" },
{ "fieldCaption": "Is Data Extract" },
{ "fieldCaption": "Item LUID", "function": "COUNTD", "fieldAlias": "n" }
], "filters": [{
"field": { "fieldCaption": "Item Type" }, "filterType": "SET", "values": ["Datasource"]
}]}
}
inventory.refresh_jobs{
"datasourceLuid": "<Job Performance LUID>",
"query": { "fields": [
{ "fieldCaption": "Job Type" },
{ "fieldCaption": "Final Job Result" },
{ "fieldCaption": "Job ID", "function": "COUNTD", "fieldAlias": "jobs" },
{ "fieldCaption": "Job Duration", "function": "AVG", "fieldAlias": "avg_duration_s" }
]}
}
inventory.workbook_usage{
"datasourceLuid": "<TS Events LUID>",
"query": { "fields": [
{ "fieldCaption": "Workbook Name" },
{ "fieldCaption": "Number of Events", "function": "SUM", "fieldAlias": "accesses", "sortDirection": "DESC", "sortPriority": 1 },
{ "fieldCaption": "Count of Distinct Actors", "fieldAlias": "actors" }
], "filters": [
{ "field": { "fieldCaption": "Event Type" }, "filterType": "SET", "values": ["Access"] },
{ "field": { "fieldCaption": "Item Type" }, "filterType": "SET", "values": ["View", "Workbook"] },
{ "field": { "fieldCaption": "Event Date" }, "filterType": "QUANTITATIVE_DATE", "quantitativeFilterType": "MIN", "minDate": "<today minus USAGE_DAYS (default 90)>" }
]}
}
inventory.workbook_inventory{
"datasourceLuid": "<Site Content LUID>",
"query": { "fields": [
{ "fieldCaption": "Item Name" },
{ "fieldCaption": "Owner Email" },
{ "fieldCaption": "Top Parent Project Name" },
{ "fieldCaption": "Size (MB)", "function": "SUM", "fieldAlias": "size_mb" },
{ "fieldCaption": "Last Accessed At" },
{ "fieldCaption": "Is Data Extract" },
{ "fieldCaption": "Has Refresh Scheduled" },
{ "fieldCaption": "Item Hyperlink" }
], "filters": [
{ "field": { "fieldCaption": "Item Type" }, "filterType": "SET", "values": ["Workbook"] },
{ "field": { "fieldCaption": "Top Parent Project Name" }, "filterType": "SET", "exclude": true, "values": ["Personal Space"] }
]}
}
Drop the
Top Parent Project Nameexclusion filter ifINCLUDE_PERSONAL=1. Add additional project names to thevaluesarray if the customer has custom "Archive" / "Retired" projects to skip.
Merge the seven outputs into <out>/inventory.json following the schema in
refs/output-shapes.md.
users.json (after running analyze-users.rb)For user-population segmentation and per-user migration coverage:
{
"datasourceLuid": "<TS Users LUID>",
"query": { "fields": [
{ "fieldCaption": "User Email" },
{ "fieldCaption": "User License Type" },
{ "fieldCaption": "User Site Role" },
{ "fieldCaption": "Days Since Last Login", "function": "MAX", "fieldAlias": "days_since" },
{ "fieldCaption": "Workbooks", "function": "SUM", "fieldAlias": "owned_wb" },
{ "fieldCaption": "Views", "function": "SUM", "fieldAlias": "owned_views" },
{ "fieldCaption": "Total Traffic - Views", "function": "SUM", "fieldAlias": "traffic_views" },
{ "fieldCaption": "Access Events - Views", "function": "SUM", "fieldAlias": "access_views" },
{ "fieldCaption": "Last Login Date" }
]}
}
Save the response under <out>/raw-ts-users.json.
Then the per-user-per-workbook access map (used to compute migration coverage):
{
"datasourceLuid": "<TS Events LUID>",
"query": { "fields": [
{ "fieldCaption": "Actor User Name" },
{ "fieldCaption": "Workbook Name" },
{ "fieldCaption": "Number of Events", "function": "SUM", "fieldAlias": "accesses" }
], "filters": [
{ "field": { "fieldCaption": "Event Type" }, "filterType": "SET", "values": ["Access"] },
{ "field": { "fieldCaption": "Item Type" }, "filterType": "SET", "values": ["View", "Workbook"] },
{ "field": { "fieldCaption": "Event Date" }, "filterType": "QUANTITATIVE_DATE", "quantitativeFilterType": "MIN", "minDate": "<today minus USAGE_DAYS (default 90)>" }
]}
}
Save the response under <out>/raw-ts-events-per-user.json.
This is the section that differentiates the skill from Hakkoda. Hakkoda owns assessment but not conversion; this skill ties them together by predicting per-workbook conversion cost.
ruby scripts/setup-tableau.sh # one-time, prompts for PAT name + secret
eval "$(scripts/get-tableau-token.sh)" # refreshes ~hourly auth token
ruby scripts/fetch-all-twbs.rb --out /tmp/assessment-<site>
fetch-all-twbs.rb lists every workbook via REST, downloads .twb content in
parallel (12-thread default), and unzips any .twbx to extract the inner .twb.
For large sites (500+ workbooks) the script is built to handle the long-run failure modes:
<out>/twbs/ are skipped, so a
failed or interrupted run can just re-invoke the same command.--refresh-min N), and every request retries once after refreshing on a
401. Long runs (1000+ workbooks, multi-hour) survive Tableau Cloud session
timeout without manual re-auth.[N/total] R wb/s eta M minutes.Tuning flags:
ruby scripts/fetch-all-twbs.rb --out /tmp/assessment-<site> \
--threads 12 \ # raise to 16-24 if customer's Tableau Cloud is fast and not throttling
--refresh-min 60 \ # lower to 30 if customer site has strict session policy
--limit 50 # for a sanity pass before fetching the whole site
Expected throughput on Tableau Cloud 10ay.online.tableau.com: ~300 wb/min
on small workbooks, ~60-120 wb/min on a mixed corpus with several 5MB+
.twbx files. If you measure < 30 wb/min on a customer's site, suspect
network latency or large embedded extracts; lower threads to avoid 429s
rather than raising them.
ruby scripts/aggregate-complexity.rb /tmp/assessment-<site>
Iterates <out>/twbs/*.twb, runs tableau-to-sigma/scripts/scan-workbook-gaps.rb
on each, parses each <luid>-gaps-report.json, aggregates feature counts in
four buckets (auto / hint / manual / unhandled) per workbook. Writes
complexity.json.
ruby scripts/build-shortlist.rb /tmp/assessment-<site>
Cross-tabulates inventory.workbook_usage with complexity.json. Scores each
workbook:
value = accesses × √(distinct_viewers)cost = 10·unhandled + 3·manual + 1·hintscore = value / (1 + cost)Writes shortlist.json — ranked by score, with explicit "retire" tags on
zero-access workbooks and explicit "needs gap-scout" flags on workbooks with
unhandled features.
The Tableau Metadata API exposes the full lineage graph — workbooks, embedded / published datasources, connection hostnames, custom SQL queries, Prep flows — in a single GraphQL call. This unlocks the prescriptive data-source analysis (red flags, similarity clusters, Sigma-readiness verdicts).
eval "$(scripts/get-tableau-token.sh)"
ruby scripts/fetch-metadata-graph.rb --out /tmp/assessment-<site>
One POST to /api/metadata/graphql. Writes metadata-graph.json (~100–250 KB
for most sites; can grow to several MB for sites with thousands of calc fields).
Requires Site Admin role.
Per-workbook calc-field formulas live here. The
embeddedDatasources.fieldsblock now includes theformula,isHidden,role,dataType, andaggregationof everyCalculatedField— added 2026-05-26 so downstream conversion (tableau-to-sigma/scripts/extract-calc-fields.rb) can read calc formulas straight from the assessment dump without re-querying Tableau. This replaces the older VDS-based calc discovery, which fails on sites where VDS is disabled.
ruby scripts/analyze-datasources.rb --out /tmp/assessment-<site>
For each data source (published + embedded), classifies:
| Verdict | Means |
|---|---|
drop-in | Cloud warehouse natively supported by Sigma; connect directly |
verify-network | Cloud type on an unrecognized host, or detected on-prem hostname; confirm Sigma can reach |
verify-db | Database supported via a Sigma connector that may need extra config |
verify-modeling | Federated cross-source join; review Sigma data-model relationship coverage |
resolve-published | References another published datasource; resolve recursively |
land-in-warehouse | File-based (Excel / CSV / Google Drive / .hyper); needs warehouse upload first. Recommended path: use the sibling tableau-vds-to-snowflake skill to auto-generate Snowflake DDL + Sigma data model from the .tds. |
Also emits:
Connection-type → verdict mapping is in analyze-datasources.rb's constants block. Update when Sigma adds a new connector.
ruby scripts/analyze-users.rb --out /tmp/assessment-<site>
Reads raw-ts-users.json + raw-ts-events-per-user.json (Phase 2h) + shortlist.json.
Segments users into power-user / active-creator / heavy-consumer / casual / light /
dormant / never-logged-in buckets. For each user, computes pilot-migration coverage
— what percent of their actual workbook accesses are covered by the top-5 pilot.
Bucket thresholds and segment definitions are in analyze-users.rb's
segment_for function — tune for the customer's site size.
ruby scripts/render-readout.rb /tmp/assessment-<site>
Composes the 12-section markdown report (template at refs/readout-template.md).
Sections covered:
Deliverables in /tmp/assessment-<site>/:
readout.md — customer-facing markdowninventory.json — raw Admin Insights aggregatescomplexity.json — per-workbook gap counts (PAT mode)shortlist.json — ranked migration shortlist (PAT mode)twbs/ — cached .twb files (PAT mode; can be deleted after rendering)After render-readout.rb finishes, always run:
ruby scripts/migration-plan.rb --out /tmp/assessment-<site>
This composes migration-plan.json from shortlist.json, data-sources.json, and the cached .twbs. Each workbook gets a recommended_path:
recommended_path | What it means |
|---|---|
tableau-to-sigma | Ready for conversion. ≤5 manual/unhandled features, score > 0. |
tableau-to-sigma-with-scout | Needs gap-scout subagent runs for unhandled calc fields first. |
vds-to-snowflake | Datasource (not workbook) flagged as land-in-warehouse or red-flag — best to materialize in Snowflake first, then convert the workbook on top. |
retire | No usage (accesses=0); recommend not migrating. |
blocked | >5 manual/unhandled features; needs human rework before automation can help. |
The plan also computes DM clusters — workbooks that share warehouse tables (Jaccard ≥ 0.5 + at least one shared *_FACT-shaped table). The bulk-conversion orchestrator uses these to share a single Sigma data model across a cluster's workbooks instead of building N redundant DMs.
After Phase 6, the assessment agent MUST present a AskUserQuestion menu so the user picks the next step. Do NOT silently end the assessment — the user is here to migrate something, surface the choice. Build the menu dynamically from migration-plan.json's summary:
Assessment summary:
• N workbooks total, M ready for conversion (score-ranked, top 8 below)
• K datasources flagged for VDS→Snowflake first
• C DM clusters detected (workbooks sharing warehouse tables)
What next?
→ Migrate top N dashboards in parallel [tableau-to-sigma × N subagents]
→ Migrate one specific dashboard [pick from list]
→ Land Tableau datasources in Snowflake first [tableau-vds-to-snowflake]
→ Do both: VDS first, then dashboards [chained]
→ Just write the readout — act later
Use the AskUserQuestion tool to render this. Each option dispatches differently:
User picks one workbook. Invoke the conversion skill in the same conversation (not a subagent — agent stays in the assessment thread):
Skill(
skill: "tableau-to-sigma",
args: "Convert workbook <luid> (<name>) from the just-finished assessment at /tmp/assessment-<site>. Read /tmp/assessment-<site>/migration-plan.json for the recommended_path, blockers, and warehouse_tables for this workbook. Use the cluster's denorm plan if one exists."
)
Where to run this: Option B requires the
Agent()tool, which is only available to a top-level interactive Claude Code session. Subagents in a nested context (i.e., when this assessment itself is being driven by a parentAgent()call) cannot themselves spawn furtherAgent()calls — they only haveBash + run_in_background. If you're nested, do NOT attempt Option B from within; surface the batch-plan to the parent session and let it drive the wave fan-out.
ruby scripts/orchestrate-batch.rb \
--plan /tmp/assessment-<site>/migration-plan.json \
--out /tmp/assessment-<site>/batch \
--concurrent 3 \
--limit 8
This emits batch-plan.json with wave-by-wave subagent briefs. The conversation-layer agent then:
--concurrent parallel Agent() calls. Each Agent() gets subagent_type: "general-purpose" and the agent_brief string from the plan as its prompt. Set run_in_background: true on all of them — agents in a wave run truly in parallel and the conversation-layer waits for completion notifications.ruby /tmp/assessment-<site>/batch/aggregate-results.rb to show the running tally and surface YELLOW (review-needed) and RED (failed) results immediately.Mid-batch progress depends on Agent completion notifications, not stdout streaming. The aggregator only sees completed subagent result lines in
batch-results.jsonl— there's no in-flight "X% done" indicator. Use the completion notifications themselves as the progress signal.
Cluster-aware execution: a cluster's leader subagent runs first (alone or with other clusters' leaders in parallel) so it can build/pick the DM. Followers run in the next wave reusing the leader's DM via find-or-pick-dm.rb + inspect-dm-shape.rb. Within a cluster, leaders never run in the same wave as their own followers. The orchestrator handles this ordering.
Parity tiers (continue-on-failure):
verify-workbook.rb clean), all chart actuals strict-PASS. Ready to publish.User wants to land Tableau-managed datasources into Snowflake before dashboards. Invoke:
Skill(
skill: "tableau-vds-to-snowflake",
args: "Land these <K> datasources from /tmp/assessment-<site>/migration-plan.json (each flagged recommended_path=vds-to-snowflake) into Snowflake. After completion, workbooks that source from these datasources become candidates for tableau-to-sigma conversion."
)
Run Option C, then on completion run Option B. The orchestrator picks workbooks where the source datasources now exist as Snowflake tables.
End the assessment. User will pick this up later.
Sigma reads from many warehouses. The Tableau-side discovery in this skill is warehouse-neutral (Tableau Cloud's Admin Insights doesn't care where the underlying warehouse lives). The Sigma-side reconciliation and downstream conversion path can be steered per warehouse:
| Stage | Snowflake | BigQuery | Databricks | Postgres / SQL Server / Redshift |
|---|---|---|---|---|
Already-landed-table check (migration-plan.rb) | --snowflake-conn <name> shells out to snow sql --connection ... against INFORMATION_SCHEMA.TABLES. | Use --warehouse-cli bq (see "Extending the warehouse CLI" below) — run bq query --use_legacy_sql=false 'SELECT table_name FROM <proj>.<ds>.INFORMATION_SCHEMA.TABLES'. | Use --warehouse-cli databricks — databricks sql query against information_schema.tables. | --warehouse-cli psql / sqlcmd / psql against information_schema.tables (Postgres-shaped — Redshift uses pg_table_def). |
| Column discovery for DM build | mcp__sigma-mcp-v2__describe on a connection table, OR scripts/discover-warehouse-columns.rb (Sigma REST). Both warehouse-agnostic. | Same — Sigma's /v2/connections/tables/<inodeId>/columns works the same. | Same. | Same. |
recommended_path: vds-to-snowflake value | Default: assumes Snowflake landing. | Substitute "BigQuery" / "Databricks" / etc. in the customer-facing readout. The internal token can stay vds-to-snowflake for now (renaming touches downstream consumers); prefer a customer-friendly target_warehouse field in migration-plan.json next iteration. | Same. | Same. |
| Custom SQL DM elements | Snowflake dialect by default — UPPERCASE aliases match Snowflake identifier casing. | BigQuery: use backticked names, watch for case sensitivity (it's case-sensitive on table names but not on column names by default). | Databricks: lowercase identifiers; quote with backticks. | Postgres / Redshift: lowercase identifiers by default; quote with double quotes. |
migration-plan.rb's fetch_landed_tables(snow_conn, target_schema) shells
out to snow sql to enumerate already-landed tables. To support other
warehouses, follow the same shape:
def fetch_landed_tables_bq(project, dataset)
q = "SELECT table_name FROM `#{project}.#{dataset}.INFORMATION_SCHEMA.TABLES`"
out = `bq query --use_legacy_sql=false --format=json #{q.shellescape}`
return Set.new unless $?.success?
JSON.parse(out).map { |r| r['table_name'].to_s.upcase }.to_set
rescue StandardError
Set.new
end
The function contract is: return a Set<String> of bare table names in
uppercase. Drop it in as a new branch off --warehouse-cli and the
downstream recommended_path: vds-already-landed reconciliation works
unchanged.
Every worked example in this SKILL.md (the TJ.PUBLIC.* fixture tables,
the snow sql reconciliation, the --snowflake-conn flag, the
tableau-vds-to-snowflake sibling skill) uses Snowflake because that's
where the development corpus and audit-run fixtures live. The Sigma-side
calls (/v2/connections, /v2/connections/tables/<inodeId>/columns,
mcp__sigma-mcp-v2__query) are warehouse-agnostic, so a customer running
the same assessment against a BigQuery / Databricks / Postgres Tableau
deployment gets the same readout structure.
The conversion skill's Phase 0a (scan-workbook-gaps.rb) produces the same gap-counts this assessment already cached in complexity.json. When invoking tableau-to-sigma from this assessment's handoff, point it at the assessment dir — the converter can skip re-scanning.
| Symptom | Cause | Fix |
|---|---|---|
probe-admin-insights.rb finds 0 datasources | User is not Site Admin | Surface limited-mode banner; skip sections 2–3 |
MCP get-datasource-metadata returns 401 in batch | VizQL session contention | Serialize Admin Insights metadata calls (one at a time) |
query-datasource returns Field 'X' not found | Field-name typo (e.g., Event LUID instead of Event Id) | Check refs/admin-insights-fields.md |
fetch-all-twbs.rb 302 redirect on /workbooks | Missing Tableau.base_path prefix | Use Tableau.base_path + "/workbooks", not /workbooks alone |
| Tableau PAT 4 consecutive signin failures | Tableau Cloud invalidates the PAT | Mint a new PAT in Tableau settings; re-run setup |
aggregate-complexity.rb skips .twbx workbooks | Inner .twb not unzipped | fetch-all-twbs.rb unzips automatically; rerun the fetch step |
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub twells89/sigma-migration-skills --plugin tableau-to-sigma