Skip to main content
You can create a React-based bulk action extension for your app on the 2026.09 version of the developer platform. Bulk actions let users perform operations on selected records directly from CRM object index pages. When a user selects records and triggers your action, HubSpot renders your custom React component as a modal or panel, with the selected record IDs and object type available in the extension context. You can also opt your action in to select all, so it can run on every record that matches the index page’s filters. This guide walks you through how to build a bulk action component using React and how to upload and preview it in HubSpot. Before you begin, follow the quickstart guide to create your first app on the latest version of the developer platform.

Prerequisites

  • Ensure you’ve installed the latest beta version of the HubSpot CLI.
  • If you haven’t done so yet, create a new app.
  • In your project’s hsproject.json file, set platformVersion to 2026.09.
The latest version of the HubSpot CLI, which is recommended, is 8.16.0. You can check which version of the CLI you have installed by running hs --version.

Create a bulk action

1

Add a bulk action component to your project

To add a new bulk action component to an existing project:
  1. Use the terminal to navigate into your local project directory, then run the following command:
  1. When prompted to select a component to add, select Bulk action.
Select-bulk-actionsA new actions/ directory will be created in the project’s src/app/ directory. The actions/ directory will contain:
  • A JSON configuration file (*-hsmeta.json)
  • A React component file (.tsx)
  • A package.json file
2

Upload to HubSpot

To upload your bulk action to HubSpot:
  1. Run hs project install-deps from within your local project directory to install necessary dependencies. This will create a package-lock.json file, which speeds up the build of the uploaded bulk action extension and ensures that any dependencies in your local development environment and production match.
  2. Run hs project upload.
  3. After the project finishes deploying, open the project in HubSpot by running hs project open. Alternatively, in HubSpot you can navigate to Development > Projects, then click the name of your project.
  4. Your bulk action component should now be listed on the details page.
Uploaded-bulk-action-component-on-project-details-page

View the bulk action in HubSpot

To verify the bulk action is working correctly:
  1. In your HubSpot account, navigate to a CRM object index page. For example, navigate to CRM > Contacts.
  2. Select the checkboxes of records for which to test bulk actions.
  3. At the top of the table, click More.
  4. Your bulk actions will be listed under your app’s name at the bottom of the menu.
Bulk-action-menu-options-on-index-page
  1. After clicking on your action’s name, it will open as either a modal or panel (depending on how you configured it in the hsmeta.json file).
Bulk-action-panel You can now continue to build out your bulk action as needed. You’ll use the UI extensions SDK to add functionality and visual elements to your component. Note that all the existing limitations around building UI extensions apply to building a bulk action.
  • Use hubspot.fetch() to leverage your backend to retrieve and act on data. Learn more about fetching data in UI extensions.
  • Check out the reference documentation on standard components for how to use React components when building your extension, or use the component in the Figma Design Kit.
  • Use the hs project dev command to iteratively build out your bulk action and preview your changes locally.
Please note: the local development server will only pick up changes saved to the front-end React file. If you update the *-hsmeta.json or package.json files, you’ll need to stop the server, upload your changes, then start the server again.

Work with selected records

When a user opens your bulk action, the extension context includes the records they selected. Access it through the context argument of hubspot.extend(), or with the useExtensionContext hook. To display property values for the selected records, or to process them in batches, use the useBulkSelectionRecords and useBulkSelectionAction hooks instead of reading crm.objectIds directly. The hooks fetch property values for you and split large selections into batches.
Please note: the bulk selection hooks are experimental, so their API may change before general availability. Import them from @hubspot/ui-extensions/experimental. They require version 0.18.0 or later of the @hubspot/ui-extensions package. The ESLint configuration included with a new bulk action reports a no-experimental-imports warning for these imports.

Display selected records

The useBulkSelectionRecords hook fetches one page of the selected records at a time, along with the properties you request.
object
required
The selection to read. Pass context.crm.selection.
array
required
The properties to return for each record, such as ["firstname", "email"].
number
The number of records per page. Default: 10. Values outside 1 to 100 are clamped to that range.

Process selected records

The useBulkSelectionAction hook runs your action’s logic over the whole selection. It returns two values:
  • run(): a function that splits the selection into batches, calls your callback once per batch, and resolves with a summary when it finishes.
  • status: an object that reports the progress of the current run.
useBulkSelectionAction takes the selection to process. Pass context.crm.selection. It returns an object with the following fields:
function
A function that processes the selection in batches. Returns a promise that resolves with a summary of the run.
object
The progress of the current run, including running, processed, and total.
run() takes an async callback, which receives an array of records for each batch, and the following options:
array
The properties to fetch for each record. When set, each record includes objectId and status, plus properties when status is SUCCESS, with the same format as the useBulkSelectionRecords results. When omitted, each record only includes objectId, and no property values are fetched. To get IDs only, omit properties rather than passing an empty array.
number
The number of records in each batch. Default: 100. Values outside 1 to 100 are clamped to that range.
boolean
When true, an error thrown by your callback marks that batch as failed, and the run continues with the next batch. When false (default), the first error thrown by your callback stops the run, and run() rejects with that error. If HubSpot fails to fetch a batch of records, the run stops and run() rejects with that error, even when continueOnError is true. In that case, no summary is returned.
AbortSignal
An AbortSignal to cancel the run. Aborting doesn’t interrupt a batch that’s already in progress. The run stops after the current batch and resolves with aborted set to true.

