tableau-assessment

star 5

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.

sigmacomputing By sigmacomputing schedule Updated 6/2/2026

name: tableau-assessment description: 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.

Tableau Assessment

Surveys 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-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-<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.


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-sigma invocation 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/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.


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.


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": "<Site Content LUID>",
  "query": { "fields": [
    { "fieldCaption": "Item Type" },
    { "fieldCaption": "Item LUID", "function": "COUNTD", "fieldAlias": "n" }
  ]}
}

2b. License breakdown → 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" }
  ]}
}

2c. Content ownership → 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"]
  }]}
}

2d. Datasource types + extract mix → 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"]
  }]}
}

2e. Refresh history → 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" }
  ]}
}

2f. Workbook usage ranking → 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)>" }
  ]}
}

2g. Workbook inventory (size, owner, last accessed, hyperlink) → 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 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 <out>/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": "<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.


Phase 3 — Per-workbook complexity (PAT, optional but recommended)

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.

3a. Auth + fetch

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:

  • Resumable. Files already on disk in <out>/twbs/ are skipped, so a failed or interrupted run can just re-invoke the same command.
  • Token auto-refresh. Background thread re-signs in every 60 min (--refresh-min N), and every request retries once after refreshing on a
    1. Long runs (1000+ workbooks, multi-hour) survive Tableau Cloud session timeout without manual re-auth.
  • Persistent HTTPS per worker — measured ~2× speedup vs. the previous fresh-connection-per-request approach.
  • Adaptive backoff on 429 / 502 / 503 / 504, up to 4 retries with exponential delay capped at 30 s.
  • Live ETA logged every 10 workbooks: [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.

3b. Run the gap-scanner against each workbook

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.

3c. Build the migration shortlist

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·hint
  • score = 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.


Phase 4 — Site-wide lineage via Metadata API (PAT, optional but recommended)

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).

4a. Fetch the site-wide metadata graph

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.fields block now includes the formula, isHidden, role, dataType, and aggregation of every CalculatedField — 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.

4b. Analyze data sources

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:

  • Similarity clusters — embedded datasources whose field-name sets overlap by ≥75% (Jaccard). Strong consolidation candidates.
  • Custom SQL inventory — every Custom SQL block on the site with its downstream workbooks.
  • Prep flow inventory — flow → downstream-datasource/workbook lineage, orphan detection.

Connection-type → verdict mapping is in analyze-datasources.rb's constants block. Update when Sigma adds a new connector.

4c. Analyze user populations

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.


Phase 5 — Render the readout

ruby scripts/render-readout.rb /tmp/assessment-<site>

Composes the 12-section markdown report (template at refs/readout-template.md). Sections covered:

  1. Environment overview
  2. Licenses & cost scenario
  3. Content ownership
  4. Datasource patterns
  5. Refresh insights
  6. Workbook priority — usage-ranked
  7. Migration shortlist (PAT-mode only — falls back to usage-only if MCP-only)
  8. PAT-mode addendum: per-workbook complexity (PAT-mode only)
  9. What the skill found vs. what it didn't
  10. Privacy disclosure (links to PRIVACY.md)
  11. Hand-off package contents
  12. Next steps

Deliverables in /tmp/assessment-<site>/:

  • readout.md — customer-facing markdown
  • inventory.json — raw Admin Insights aggregates
  • complexity.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)

Phase 6 — Build the migration plan

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.


Phase 7 — Hand off to the next skill (MANDATORY: ask the user)

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:

Option A — Single dashboard

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."
)

Option B — Bulk parallel migration

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 parent Agent() call) cannot themselves spawn further Agent() calls — they only have Bash + 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:

  1. For each wave in order, batch its subagents into messages of --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.
  2. After every wave completes, run ruby /tmp/assessment-<site>/batch/aggregate-results.rb to show the running tally and surface YELLOW (review-needed) and RED (failed) results immediately.
  3. Final aggregation prints the GREEN / YELLOW / RED breakdown and per-workbook Sigma URLs.

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):

  • GREEN — workbook posted clean (0 column-errors, verify-workbook.rb clean), all chart actuals strict-PASS. Ready to publish.
  • YELLOW — workbook posted clean BUT one or more charts diverge in values. Structural conversion succeeded; review numbers before stakeholder.
  • RED — column-type errors, POST failure, verify failure, or no actuals fetchable. Auto-files a beads ticket; batch continues.

Option C — VDS to Snowflake first

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."
)

Option D — Both (chained)

Run Option C, then on completion run Option B. The orchestrator picks workbooks where the source datasources now exist as Snowflake tables.

Option E — Just write the readout

End the assessment. User will pick this up later.


Multi-warehouse considerations

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 databricksdatabricks 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.

Extending the warehouse CLI

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.

Snowflake-flavored examples

Every worked example in this SKILL.md (the MYDB.MYSCHEMA.* 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.


Pre-Phase-6 enhancement: complexity reuse

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.


Troubleshooting

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
Install via CLI
npx skills add https://github.com/sigmacomputing/quickstarts-public --skill tableau-assessment
Repository Details
star Stars 5
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator
sigmacomputing
sigmacomputing Explore all skills →