Your chart type, on the Marks card

Know what changed in Tableau 2024.2: a viz extension replaces the marks themselves, and your page IS the viz — then stand one up.

intermediate7 min2 steps

Why it matters

Dashboard extensions live beside the vizzes; viz extensions ARE the viz. Since Tableau 2024.2, the Marks card's type dropdown — Bar, Line, Map — has an Add Extension entry, and choosing yours hands your web page the entire worksheet canvas: Tableau still does the data, the filters, the tooltips' worth of state, and you do the drawing. This is the answer to every 'Tableau can't draw X' conversation — Sankeys, network graphs, sparkline grids — without background-image hacks or floating web objects. The API is deliberately familiar: same library, same handshake, and worksheetContent.worksheet where dashboardContent.dashboard used to be.

Prompt Recipe

Build me a minimal Tableau viz extension (requires Tableau 2024.2+): a viz.html that loads the Extensions API library from https://tableau.github.io/extensions-api/lib/tableau.extensions.1.latest.js, calls tableau.extensions.initializeAsync(), reads tableau.extensions.worksheetContent.worksheet, and prints the worksheet's name and its summary-data row count into the page. Generate the matching .trex using the worksheet-extension element (not dashboard-extension) with id com.tableauops.hello-viz, min-api-version 1.12, and source-location http://localhost:58072/viz.html. Then tell me the exact click-path to run it: a worksheet with a dimension and a measure in play, Marks card type dropdown, Add Extension, choose a local extension.

Step 1One element renames, everything transfers

Put the manifest below next to the dashboard one from base camp and the diff is two things: the root child is worksheet-extension instead of dashboard-extension, and the version floor rises to 1.12 (Tableau 2024.2, where viz extensions shipped). Same namespace, same source-location, same icon rules, same permissions story — everything base camp taught about manifests transfers untouched.

That one element changes where Tableau offers your extension: a worksheet-extension appears in the Marks card’s type dropdown on a worksheet, not in the dashboard Objects palette.

xml
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1"
          xmlns="http://www.tableau.com/xml/extension_manifest">
  <!-- worksheet-extension, not dashboard-extension: this is the
       whole difference between living beside the viz and BEING it. -->
  <worksheet-extension id="com.tableauops.hello-viz"
                       extension-version="0.1.0">
    <default-locale>en_US</default-locale>
    <name resource-id="name"/>
    <description>A custom mark type, drawn by my page</description>
    <author name="Your Name" email="you@example.com"
            organization="TableauOps" website="https://tableauops.com"/>

    <!-- Viz extensions shipped in 1.12 / Tableau 2024.2. -->
    <min-api-version>1.12</min-api-version>

    <source-location>
      <url>http://localhost:58072/viz.html</url>
    </source-location>

    <icon>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==</icon>
  </worksheet-extension>
  <resources>
    <resource id="name">
      <text locale="en_US">Hello, Viz</text>
    </resource>
  </resources>
</manifest>

Step 2The handshake, one property over

Build a worksheet with at least a dimension and a measure in play, open the Marks card type dropdown — the one that says Automatic — and choose Add Extension, then your local .trex. Tableau swaps its marks for your page.

In code, the move is worksheetContent.worksheet where the dashboard lessons used dashboardContent.dashboard — and it is a Worksheet like any other: the same getSummaryDataReaderAsync, the same events. Which is the point: everything the reading-the-dashboard route taught is about to become your renderer’s data feed.

javascript
(async () => {
  await tableau.extensions.initializeAsync();

  // The one-property move: worksheetContent, not dashboardContent.
  const worksheet = tableau.extensions.worksheetContent.worksheet;

  const reader = await worksheet.getSummaryDataReaderAsync();
  try {
    document.body.textContent =
      'Drawing "' + worksheet.name + '" — ' +
      reader.totalRowCount + ' rows to render';
  } finally {
    await reader.releaseAsync();
  }
})();

Got what you came for? Mark the stop and the line fills in beneath you.