Get property values for each record

The records passed to your run() callback depend on whether you set the properties option:
  • Without properties: each record only includes objectId. HubSpot doesn’t fetch any property values or check whether the records still exist, so use this when your backend only needs the record IDs.
  • With properties: each record includes objectId and status. When status is SUCCESS, the record also includes properties, an object with a value (or null) for each property you requested. Check status before you read properties. A misspelled property name returns null rather than an error.
The following examples show both options:
To show property values before the user starts the run, such as in a table where they review the records, use useBulkSelectionRecords. The two hooks fetch data independently, so run() only fetches property values when you pass properties to it. Keep the following in mind when processing records:
  • Errored records: when you pass properties, records with a status of ERROR are still passed to your callback, so skip them as needed. They count toward succeeded or failed based on whether your callback threw an error for their batch, so count the records you actually process if you need an exact number.
  • Failed fetches reject: if HubSpot can’t fetch a batch of records, run() rejects instead of resolving with a summary. Wrap run() in try/catch.
  • One run at a time: calling run() while a run is in progress rejects with an error. Use status.running to disable the control that starts the run.
  • Runs stop on unmount: if the component unmounts, the run stops after the current batch and resolves with aborted set to true.
  • Processing happens in the browser: the run stops if the user closes the panel, modal, or browser tab. For long-running work, send the record IDs to your backend and process them there.

Support the select all checkbox

By default, a bulk action only runs on the records a user selects with checkboxes. You can optionally configure your bulk action to allow users to use the Select all checkbox, so it also runs when a user selects all records that match the current filters and search on the index page, including records on other pages. Selecting specific records gives your action a BY_IDS selection. Selecting all records gives your action a BY_QUERY selection, which describes the records with a search query instead of a list of IDs. The bulk selection hooks handle both selection methods, so your action doesn’t need separate code for each. Select all requires version 0.18.0 or later of the @hubspot/ui-extensions package. To configure support for the select all checkbox, edit your bulk action’s *-hsmeta.json file to set the supportedSelectionModes property to ["BY_IDS", "BY_QUERY"]:
Then run hs project upload. If you’re running the local development server, stop it, upload your changes, then start it again, because the server doesn’t pick up changes to *-hsmeta.json files. Bulk actions that don’t include BY_QUERY stay disabled when a user selects all records, and display a tooltip explaining why.

Read a select all selection

When a user selects all records, context.crm.selection has the following fields: For a BY_QUERY selection, context.crm.objectIds is an empty array. Pass context.crm.selection to useBulkSelectionRecords to display the matching records, and to useBulkSelectionAction to process them. The hooks page through the matching records for you, using the same code you use for a BY_IDS selection.
HubSpot captures the query and total when the user opens your action, so they don’t change while your action is open. The records that match the query are fetched again for each page or batch, so they can change while your action runs (see Select all limitations).

Unsupported filters

HubSpot only passes a BY_QUERY selection to your action when it can convert the index page’s filters and sorts exactly. If it can’t, it disables your action with a tooltip instead of running it on a broader set of records. Your action is disabled when the index page uses:
  • A filter operator other than EQ, NEQ, LT, LTE, GT, GTE, BETWEEN, IN, NOT_IN, HAS_PROPERTY, NOT_HAS_PROPERTY, CONTAINS_TOKEN, or NOT_CONTAINS_TOKEN.
  • A filter with settings that change its meaning, such as a rolling date range.
  • A filter value that isn’t a string, number, or boolean.
  • A filter on an associated object.
  • A sort that HubSpot can’t convert.
Users can still select specific records with checkboxes to run your action. When your action opens, HubSpot also resolves placeholder filter values, such as an owner filter set to the current user. If that fails, or the resolved filters can’t be converted, the panel or modal shows an error instead of loading your action.

Select all limitations

  • Results reflect the time of each request: if records change while your action runs, the run may skip or repeat records. The total in the hook’s status and in the run summary is an estimate that may change during the run.
  • Deleted records usually drop out: a record that’s deleted after the user opens your action no longer matches the query, so it isn’t passed to your callback. When you request properties, a record that’s deleted between the search and the property read is passed to your callback with a status of ERROR.
  • Batches are limited to 100 records: each batch contains at most 100 records, regardless of the size of the selection.

Bulk action configuration

Each bulk action requires a *-hsmeta.json configuration file. Include the following properties:
Last modified on November 3, 2026