Headless CMS > Custom Entries Bulk Action
Custom Entries Bulk Action
Learn how to create a custom Headless CMS entries bulk action that Webiny runs as a background task.
- What a Headless CMS entries bulk action is
- How Webiny turns a bulk action into a background task and a GraphQL mutation
- How to implement the API-side bulk action (
loadData/processData) - How to add the Admin-side button that triggers it
- How to make the task converge and finish
- How to notify the user in real time as entries are processed
The APIs on this page are available from Webiny 6.5.0. The bulk action itself is
imported from webiny/api/cms/entry.
Overview
A bulk action lets an editor select multiple entries in the Headless CMS entry list and run a single operation over all of them — for example, apply a discount to every selected product.
The important thing to understand is what Webiny does with a bulk action you register. For every EntriesBulkAction, the framework automatically generates:
- a pair of background tasks (a “list” task that paginates the matching entries, and a “process” task that runs once per entry, in batches), and
- a GraphQL mutation that the Admin app calls to trigger those tasks.
So you never schedule, chunk, retry, or resume-on-timeout anything yourself — that machinery comes from the background tasks system. You only describe what to do: which entries to operate on, and what to do to each one.
A full-stack bulk action has two parts:
- API — the
EntriesBulkActionimplementation (the background-task body). - Admin — a button in the entry list that triggers it.
The example below implements an “Apply Discount” bulk action on a product model.
The API-Side Bulk Action
You implement EntriesBulkAction.Interface and register it with EntriesBulkAction.createImplementation(). The interface has two methods:
loadData(model, params)— collect the entries to operate on. Runs in the “list” task.processData(model, params)— operate on a single entry. Runs in the “process” task.
Making the Task Converge
This is the single most important detail. The tasks engine calls loadData repeatedly — after each processing round it re-lists to check whether more work remains, and it stops only when loadData returns zero entries.
That means your filter must exclude entries that have already been processed, otherwise the task never converges: it would re-discount the same products forever and eventually fail with a maxIterations error.
In the example, processData sets onSale: true, and loadData filters those out with "values.onSale_not": true. Each round shrinks the working set until nothing is left, and the task finishes.
The bulk-action list path talks to storage directly and bypasses the GraphQL where-transform. At the storage layer, custom fields are namespaced under values. (system fields like id stay top-level), so you write the filter as "values.onSale_not". A bare onSale_not cannot be resolved and throws “There is no field with the fieldId onSale”.
skipValidation
processData updates the entry with { skipValidation: true } because this is a targeted, system-driven field update — it should not fail just because some unrelated field on the entry happens to be empty or invalid. Full validation still runs when a human edits the entry in the Admin app.
Success and Failure per Entry
Inside processData, throwing marks that single entry as failed in the task report; returning marks it done. The rest of the batch keeps going either way.
The Admin-Side Button
The button lives in the content entry list and triggers the background task. The browser does not loop over entries — it hands the whole selection to the API and the task processes it server-side. The user can navigate away while it runs.
A few things to note:
useFeature(BulkActionFeature)resolves the use case whoseexecute()fires the generated GraphQL mutation.action: "ApplyDiscount"is the PascalCased form of the APIname("applyDiscount").whereis the selection scope. Pass{ id_in: [...] }for an explicit selection, orundefinedwhen the user chose “select all” across pages, so the task processes everything matching the current view.datais the arbitrary payload delivered toprocessDataasparams.data(here, the discount percentage).
Registering the Bulk Action
On the Admin side, register the button in ContentEntryListConfig with a Browser.BulkAction whose name matches the API name:
Then wire both sides into your project with a single extension component and register it in webiny.config.tsx:
Monitoring the Task
Because a bulk action is a background task, you monitor and troubleshoot it exactly like any other task — list runs, inspect input/output, read logs, or abort it. See Background Tasks for the available GraphQL queries and mutations.
Real-Time Notifications (Optional)
Since processing happens in the background, it’s nice to tell the user as each entry completes. You can emit a websocket message from processData and toast it in the Admin app. See Generate AI Summary Bulk Action for a complete example that adds per-entry websocket notifications on top of the pattern shown here.