// Tableau + AI · Claude · Extensions API
Build a Tableau Extension with Claude, Start to Finish
The full walkthrough: the prompt, the Extensions API code, the .trex manifest, local testing, loading it in Tableau Desktop and hosting it. Plus the shortcut where Claude writes the component and TableauOps hands back the .trex.
OCT 14, 2026
I build Tableau extensions with Claude almost every day now, and the process has settled into something repeatable. This guide is that process, start to finish: the prompt, the code Claude writes, the .trex manifest, running it locally, loading it in Tableau Desktop, and hosting it so other people can use it.
I'll walk through a viz extension (a custom mark type on the Marks card), because that's where AI pays off most. Near the end I'll show the shortcut I use when I don't want to touch a file at all.
What is a Tableau extension, and which kind should you build?
A Tableau extension is a small web app that Tableau loads in an embedded browser and talks to through the Tableau Extensions API. There are two kinds, and you have to pick before you prompt, because they use different manifest roots and different API entry points:
- Viz extension: a custom mark type that lives inside one worksheet. You choose it from the Mark Type dropdown on the Marks card, and the user drags fields onto tiles you declare. Tableau introduced these in 2024.2. Use this for a custom chart: a ridgeline, a bullet chart, a metric card.
- Dashboard extension: an object you drop on a dashboard. It can read any worksheet on the dashboard, apply filters and react to selections. Use this for tools: a search box, a filter panel, an export button.
If you're not sure, I wrote up the trade-offs in dashboard extension or viz extension. For a custom chart, start with a viz extension.
What do you need before you start?
- Tableau Desktop 2024.2 or later. A trial works. Extensions don't run in Tableau Public, so build and test in Desktop.
- Claude, ideally Claude Code, because it can write the files straight into a folder.
- A static file server for local testing. Anything works:
npx serve, Python'shttp.server, or the server your editor ships with. - A host with real HTTPS for when you share it: GitHub Pages, Netlify, Cloudflare Pages, or a managed host.
The one thing I'd add, and it matters more than any of the above, is the rules. Claude is good at React and D3. It does not know, by default, that a .trex manifest has a fixed element order or that two Marks card tiles can't share an icon. That's what a skill is for, and I cover that in Claude Code skills for Tableau developers.
Step 1: How should you prompt Claude for a Tableau extension?
Be specific about four things: the kind of extension, the tiles on the Marks card, what happens when there's no data, and how it's styled. Here's a prompt I'd actually use for a lollipop chart:
Build a Tableau VIZ extension (a custom mark type on the Marks card, not a
dashboard extension) that draws a lollipop chart.
Marks card tiles (declare these as encodings in the .trex):
- Category: dimension, required, one mark per value
- Value: measure, required, the stem length
Every encoding gets a different encoding-icon token.
Behavior:
- Read the fields by encoding id with getVisualSpecificationAsync, never by
column order. Read rows with the summary data reader.
- Sort descending by Value. Negative values extend left across a zero line.
- Re-render on SummaryDataChanged and on resize. No scrollbars, ever.
- Empty state: if a required tile is empty, show "Drag a dimension onto
Category and a measure onto Value" instead of a blank pane.
- Outside Tableau, render with sample data and a small DEMO DATA badge.
Style: one accent color, muted gray stems, mono labels, a light and a dark
theme, and a configure dialog with accent, theme and size.
Files: index.html, viz.js, style.css, config.html, config.js,
manifest.local.trex (localhost) and manifest.hosted.trex (HTTPS URL).
Plain HTML, CSS and JS with no build step.Notice what's in there. The tiles are spelled out, because a viz extension only sees the fields you declare. The empty state is spelled out, because otherwise you get a blank box and no clue why. And the two manifests are named, so you never ship the localhost one by accident. I keep a whole library of these in prompts that build good Tableau viz extensions.
Step 2: What does the extension code look like?
Claude will hand back a handful of files. The heart of it is the read loop in viz.js. Yours should look roughly like this:
let worksheet;
async function boot() {
if (!window.tableau?.extensions) return demoMode();
try {
await tableau.extensions.initializeAsync({ configure });
} catch (e) {
return demoMode();
}
worksheet = tableau.extensions.worksheetContent.worksheet;
worksheet.addEventListener(tableau.TableauEventType.SummaryDataChanged, refresh);
new ResizeObserver(() => draw()).observe(document.body);
await refresh();
}
async function refresh() {
// Which field landed on which Marks card tile
const spec = await worksheet.getVisualSpecificationAsync();
const marks = spec.marksSpecifications[spec.activeMarksSpecificationIndex];
const enc = {};
for (const e of marks.encodings) enc[e.id] = e.field.name;
if (!enc.category || !enc.value) {
return drawEmpty('Drag a dimension onto Category and a measure onto Value');
}
// Page through the summary data, then find columns by name
const reader = await worksheet.getSummaryDataReaderAsync();
const table = await reader.getAllPagesAsync();
await reader.releaseAsync();
const cat = table.columns.find((c) => c.fieldName === enc.category);
const val = table.columns.find((c) => c.fieldName === enc.value);
const rows = table.data.map((r) => ({
label: r[cat.index].formattedValue,
value: r[val.index].nativeValue,
}));
draw(rows);
}
boot();Three habits in that code are worth checking every time Claude writes one:
- Everything waits for
initializeAsync(). No API call happens before it resolves. - Columns are found by name, never by position. Use
nativeValuefor math andformattedValuefor labels. - The empty state is a real message. A pane that says what to drag where saves you a support email.
The getVisualSpecificationAsync() pattern is straight from Tableau's own viz extension docs. If Claude reaches for something else to figure out which field is on which tile, push it back to this.
Step 3: How do you write the .trex manifest?
The .trex is a short XML file that tells Tableau where your extension lives and which tiles to put on the Marks card. It's also where most first loads fail. Here's a complete viz extension manifest for the lollipop:
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1" xmlns="http://www.tableau.com/xml/extension_manifest">
<worksheet-extension id="com.yourorg.lollipop" extension-version="1.0.0">
<default-locale>en_US</default-locale>
<name>Lollipop Chart</name>
<description>A stem and a dot per category, sorted and labeled.</description>
<author name="Your Name" email="you@example.com" organization="YourOrg" website="https://example.com"/>
<min-api-version>1.12</min-api-version>
<source-location>
<url>http://localhost:41873/index.html</url>
</source-location>
<icon>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==</icon>
<encoding id="category">
<display-name>Category</display-name>
<role-spec><role-type>discrete-dimension</role-type></role-spec>
<fields max-count="1"/>
<encoding-icon token="level-of-detail"/>
</encoding>
<encoding id="value">
<display-name>Value</display-name>
<data-spec><data-type>numeric</data-type></data-spec>
<role-spec><role-type>continuous-measure</role-type></role-spec>
<fields max-count="1"/>
<encoding-icon token="metric"/>
</encoding>
</worksheet-extension>
</manifest>The rules that bite, every one of which I've hit:
- Element order is fixed.
default-locale,name,description,author,min-api-version,source-location,icon, then the encodings. Right elements in the wrong order fail the same way as a missing element. - Include the
<icon>. Some Tableau builds throwFD722608without it. - No
--inside an XML comment. It's illegal XML, and it surfaces as the sameFD722608code, so don't assume it's the icon. - Four encodings at most, each with a different icon token. Two tiles sharing a token fails on add with
ED626076. Never name a tiledetailortooltip; those are Tableau's built-in tiles.
The full element-by-element tour is in what's inside a .trex, and the encoding rules are in viz extension encodings.
Step 4: How do you test a Tableau extension locally?
Serve the folder over HTTP on the port your local manifest points at. Don't open the HTML with file://; the API won't load that way.
npx serve -l 41873 .Then open http://localhost:41873/index.html in a normal browser tab. Because the prompt asked for a demo mode, you should see the chart drawn with sample data and a DEMO DATA badge. This is my fastest debugging loop: if it's broken in the browser, it's broken in Tableau, and the browser console tells you why.
localhost over plain HTTP is allowed in Tableau Desktop for development. It is not something you can share.
Step 5: How do you load a viz extension in Tableau Desktop?
- Open a worksheet and connect to some data (Superstore is fine).
- On the Marks card, open the Mark Type dropdown. Under Viz Extensions, choose Add Extension.
- In the dialog, choose Access Local Extensions and pick
manifest.local.trex. - Allow it to run when Tableau asks.
- Your tiles appear on the Marks card. Drag a dimension onto Category and a measure onto Value.
Those steps match Tableau's getting started guide for viz extensions. Two reload rules save a lot of confusion: edits to HTML, JS or CSS only need a Reload of the extension. Any edit to the .trex needs a full remove and re-add, because Tableau embeds a copy of the manifest in the workbook.
When something fails, paste the exact error back to Claude. Not a summary, the exact text with the code. One or two rounds is normal.
Step 6: How do you host a Tableau extension so others can use it?
A manifest pointing at localhost works on exactly one machine: yours. To share it:
- Upload the folder to a static host with a real HTTPS certificate. Self-signed certificates are rejected.
- Point
manifest.hosted.trexat that HTTPS URL. That's the only line that changes. - Share the hosted manifest.
- On Tableau Cloud or Server, an admin adds the host to the extensions safe list once.
I wrote a whole post on this: how to host a Tableau extension.
Is there a faster way to build a Tableau extension with AI?
Yes. Everything above is the do-it-yourself path, and it's worth doing once so you understand the moving parts. Day to day, I skip most of it with two tools I built at TableauOps.
ext.tableauops.com Studio is a browser workspace for viz extensions. Pick a starter or paste a component, load Superstore or your own CSV, and watch React and D3 draw it live. It runs the checks Tableau would otherwise only tell you about after you add the extension: a fifth tile, a duplicate icon, a field you read but never declared. No account is needed to build, check and download.
The TableauOps MCP connector puts the same pipeline inside Claude. Claude writes one component, TableauOps compiles and hosts it, and hands back a .trex. The component follows a short contract: one function named Component, two metadata lines, and React and D3 provided as globals.
// viz: {"type":"bar","variant":"lollipop"}
// fields: [{"id":"category","name":"Category","accepts":"dimension","max":1,"required":true,"icon":"level-of-detail","hint":"One mark per value."},{"id":"value","name":"Value","accepts":"measure","max":1,"required":true,"icon":"metric","hint":"Stem length, e.g. SUM(Sales)."}]
function Component({ data, schema, theme, fields, config }) {
const rows = Array.isArray(data) ? data : [];
if (rows.length === 0) return <div style={{ padding: 20 }}>Drag a dimension onto Category and a measure onto Value</div>;
// ...scales with d3, marks with React, an <svg> at 100% width and height...
}The // fields: line becomes the Marks card tiles in the .trex, so the manifest is written for you. To connect it in Claude Code:
claude mcp add --transport http tableauops https://ext.tableauops.com/mcpThen ask for the chart in plain English, something like "publish a lollipop chart to TableauOps, sorted descending, negatives across a zero line." Claude reads the authoring guide, calls publish_extension, and you get back a hosted page with the .trex and install steps. Ask for a change and it calls update_extension in place. The full contract is in the hosted viz component contract, and setup for every client is in Tableau MCP server: connect Claude to Tableau.
Extensions are free to use in Tableau Desktop. Publishing to Tableau Cloud or Server takes an annual license; the details are on the pricing page.
What are the most common errors when Claude builds a Tableau extension?
| What you see | What it usually is |
|---|---|
FD722608 on load | A missing or out-of-order manifest element, often the icon, or a -- inside an XML comment |
ED626076 on add | Two encodings share an icon token |
| Blank pane, no error | A JS error. Open the URL in a browser tab and read the console |
| Works for you, blank for everyone else | The shared .trex still points at localhost |
| Edits don't show up | You changed the .trex. Remove and re-add the extension |
| Wrong numbers | Columns matched by position instead of by field name |
The longer list, with fixes, is in the errors every Tableau extension throws.
Your next step
If you want to see one working before you write a line, open Studio, pick a starter, and drop in your own CSV. If you'd rather start from finished charts, browse the extension gallery. And if you're building in Claude Code, grab the free Tableau extension skill first. For the wider picture, the Tableau + AI hub has the rest of this series.
Frequently asked questions
- Can Claude build a Tableau extension?
- Yes. Claude writes the HTML, JavaScript and .trex manifest for both dashboard and viz extensions. It works best when you spell out the Marks card tiles, the empty state and the two manifests, or give it a skill with the manifest rules built in.
- Do I need to know JavaScript to build a Tableau extension with AI?
- It helps to read it, but you don't need to write it. The loop is: prompt, load it in Tableau, paste the exact error back, repeat. With Studio or the TableauOps MCP connector you don't handle the manifest or hosting at all.
- Which Tableau version supports viz extensions?
- Viz extensions arrived in Tableau 2024.2 and run in Tableau Desktop, Tableau Server and Tableau Cloud. They don't run in Tableau Public.
- Why does my extension work on my machine but not for anyone else?
- Almost always the .trex points at localhost. Host the files over HTTPS, point the manifest at that URL, and share the hosted manifest.