grill-me
Interview the user relentlessly about a plan or design until reaching shared understanding,…
Authoring a custom Headless CMS bulk action (EntriesBulkAction) that Webiny runs as a background task, plus the Admin-side button that triggers it. Use this skill when the developer wants to add a bulk action to the content-entry list (e.g. apply a discount, generate content,
$ npx -y skills add webiny/webiny-js --skill cms-bulk-actions --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/cms-bulk-actionsContext preview
The summary Claude sees to decide when to auto-load this skill.
Authoring a custom Headless CMS bulk action (EntriesBulkAction) that Webiny runs as a background task, plus the Admin-side button that triggers it. Use this skill when the developer wants to add a bulk action to the content-entry list (e.g. apply a discount, generate content,
name: webiny-cms-bulk-actions description: > Authoring a custom Headless CMS bulk action (EntriesBulkAction) that Webiny runs as a background task, plus the Admin-side button that triggers it. Use this skill when the developer wants to add a bulk action to the content-entry list (e.g. apply a discount, generate content, bulk-transform entries), understand loadData/processData, make the task converge, filter by custom fields, or trigger the action from the Admin UI. Requires Webiny 6.5.0 or newer.
A bulk action is a class implementing `EntriesBulkAction.Interface` with two methods — `loadData` (which entries) and `processData` (what to do to each). Register it with `export default EntriesBulkAction.createImplementation({...})` via `<Api.Extension src>`. For every registered bulk action, Webiny **automatically generates** a list background task, a process background task, and a GraphQL mutation. On the Admin side, add a `ContentEntryListConfig.Browser.BulkAction` button that calls `BulkActionFeature`'s `useCase.execute({ model, action, where, data })`.
Available from **Webiny 6.5.0** (`webiny/api/cms/entry`).
// extensions/myBulkAction/api/MyBulkAction.ts
import {
EntriesBulkAction,
ListLatestEntriesUseCase,
UpdateEntryUseCase
} from "webiny/api/cms/entry";
class MyBulkActionImpl implements EntriesBulkAction.Interface {
// PascalCased into the task ids + GraphQL enum value, so "applyDiscount" →
// tasks hcmsBulk(List|Process)ApplyDiscountEntries and frontend action "ApplyDiscount".
readonly name = "applyDiscount";
// Optional: restrict which models get the mutation/button.
readonly modelIds = ["product"];
// Optional: entries processed per batch (defaults to the configured batchSize).
// readonly batchSize = 50;
constructor(
private listEntries: ListLatestEntriesUseCase.Interface,
private updateEntry: UpdateEntryUseCase.Interface
) {}
// Runs in the "list" task, with pagination (params.where/search/after/limit).
async loadData(model, params) {
const result = await this.listEntries.execute(model, params);
return result.value; // { entries, meta }
}
// Runs in the "process" task, once per entry, in batches.
async processData(model, params) {
// params.id is a revision id ("<entryId>#0001"); params.data carries whatever the
// Admin action sent.
// ...update / transform the entry here...
}
}
export default EntriesBulkAction.createImplementation({
implementation: MyBulkActionImpl,
dependencies: [ListLatestEntriesUseCase, UpdateEntryUseCase]
});`loadData`/`processData` **are** the background-task body. You never write scheduling, batching, retry, or timeout-resume code — the tasks system provides all of it. Webiny generates `hcmsBulkList<Name>Entries`, `hcmsBulkProcess<Name>Entries`, and the mutation `bulkAction<SingularApiName>(action: <Name>, ...)`.
The engine calls `loadData` **repeatedly** until it returns zero entries — after each processing round it re-lists to check for more work. **If `loadData` keeps returning the same entries, the task never converges: it re-processes them until it hits `maxIterations` and fails.** So the filter MUST exclude already-processed entries.
and `processData` publishes; the next list is smaller. Built-in actions rely on this.
blocks re-running until you reset the flag.
click and filters "not stamped with this run"; `processData` stamps the entry with `runId`. The run converges once everything is stamped, but the next click uses a new token, so the same entries are eligible again — no manual reset.
The bulk-action list path talks to storage **directly**, bypassing the GraphQL where-transform. Mind the difference:
system fields are top-level (`id_in`, `status_not`, `savedOn_lt`, …); **custom fields are nested** under `values` — `where: { values: { onSale_not: true } }`. A dotted key like `"values.onSale_not"` is rejected by the typed input.
**flat dotted** — `{ "values.onSale_not": true }`; system fields stay top-level. A bare `onSale_not` throws `There is no field with the fieldId "onSale"`.
So if the Admin action sends a custom-field filter, flatten it in `loadData`:
async loadData(model, params) {
const where = { ...params.where };
if (where.values && typeof where.values === "object") {
for (const [k, v] of Object.entries(where.values)) {
where[`values.${k}`] = v;
}
delete where.values;
}
return (await this.listEntries.execute(model, { ...params, where })).value;
}Alternatively, add a **constant** custom-field filter entirely in `loadData` (storage format) and send only system fields from the Admin (that's how the simplest actions work).
Note: only **searchable** custom fields appear in the GraphQL where input; a plain field may not be filterable via GraphQL, in which case add the filter backend-side in `loadData`.
Use `UpdateEntryUseCase`; field values are nested under `values`, and pass `{ skipValidation: true }` for targeted, system-driven field updates so an unrelated required/invalid field on the entry doesn't fail the operation:
await this.updateEntry.execute(
model,
entry.id,
{ values: { price: newPrice } },
{ skipValidation: true }
)Open-source content platform. Self-hosted on AWS serverless. Built as a TypeScript framework you extend with code, not a closed product you configure through a UI. Runs on Lambda, DynamoDB, S3, and CloudFront inside your own AWS account. Scales automatically.
Repo: webiny/webiny-js
Interview the user relentlessly about a plan or design until reaching shared understanding,…
Turn a PRD into a multi-phase implementation plan using tracer-bullet vertical slices, saved…
Webiny-only. Run all checks required before packages are ready for publish: deps, build,…
Use when running tests. Shows how to run tests for a single package, including OpenSearch…
Generate, refresh, and maintain Webiny MCP server skills from source documentation and…
Create a PRD through user interview, codebase exploration, and module design, then submit as…