---
schema_version: "1"
id: 2be18ff0c5170181dae2935259be245b4d7a9af226fb7aec9bff4fa56b746bd8
title: Enhance Your Dashboard Functionality with Custom LWC Extensions
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/analytics/tableau-next-isv-dev/guides/tn-development-dashboard-extensions.md
canonical_url: https://developer.salesforce.com/docs/analytics/tableau-next-isv-dev/guide/tn-development-dashboard-extensions.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 801b7ce4d81a9467ef1ca71b8d9b548e47ea90f02fdca757379722a1a6bb7122
products:
  - Tableau Next ISV/Developer Guide
  - analytics
---
# 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
 - Install VS Code and configure it for Salesforce development. Use these Trailhead modules for installation and configuration:
   - [Quick Start: Visual Studio Code for Salesforce Development](https://trailhead.salesforce.com/content/learn/projects/quickstart-vscode-salesforce)
   - [Quick Start: Lightning Web Components - Set Up Visual Studio Code](https://trailhead.salesforce.com/content/learn/projects/quick-start-lightning-web-components/set-up-visual-studio-code)
 - Install the [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm).

## 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:

```javascript
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:

```javascript
@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](https://trailhead.salesforce.com/content/learn/projects/quick-start-lightning-web-components/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.

```xml
<targets>
  <target>analytics__Dashboard</target>
</targets>
```

Set the LWC's display name and description, which appear in the extension picker.

```xml
<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
```text
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
```text
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
```xml
<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](https://developer.salesforce.com/docs/media/tn-isv-dev/tn_isv_dashboard_ext_menu.png '{"class": "image-md"}')
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.](https://developer.salesforce.com/docs/media/tn-isv-dev/tn_isv_dashboard_ext_widget_properties.png '{"class": "image-md"}')

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.
```javascript
@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.

```javascript
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.

```javascript
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.

```javascript
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.

```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.

