Enhance Your Dashboard Functionality with Custom LWC Extensions

Use custom Lightning Web Components (LWC) to augment native Tableau Next dashboards with your own custom functionality. With the Dashboard Extension SDK, your custom components interact directly with the data and widgets in your dashboards. Because these extensions inherit global dashboard styling and respond to filter updates, they provide a cohesive and native user experience.

This guide covers implementing and integrating custom LWCs into Tableau Next dashboards with the Component Widget framework.

Prerequisites

Build Your Custom LWC

Build a custom LWC for your dashboard with this approach.

Decide between hard-coded and dynamic data binding

Your custom LWC references the dashboard data source, either hard-coded or through dynamic data binding. Use dynamic data binding to create a more powerful and flexible dashboard extension.

Type Hard-Coded Data-Bound
Semantic model Set in code Selected by the author
Fields API names set in code Mapped in the configuration UI
Retarget Edit code and redeploy Change a property value
Use when One extension serves one model One extension serves many models or orgs

Compare hard-coded and dynamic binding

When hard coding, the component owns the binding contract. You create variables for your data source, dimensions, and measures, and then assign static values to these variables.

Hard coding example:

const DATA_SOURCE = 'Superstore'; 
const DIMENSION = 'Orders.Region';

For dynamic binding, the SDK provides well-defined types, SemanticModel, SemanticMeasure, and SemanticDimension, which the dashboard author uses to select the data source, measure, and dimension at runtime. Your component then reads the resolved values from these types.

Dynamic binding example:

@api sdmName;         // semantic model
@api measureField;    // measure + aggregation
@api dimensionField;  // dimension

Create the component

To build a custom LWC, follow the steps in Create a Hello World Lightning Web Component.

Enable the Component Widget

Required metadata

To make your custom LWC visible in the Tableau Next dashboard extension picker, your LWC must include the analytics__Dashboard target in the component metadata file.

<targets>
  <target>analytics__Dashboard</target>
</targets>

Set the LWC's display name and description, which appear in the extension picker.

<masterLabel>My Custom Component</masterLabel>
<description>A unique description for my custom component.</description>

Optional metadata for data binding

To use data binding, declare the data properties in targetConfigs. Set label and description for each property, because these values render in the configuration UI. You must name label by role, such as Group-by Dimension, not by variable name.

When the LWC declares these types, the configuration UI passes an object for each property. Because these object shapes are guaranteed, read the values directly.

The defined data types are SemanticModel, SemanticMeasure, and SemanticDimension.

For a semantic model, the component sets the apiName and id values internally from the configuration UI. Don't specify these values in the metadata configuration.

The configuration UI adds an aggregation selector automatically for measure fields.

Data type syntax

type SemanticModel = {
 apiName: string;   // data source developer name
 id: string;        // data source ID
 label: string;     // display name
}

type SemanticMeasure = {
 name: string;         // qualified: Table.Field
 aggregation: string;  // e.g. "Sum"
 label: string;
}

type SemanticDimension = {
 name: string;         // qualified: Table.Field
 label: string;
}

Data type example

sdmName
 apiName: superstore3
 id:      2SMUA000000OXXXXXX
 label:   Superstore

measureField
 name:        superstore3.Sales134
 aggregation: Sum
 label:       Sales

dimensionField
 name:  superstore3.Region457
 label: Region

How to use each defined value in your component code:

  • apiName — pass as the data source name for applyFilter().
  • name — use in query field expressions. Qualified as ModelName.Field; split on the ..
  • label — use for display text such as column headers. Don't derive labels from field names.
  • aggregation — use for aggregating measure values, this.measureField?.aggregation;

Metadata example

<apiVersion>66.0</apiVersion>
<targetConfigs>
   <targetConfig targets="analytics__Dashboard">
       <property name="sdmName" type="SemanticModel"
           label="Semantic Model" description="Model to query." required="true"/>
       <property name="measureField" type="SemanticMeasure"
           label="Measure" description="Measure to aggregate." required="true"/>
       <property name="dimensionField" type="SemanticDimension"
           label="Group-by Dimension" description="Dimension to group by." required="true"/>
   </targetConfig>
</targetConfigs>

Deploy your component

Deploy your custom LWC into your org. Use the VS Code palette commands SFDX: Authorize an Org and SFDX: Deploy This Source to Org.

Add Your Custom Component to Your Dashboard

After deploying your custom LWC to your org, add the component to your Tableau Next dashboard. Use the widget configuration to customize the component functionality and dynamically bind to data sources in your org.

Add a widget

  1. In Tableau Next, open your dashboard for editing.

  2. Click the Extension icon and select an area of the dashboard.

    Tableau Next dashboard extension menu option

  3. Click Add Extension.

  4. Select your custom component and click Select.

  5. Set properties for the component on the widget panel, or replace it with a different component.

  6. Set the widget style on the design panel.

Configure the data bindings in the widget panel

Use the widget panel to bind your data.

The properties panel for a Tableau Next dashboard extension widget.

  1. Select a semantic model.
  2. Map each measure and dimension property to a field in the selected model.
  3. If applicable, select the aggregation for any measure fields.
  4. Save and confirm that the extension renders data.

To retarget the component, select a different semantic model and map the properties, and then save. No redeployment of the component is required.

Use the Dashboard Extension SDK

Pass dashboard filters, parameters, and state to your custom LWC with the Dashboard Extension SDK, so your LWC to interacts with the data in your dashboard.

The Dashboard Extension SDK is the integration layer and secure boundary between the Tableau Next dashboard runtime and custom LWC. Extensions receive the SDK via DashboardWidgetComponentProps and use it to read dashboard context, subscribe to runtime events, and publish filters or parameters.

Get the Dashboard Extension SDK

Each dashboard extension widget automatically includes the SDK. In your component, the SDK is available as the sdk property passed with your widget properties. The constructor registers with the runtime so that when filters or parameters are applied elsewhere on the dashboard, the SDK sends FILTER_CHANGE and PARAMETER_CHANGE events to your subscribers.

API Overview

Context

Retrieve the current dashboard context.

@returns (Record<string, unknown>)
getContext()

This method optionally takes the parameters:

  • currentPage - (string) the name of the current dashboard page
  • currentLayout - (string) the name of the current dashboard layout
  • dashboardState.filters - (array) a list of the current semantic filters
  • dashboardState.parameters - (array) a list of the current parameters

Events

Use SDK_EVENTS to subscribe to dashboard runtime events.

Event Constant When Fired
Filter change SDK_EVENTS.FILTER_CHANGE Dashboard filters change
Parameter change SDK_EVENTS.PARAMETER_CHANGE Dashboard parameters change

on(eventName, handler) subscribes to an event. Returns an unsubscribe function.

off(eventName, handler) removes an event subscription.

const SDK_EVENTS = {
  FILTER_CHANGE: 'filterChange',
  PARAMETER_CHANGE: 'parameterChange',
};

FILTER_CHANGE and PARAMETER_CHANGE are fired internally when the runtime invokes the callbacks for setApplyFilter and setApplyParameter, which the SDK registered during construction.

Actions

Extension actions are available in sdk.actions.

applyFilter()

Publish a filter to the dashboard. sdk.actions.applyFilter(filter)

filter is an object with:

  • fieldOrFields — string[] for a single field or string[][] for multiple fields
  • values — string[] for a single value or string[][] for multiple values; for values for multiple fields, use an array of value tuples
  • operator — string filter operator; defaults to In
  • dataSourceName — string the optional data source name; defaults to the value from registerDataSource

applyParameter()

Publish a parameter for the dashboard to use in a query. sdk.actions.applyParameter(parameter)

parameter is an object with:

  • name — string the parameter name
  • value — string the parameter value
  • dataSourceName — string the optional data source name. Specify a data source name or the value from registerDataSource.

notifyLifecycleChange()

Notifies when the lifecycle state of the component changes. For each state, add a notifyLifecycleChange() call so the dashboard renders consistently instead of showing a blank widget. sdk.actions.notifyLifecycleChange(eventName, details?)

