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.jsonfile, setplatformVersionto2026.09.
Create a bulk action
1
Add a bulk action component to your project
To add a new bulk action component to an existing project:
A new
- Use the terminal to navigate into your local project directory, then run the following command:
- When prompted to select a component to add, select Bulk action.
A 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.jsonfile
- NewAction-hsmeta.json
- NewAction.tsx
- package.json
2
Upload to HubSpot
To upload your bulk action to HubSpot:
- Run
hs project install-depsfrom within your local project directory to install necessary dependencies. This will create apackage-lock.jsonfile, which speeds up the build of the uploaded bulk action extension and ensures that any dependencies in your local development environment and production match. - Run
hs project upload. - 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. - Your bulk action component should now be listed on the details page.

View the bulk action in HubSpot
To verify the bulk action is working correctly:- In your HubSpot account, navigate to a CRM object index page. For example, navigate to CRM > Contacts.
- Select the checkboxes of records for which to test bulk actions.
- At the top of the table, click More.
- Your bulk actions will be listed under your app’s name at the bottom of the menu.
- 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.jsonfile).
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 devcommand to iteratively build out your bulk action and preview your changes locally.
Work with selected records
When a user opens your bulk action, the extension context includes the records they selected. Access it through thecontext 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.
Display selected records
TheuseBulkSelectionRecords hook fetches one page of the selected records at a time, along with the properties you request.
- Call
- Response
- Example usage
Process selected records
TheuseBulkSelectionAction 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.
- Call
- Response
- Example usage
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 yourrun() callback depend on whether you set the properties option:
- Without
properties: each record only includesobjectId. 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 includesobjectIdandstatus. WhenstatusisSUCCESS, the record also includesproperties, an object with a value (ornull) for each property you requested. Checkstatusbefore you readproperties. A misspelled property name returnsnullrather than an error.
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 astatusofERRORare still passed to your callback, so skip them as needed. They count towardsucceededorfailedbased 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. Wraprun()intry/catch. - One run at a time: calling
run()while a run is in progress rejects with an error. Usestatus.runningto 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
abortedset totrue. - 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 aBY_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"]:
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.
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 aBY_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, orNOT_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.
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
totalin the hook’sstatusand 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 astatusofERROR. - 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: