Direct answer
Never run a catalogue-wide change straight against live data. Export the current state first, change one thing at a time, and keep that export as your rollback. For a repeatable, rule-based edit, prices, metafields, tags or redirects, script it against the GraphQL Admin API: read with a bulk query, write with bulkOperationRunMutation over a JSONL file, and let an AI agent draft the mutation and the file while a scoped, short-lived token does the work. For a one-off migration a non-developer will run, reach for Matrixify and its dry-run, because the native CSV import cannot touch metafields. Under both, one rule holds: a bulk write reaches every product in a single call, so the snapshot and the dry run are not optional extras.
Key takeaways
- A bulk write reaches every product in one call. Export the before-state first; that export is your only rollback.
- There is no single mutation for variant prices or inventory. Wrap productVariantsBulkUpdate in a bulk operation over a JSONL file, one line per product.
- Dry-run on a development store or a small subset, read the per-line userErrors, then apply the full file.
- Redirects are catalogue data too: a mis-mapped column can point hundreds of URLs at the wrong place. Validate every target before import.
- Script the repeatable change; buy Matrixify for the one-off migration a non-developer runs. Native CSV import cannot set metafields.
What a bulk edit actually touches
Our earlier guides put the Shopify theme under version control with an AI agent and gave that agent a scoped credential rather than a login. This one is about the other half of the job: changing the data behind the storefront in bulk, safely, when there are thousands of products and one afternoon to do it. It sits in our UK payments and ecommerce work, where a wrong price or a broken redirect is not a cosmetic bug, it is lost revenue.
Shopify gives you a handful of things you will end up changing across the whole catalogue, and an honest constraint on each.
productVariantsBulkUpdate updates the variants of a single product, so a catalogue price change is one JSONL line per product, not one request. Inventory has no bulk-operation path at all; it is set per item and location.
One call sets up to 25 metafields, atomically, so a partial write inside a call cannot happen. Native CSV import cannot create or update metafields, which is exactly why bulk metafield work goes through the API or a paid tool.
tagsAdd, tagsRemove and collection publishing are safe to repeat. urlRedirectCreate is the sharp one: a redirect points somewhere, and a wrong target is live SEO damage the moment it saves.
So the first decision is how you run the change, and it is genuinely a decision. A one-off migration a client will run themselves belongs in a tool with a dry-run and a spreadsheet, not a bespoke script. A change you will make every week, driven by a rule and wired into other systems, belongs in a scoped Admin API script you own. Choose by how often it runs.
- Repeatable and rule-based: same logic, new data, every week
- Reads from and writes to other systems, feeds and models
- An AI agent can draft the mutation and the JSONL file
- You own the dry run, the logging and the rollback
- One-off migration or a change a non-developer will run
- Reaches metafields and redirects the native importer cannot
- A built-in dry-run that lists errors and warnings first
- Priced by tier; scale it up to migrate, then scale it back down
The safe bulk-change pipeline
Every safe bulk change we run is the same five steps, and the three that matter are the ones people skip when they are in a hurry. Export the before-state to a JSONL file and keep it. Build the change file and validate its schema. Dry-run it, on a development store or a slice of ten products, and read the userErrors it returns. Only then apply the full file. And when it is done, diff the result against the snapshot and keep that snapshot somewhere you can find it, because it is the undo button. The scoped token limits how far a mistake can reach; the snapshot and the dry run limit the damage inside that reach.
Whatever runs the job, it should carry all of this, every time:
How a bulk mutation runs
A bulk mutation is asynchronous, and that shape is the whole trick. You reserve an upload with stagedUploadsCreate, POST a JSONL file where each line is one input, and run bulkOperationRunMutation, which executes your mutation once per line. Then you poll currentBulkOperation until it reports COMPLETED and download a results JSONL, line by line, with successes and userErrors separated. The recovery model falls out of that: re-run only the lines that failed. Shopify: bulk import with the GraphQL Admin API
Three limits decide how you shape the job. The GraphQL Admin API restores 100 points a second on a standard plan, 200 on Advanced, 1,000 on Plus and 2,000 on Enterprise, and no single query may cost more than 1,000 points, so a naive loop throttles itself long before it throttles the store. Shopify: API rate limits. metafieldsSet takes at most 25 metafields per call. And since API version 2026-01 a shop can run up to five bulk mutations at once, where older versions allowed one of each type, so parallelism is a version decision, not a guess. Inventory is the exception with no bulk path, which is where a throttled cron that updates a batch on a schedule earns its place over a single doomed request.
| Change | How it is written | The limit that bites | How you roll it back |
|---|---|---|---|
| Variant prices | productVariantsBulkUpdate in a bulk operation | One product per call, so one JSONL line per product | Re-apply the exported prices |
| Inventory | Set per item and location; no bulk-operation path | Throttle a cron against the point budget | Re-apply the exported quantities |
| Metafields | metafieldsSet | 25 per call, atomic per call | Re-apply, or delete the keys you set |
| Tags & collections | tagsAdd, tagsRemove, publish | Idempotent, but check publication rules | Remove the tags you added |
| Redirects | urlRedirectCreate and urlRedirectUpdate | A wrong target is live SEO damage | Delete the ones you created, re-import the old set |
// bulk-price.mjs: apply a price change to many products in one bulk mutation.
// Step 0, always: export the before-state with bulkOperationRunQuery and keep it.
// price.jsonl, one line per product, each line one productVariantsBulkUpdate input:
// {"productId":"gid://shopify/Product/123",
// "variants":[{"id":"gid://shopify/ProductVariant/456","price":"19.99"}]}
// 1. Reserve an upload with stagedUploadsCreate, POST price.jsonl to it, keep the path.
// 2. Run the mutation over every line of the file:
const VARIANT_UPDATE = `mutation call($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
userErrors { field message }
}
}`;
const run = await admin(`mutation bulk($path: String!, $q: String!) {
bulkOperationRunMutation(mutation: $q, stagedUploadPath: $path) {
bulkOperation { id status }
userErrors { field message }
}
}`, { path: stagedUploadPath, q: VARIANT_UPDATE });
// 3. Poll currentBulkOperation until COMPLETED, download the results JSONL, and
// re-run only the lines that returned a userError. The step-0 export is the
// rollback: re-apply it exactly the same way if the change was wrong.What breaks in production
For this guide we went through our own Shopify catalogue work. Client names are withheld and described by sector. These are the bulk-change failures that actually cost time, and the rule each one taught.
A merged import split price and content across roughly 1,378 products: 306 pages had the correct price but no description or image, the rest had the content but the old price.
Read and export the catalogue before you write to it. Dedupe by preserving content, never blind-delete.productVariantUpdate was gone, and productVariantsBulkUpdate turned out to be one product per call, useless for variant-level SKUs until wrapped in a JSONL bulk operation.
Confirm the mutation and your ID granularity first. The shape of your SKUs decides the shape of the file.Inventory had no bulk path, so the naive version was one REST call per item per location across tens of thousands of SKUs, straight into the rate limit.
When there is no bulk path, throttle a cron against measured throughput. Do not hammer the API and hope.A migration's redirect import mis-mapped a column and pushed spreadsheet notes in as redirect targets, so hundreds of live URLs pointed at nonsense.
A redirect points somewhere. Dry-run the import and validate the target of every row before it goes live.Every free App Store bulk editor capped at 10 products per file, and the sale and regular prices had no common pattern to formula.
Free tiers are demos. Real catalogues need a script or a paid tool, and irregular data needs a mapping, not a formula.Migration imports repeatedly hit row and daily caps and reported partial success that was easy to read as done.
Read the failure count and reconcile against the snapshot. An import that finished is not an import that succeeded.- Export before you write, or you cannot roll back. A Shopify mutation has no undo. The before-state JSONL is the only thing that turns a mistake into a re-run instead of an incident.
- Dry-run against the userErrors, not against your confidence. Run the file on a subset or a development store and read what comes back per line. The errors are cheaper to find before the full run than after it.
- Treat redirects and handles as catalogue data. Changing a handle changes a URL. Validate every redirect target, and generate the redirect when the handle moves, or search rankings pay for it.
The Catalogue Change Risk Score
Before a bulk change runs, this is the triage we use. It does not measure whether the change is a good idea. It measures how bad an afternoon it could turn into, and whether to run it or build the safety net first.
A scripted price change with an export, a dry run on a dev store and per-line error handling scores zero or one. The same change typed straight into a live store with no export, because it was quicker, scores nine before it has touched a product.
A real boundary: the API will not bulk everything
The boundary that catches every high-SKU store is that Shopify’s convenient bulk tools stop short of what you actually need. On a UK caravan, motorhome and campervan parts retailer we run, with tens of thousands of variant-level SKUs, the tidy mutation, productVariantsBulkUpdate, only handles one product per call, and there is no bulk mutation for inventory at all. That is not a bug to work around, it is the shape of the platform, and the honest answer is to build the pipeline the API rewards.
What our production record shows is that this pipeline scales. On that store we read roughly 36,000 products through a single bulk query, and a throttled price and inventory job moved about 12,000 products in five to six minutes, which extrapolates to a full six-figure catalogue in under an hour, with the rate limit a non-issue because bulk operations restore fast. It is the same discipline the team brings to a five-year custom Shopify build for an EU beauty brand: read first, change deliberately, keep the way back.
Recommendations by business type
Read the catalogue, build a JSONL from the supplier feed, dry-run it, and apply through a bulk mutation. Put it behind a scoped, short-lived token and a schedule, and keep every export.
The native importer cannot do metafields and caps SKUs a day. Migrate with Matrixify and its dry-run, generate redirects for every changed handle, then drop back to a free tier once the move is done.
Let the agent write the mutation and the JSONL and read the docs, but hand it a read scope to inspect and a single write scope per job, never a login. The dry run and the export are still yours.
A spreadsheet and the native CSV import, or a careful hour in the admin, beats a bulk pipeline you will run twice. Reach for the API when the count, the frequency or the metafields make it worth it.
Frequently asked questions
- What is the safest way to change thousands of Shopify products at once?
- Export the current state first with a bulk query, build a JSONL file that carries only the fields you are changing, dry-run it on a small subset or a development store, then apply it with bulkOperationRunMutation. Keep the export: re-applying it is the only rollback you get, because a Shopify mutation cannot be undone.
- Can I bulk-update variant prices with one Admin API call?
- Not with a single mutation. productVariantsBulkUpdate updates the variants of one product per call, and the older productVariantUpdate is gone. For a whole catalogue you wrap productVariantsBulkUpdate inside a bulkOperationRunMutation driven by a JSONL file with one line per product, uploaded through stagedUploadsCreate.
- Is there a bulk mutation for inventory?
- No. Inventory is set per item and location, with no bulk-operation path. At scale you run a throttled job, a cron that updates a batch every few seconds, sized against your plan's point budget rather than fired as one giant request.
- Should I use a script or an app like Matrixify?
- For a one-off migration or a spreadsheet a non-developer will run, Matrixify and its dry-run import are the faster, safer choice, and it reaches metafields that the native CSV import cannot touch. For a repeatable, rule-based change wired into other systems, a scoped Admin API script wins. We use both, and pick by whether the job runs once or every week.
- How do I roll back a bad bulk change?
- You do not undo it, you re-apply the previous values, so a rollback only exists if you exported the before-state first. That is why the snapshot is step one and not an afterthought. Redirects need the same treatment: export the current set before you import a new one.
Primary sources
- Shopify: bulk import data with the GraphQL Admin API
- Shopify: perform bulk operations with the GraphQL Admin API
- Shopify: bulkOperationRunMutation
- Shopify: productVariantsBulkUpdate
- Shopify: metafieldsSet
- Shopify: stagedUploadsCreate
- Shopify: urlRedirectCreate
- Shopify: API rate limits
- Shopify: API versioning
- Matrixify: how it works, including the dry-run import
Technical and operational guidance, not legal advice.
UK topic cluster
Payments & ecommerce
UK checkout, tax, fulfilment and catalogue operations, grounded in production builds.
Related guide
Safe AI agent access to Shopify
The scoped, short-lived credential a bulk job should run on, never a staff login.
Case study
EU beauty brand on Shopify
A five-year custom Shopify build: read first, change deliberately, keep the way back.




Ritesh Agarwal







