eventName — string event name, valid values are:

  • LIFE_CYCLE_EVENTS.INIT — component initializing
  • LIFE_CYCLE_EVENTS.LOADED — component loaded successfully, query returned successfully
  • LIFE_CYCLE_EVENTS.ERROR — component is in an error state: query failure or invalid configuration
  • LIFE_CYCLE_EVENTS.NODATA — component query returns no results

details — an object with:

  • message — string the optional event message
  • error — string the optional error message

Query a bound semantic model

The data shapes for the component metadata are guaranteed, so read the values directly. You don't have to verify the shape in code, only verify that the required properties are mapped. The properties are undefined until they're mapped in the extension widget panel.

get isConfigured() {
 return Boolean(this.sdmName && this.measureField && this.dimensionField);
}

get aggregationMethod() {
 return `SEMANTIC_AGGREGATION_METHOD_${this.measureField.aggregation.toUpperCase()}`;
}

buildFieldExpression(name) {
 const p = name.split('.');
 return p.length === 2
   ? { table_field: { name: p[1], table_name: p[0] } }
   : { semantic_field: { name } };
}

// Query only when isConfigured is true.
if (this.isConfigured) {
  const dim = this.dimensionField.name;
  const msr = this.measureField.name;

  const query = {
   fields: [
     { expression: this.buildFieldExpression(dim), alias: dim, rowGrouping: true },
     { expression: this.buildFieldExpression(msr), alias: msr,
         rowGrouping: false, semanticAggregationMethod: this.aggregationMethod }
     ],
    options: { limitOptions: { limit: 101 } }
  };

  const rows = await this.sdk.fetchDataUsingQueryAndSource(query, this.sdmName.apiName);
}

Always set limitOptions.limit, because high-cardinality dimensions can return more rows than an extension can render.

Create filters from a bound field

Use dynamically bound fields to create dynamic filters.

this.sdk.actions.applyFilter({
  fieldOrFields: this.dimensionField.name,
  values: [selectedValue],
  operator: 'In',
  dataSourceName: this.sdmName.apiName
});

React to runtime updates

When using dynamic data binding, re-query to pick up runtime changes to data values. Write code to handle:

  • When a bound property changes. Querying only in connectedCallback() means that the user sees stale data when editing data bindings.
  • When FILTER_CHANGE events occur, so the extension component responds to the dashboard filters, not just the component controls.

Dashboard Extension SDK Example

This example shows how to use the Dashboard Extension SDK in your component JavaScript.

// In your extension component, define sdk prop as @api. 
// After the extension component is connected, sdk is available for use
@api sdk;

// Define dynamic data binding properties
@api sdmName;         
@api measureField;   
@api dimensionField;

// Register the data source that your extension uses or pass dataSourceName on each filter or parameter
connectedCallback() {
  // React to filter changes
  this.unsubscribe = this.sdk.on(SDK_EVENTS.FILTER_CHANGE, (filters) => {
    // update your UI with filters
  });
}

disconnectedCallback() {
  // Clean up
  this.unsubscribe();
}

// Get the dashboard context
const { currentPage, dashboardState } = this.sdk.getContext();

// Apply a single field filter from the extension to the dashboard (Hardcoded filter value)
this.sdk.actions.applyFilter({
    fieldOrFields: 'Account.Type',
    operator: 'In',
    values: ['A', 'B'],
    dataSourceName: 'myDataSDM'
});

// Apply a parameter
this.sdk.actions.applyParameter({ name: 'Region', value: 'East', dataSourceName: 'myDataSDM' });

// Report a loaded lifecycle event
this.sdk.actions.notifyLifecycleChange(LIFE_CYCLE_EVENTS.LOADED);

// Report an error lifecycle event with details
this.sdk.actions.notifyLifecycleChange(LIFE_CYCLE_EVENTS.ERROR, { message: 'Load failed' });

// Use dynamic values from data binding
get isConfigured() {
 return Boolean(this.sdmName && this.measureField && this.dimensionField);
}

