Install
$ agentstack add skill-twells89-sigma-migration-skills-tableau-assessment ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Tableau Assessment
Surveys a Tableau Cloud site via the Tableau Admin Insights project (MCP) and the workbook-content REST endpoint (PAT) — or a self-hosted Tableau Server site via REST only (inventory-rest.rb; no Admin Insights, so no usage/refresh stats). 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-sigma > conversion 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-conn reconciliation > flag on migration-plan.rb (see "Multi-warehouse considerations" below for > the equivalent on other warehouses).
Privacy posture (READ FIRST, surface to the customer)
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-/ 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.
When to use this skill
- A Tableau customer wants a 5-minute scoping view before booking a Hakkoda 1-hour assessment
- A Sigma SE preparing for a discovery call wants a pre-built migration shortlist
- A customer is deciding which Tableau workbooks to retire vs. migrate
- A
tableau-to-sigmainvocation needs a Phase 0 inventory of the source site
Not for: Replacing Hakkoda's full Assessment App readout (pricing scenarios, permissions audit, dataset similarity at depth). Those still live in Hakkoda.
Scripts overview
| 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/inventory-rest.rb | Tableau Server fallback for Phases 1–2: build inventory.json (mode: rest-server, counts + per-workbook sheet_count, usage_available: false) from the plain REST workbooks/views/datasources endpoints. No usage/refresh (needs Admin Insights or the Server repository DB). | | 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. Also derives a predicted-parity % + A–D band per workbook (y9rd.6): an occurrence-weighted roll-up of the gap-scanner's status tiers (auto=1.0, hint=0.85, manual=0.5, unhandled=0.0) — a pre-migration prediction from static .twb signal, NOT the post-migration measured value-parity score (verify-parity.rb). Flags hard workbooks (band C/D) before any conversion. | | scripts/build-shortlist.rb | Cross-tabulate usage × complexity; rank by value / (1 + cost); emit shortlist.json (carries predicted_parity_pct + parity_band) | | 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/consolidation-candidates.rb | Phase 6b: within each shared-datasource pool, score workbook-variant similarity (actually-used fields, sheet/zone structure, filter sets, gap-scan feature profile, name-stem heuristics) and emit consolidation-candidates.json — groups of workbooks that could collapse into ONE Sigma workbook + a control. --decide records the user's per-group choice into migration-plan.json. Pure analysis; no Tableau/Sigma writes. | | 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.
Modes
| 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.
Phase 0 — Probe access
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."
Scope filters — usage window + personal sandbox exclusion
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 Archived field on Tableau Cloud's Admin Insights Site > Content datasource — verified against read-metadata on a live site. The > Tableau Cloud REST /workbooks endpoint 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-projects if 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.
Phase 1 — Environment inventory (MCP, always runs)
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.
Self-hosted Tableau Server (no MCP / no Admin Insights)
Tableau Server has no Admin Insights project (that's a Cloud feature) and the hosted Tableau MCP targets Cloud, so Phases 1–2 above don't apply. Instead build the inventory from the standard REST endpoints with a PAT:
eval "$(scripts/get-tableau-token.sh)" # negotiates the Server's REST API version
ruby scripts/inventory-rest.rb --out /tmp/assessment-
This writes inventory.json (mode: "rest-server") with workbook/datasource/view counts, a per-workbook sheet_count, and usage_available: false. Usage, license, and refresh history are NOT available over plain REST — they require Admin Insights (Cloud) or the Tableau Server repository (the "workgroup" Postgres DB an admin must enable). Phase 3 complexity (fetch-all-twbs.rb + scan-workbook-gaps.rb) is already REST/PAT and runs unchanged. build-shortlist.rb detects usage_available: false and ranks by complexity (easiest first) — it will not tag zero-usage workbooks retire, since on Server a missing access count is not evidence of disuse.
Phase 2 — Admin Insights queries (MCP, requires Site Admin)
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.
2a. Site-content item counts → inventory.environment_overview
{
"datasourceLuid": "",
"query": { "fields": [
{ "fieldCaption": "Item Type" },
{ "fieldCaption": "Item LUID", "function": "COUNTD", "fieldAlias": "n" }
]}
}
2b. License breakdown → inventory.licenses
{
"datasourceLuid": "",
"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" }
]}
}
2c. Content ownership → inventory.content_ownership
{
"datasourceLuid": "",
"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"]
}]}
}
2d. Datasource types + extract mix → inventory.datasource_types
{
"datasourceLuid": "",
"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"]
}]}
}
2e. Refresh history → inventory.refresh_jobs
{
"datasourceLuid": "",
"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" }
]}
}
2f. Workbook usage ranking → inventory.workbook_usage
{
"datasourceLuid": "",
"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": "" }
]}
}
2g. Workbook inventory (size, owner, last accessed, hyperlink) → inventory.workbook_inventory
{
"datasourceLuid": "",
"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 Name exclusion filter if INCLUDE_PERSONAL=1. > Add additional project names to the values array if the customer has > custom "Archive" / "Retired" projects to skip.
Merge the seven outputs into /inventory.json following the schema in refs/output-shapes.md.
2h. Per-user usage map → users.json (after running analyze-users.rb)
For user-population segmentation and per-user migration coverage:
{
"datasourceLuid": "",
"query": { "fields": [
{ "fieldCaption": "User Email" },
{ "fieldCaption": "User License Type" },
{ "fieldC
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [twells89](https://github.com/twells89)
- **Source:** [twells89/sigma-migration-skills](https://github.com/twells89/sigma-migration-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.