get aggregationMethod() {
 return `SEMANTIC_AGGREGATION_METHOD_${this.measureField.aggregation.toUpperCase()}`;
}

buildFieldExpression(name) {
 const p = name.split('.');
 return p.length === 2
   ? { table_field: { name: p[1], table_name: p[0] } }
   : { semantic_field: { name } };
}

// Query only when isConfigured is true.
if (this.isConfigured) {
  const dim = this.dimensionField.name;
  const msr = this.measureField.name;

  // Apply dynamic filter
  this.sdk.actions.applyFilter({
    fieldOrFields: this.dimensionField.name,
    values: [selectedValue],
    operator: 'In',
    dataSourceName: this.sdmName.apiName
  });

  const query = {
   fields: [
     { expression: this.buildFieldExpression(dim), alias: dim, rowGrouping: true },
     { expression: this.buildFieldExpression(msr), alias: msr,
         rowGrouping: false, semanticAggregationMethod: this.aggregationMethod }
    ],
    options: { limitOptions: { limit: 101 } }
  };

  const rows = await this.sdk.fetchDataUsingQueryAndSource(query, this.sdmName.apiName);

  if (rows) {
    this.sdk.actions.notifyLifecycleChange(LIFE_CYCLE_EVENTS.LOADED);
  } else {
    this.sdk.actions.notifyLifecycleChange(LIFE_CYCLE_EVENTS.ERROR, { message: 'Query failed' });
  }
} else {
    this.sdk.actions.notifyLifecycleChange(LIFE_CYCLE_EVENTS.ERROR, { message: 'Configuration failed' });
}

Generate a Dashboard Extension with an AI Agent

When prompting an AI agent to generate a dashboard extension, include enough detail to avoid common mistakes.

For example, to prevent the agent from hard coding values, specify these directives in your prompts:

  1. Specify the analytics__Dashboard target, and set masterLabel and description.
  2. Declare SemanticModel, SemanticMeasure, and SemanticDimension properties, and mark required ones.
  3. Read properties as @api values — don't hard code field API names.
  4. Build the query at runtime from resolved values.
  5. Show a configuration message when required properties are unmapped.
  6. Publish selections with applyFilter.

Review the generated metadata file to confirm property types before deploying.

Verify Dynamic Binding

After deploying your component, verify that the dynamic data binding works as expected.

  1. Load the extension with no data mapped.
  2. Select a semantic model and confirm the extension renders the data.
  3. Select another model with different field names and confirm the updated data renders. This step catches accidental hard coding.

Considerations and Limitations

Each component is responsible for its own internal access checks. If a permission error occurs, the component must notify the dashboard via an event to allow for consistent error propagation.

LWC support on mobile operating systems is limited. Rendering components on mobile devices can cause errors.

For dynamic data binding, keep these points in mind.

  • Set the component apiVersion to 66.0 or later. Earlier versions don't render semantic property pickers.
  • Semantic property types require dynamic data binding enabled in the org. Without it, the configuration panel is empty.
  • Deleting a bound field from the model leaves the property unresolved. Handle the empty value and remap.
  • Renaming or removing a property breaks dashboards that already mapped it. Add new optional properties rather than repurposing existing ones.
  • Interactive marks that publish filters must be keyboard operable with ARIA state. Extensions automatically inherit dashboard styling, but not accessibility behavior. Implement keyboard and ARIA support yourself.

Troubleshooting

  • Your component isn't in the extension picker — missing analytics__Dashboard target or masterLabel. Add both and redeploy.
  • The configuration panel is empty — data binding unavailable in the org, or the apiVersion is too low.
  • Configuration failures after data mapping — a property resolved to an empty value. Make sure to handle both string and object shapes.
  • Native visualizations don't respond to applyFilter — the field reference or dataSourceName doesn't match the visualization. Log the response and use the qualified field name.
  • Deployment fails after removing a property — component validation reconciles against the deployed version. Delete the component from the org, and then redeploy.
  • Your changes don't appear after deployment — the old version is cached. Hard refresh, or remove and re-add the extension.