Bulk Price Updates with Undo
End-to-end walkthrough for running a WooCommerce sale campaign via MCP — the reference scenario is "run 12% off across 47 products for a weekend flash sale, roll back cleanly Monday morning." Same pattern works for seasonal sales, clearance events, dynamic pricing tests, and end-of-quarter markdowns. Dry-run preview, snapshot-per-product commit, 72-hour undo.
WC's built-in bulk edit modifies live prices in one click with no preview + no undo. If you fat-finger a percentage, the only path back is manual per-product restore or a database backup. wc_bulk_price_update gives you the dry-run preview PLUS a per-product snapshot BEFORE the write, so undo is one MCP call away for the next 72 hours.
Prerequisites
- WooCommerce active
- Royal MCP Pro installed and licensed (see Getting Started)
- MCP client connected to your site
- Cap:
manage_woocommerce(umbrella check) + per-productedit_post - Strongly recommended: a SiteVault snapshot before the commit step on any high-blast campaign (100+ products). The 72h undo is your primary net; SiteVault is belt-and-suspenders.
The full campaign workflow
Identify the product set
Decide which products are in scope. Ask your AI to build the ID list from a category, tag, price range, or SKU pattern:
Get all products in category "Summer 2025 Collection" using wc_get_products. Give me the array of product IDs and their current regular_price + sale_price.Or if you already have the IDs from a spreadsheet, skip this step and go to step 2.
Compute new prices (client-side, in the AI)
Have your AI apply the campaign math to each product and build the updates array:
For each of those products, compute a new sale_price at 12% off the regular_price rounded to .99 (e.g. regular $50 → sale $43.99). Build me the updates array for wc_bulk_price_update.The output will look like:
updates: [
{ "product_id": 42, "sale_price": "43.99" },
{ "product_id": 108, "sale_price": "26.39" },
{ "product_id": 143, "sale_price": "17.59" },
...
]Dry-run the price update
Call wc_bulk_price_update with dry_run: true. Nothing is written — you get back the list of products that would be touched, plus which would be skipped or errored:
Preview the price update with wc_bulk_price_update dry_run=true using the updates array above.Response shape:
{
"isError": false,
"content": [
{ "type": "text", "text": "Updated prices on 47 products (0 skipped, 0 errors) [dry-run]." }
],
"structuredContent": {
"updated": [42, 108, 143, ...],
"skipped": [],
"errors": [],
"dry_run": true,
"updated_count": 47,
"skipped_count": 0,
"errors_count": 0
}
}If anything appears in skipped or errors, review before committing. Common reasons:
skipped: [{ id, reason: "insufficient_caps" }]— the acting user doesn't haveedit_poston that productskipped: [{ id, reason: "no_price_fields" }]— the update row didn't includeregular_priceORsale_price(at least one is required)errors: [{ id, reason: "not_found" }]— product ID doesn't existerrors: [{ id: 0, reason: "invalid_product_id" }]— malformed row missingproduct_id
Commit the campaign
Once the dry-run looks clean, call the tool again with dry_run: false:
Commit the price update with wc_bulk_price_update dry_run=false using the same updates array.What happens per product on the server:
- The tool snapshots the current
regular_priceandsale_priceBEFORE mutating (line-level guarantee: the undo payload reflects true pre-state, not a race). - If
regular_priceis in the row,$product->set_regular_price()is called. - If
sale_priceis in the row,$product->set_sale_price()is called (passingnulldeletes the sale price and reverts to regular). $product->save()commits.wc_delete_product_transients($pid)fires so cached shop/category displays refresh.
The response includes a 72-hour undo token you'll need for step 5:
{
"isError": false,
"content": [{ "type": "text", "text": "Updated prices on 47 products (0 skipped, 0 errors)." }],
"structuredContent": {
"updated": [42, 108, 143, ...],
"skipped": [],
"errors": [],
"dry_run": false,
"updated_count": 47,
"skipped_count": 0,
"errors_count": 0
},
"undo": {
"token": "<64-character-hex-token>",
"expires_at": "2026-08-08T14:23:15Z",
"ttl_hours": 72
}
}Save the undo token somewhere durable. Paste it into your project notes, a Slack DM to yourself, whatever. You'll need it for step 5 — and you have 72 hours before it expires.
Verify on the storefront
Load the actual shop / category pages in an incognito browser (bypass logged-in admin caching) and check:
- Sale prices render on product cards (regular price shown struck-through, sale price highlighted)
- Cart / checkout math uses the new prices
- Product structured data (view page source, look for
schema.org/Product) reflects the new prices — important for Google Shopping / SERP snippets - Any active FB/Google product catalog sync picks up the new prices on next sync (usually within 24hr)
If prices don't render but the tool response said success:
- Purge any page-cache plugin (WP Rocket, LiteSpeed Cache, W3 Total Cache) — the tool fires
wc_delete_product_transientsbut page caches are a separate layer - Purge CDN cache (Cloudflare, BunnyCDN, etc.)
- If using ForgeCache Pro, ask your AI to run
fc_purge_urlorfc_clear_cache
End the campaign — undo OR set-back
Two paths depending on how long the campaign ran:
Path A: Short campaign, still within 72h
Call royal_mcp_undo_last_operation with the token from step 4:
Undo the price campaign with royal_mcp_undo_last_operation using token <64-character-hex-token>.The undo_restore_product_meta handler loops through every snapshotted product and restores the prior regular_price + sale_price per product exactly. One call, campaign fully reverted.
Path B: Long campaign, past the 72h window
The undo token has expired. Path back: re-run wc_bulk_price_update with the original prices, passing them explicitly. If you captured the original prices in step 1 (asking the AI for current prices at the start of the workflow), those values are still in your chat transcript. Re-issue the same tool call structure:
updates: [
{ "product_id": 42, "regular_price": "50.00", "sale_price": "" },
{ "product_id": 108, "regular_price": "30.00", "sale_price": "" },
...
]Passing sale_price: "" or sale_price: null deletes the sale price and reverts the product to displaying its regular price only. This "re-run in reverse" is not as clean as the undo path but works past the TTL window.
Multi-batch campaigns (100+ products)
The tool doesn't hard-cap the updates array size, but for portfolios above ~100 products consider batching for two reasons:
- Undo granularity. Each batch gets its own undo token. If batch 3 of 5 has a bad calculation, you can undo just that batch without touching batches 1, 2, 4, 5.
- MCP client transport reliability. Some MCP clients have loose timeouts on large single-response payloads. Batching keeps each response tight.
Recommended batching pattern
- Group products by category or by SKU prefix — whatever's semantically meaningful for your store
- Run
wc_bulk_price_updateper group with dry_run first, then commit - Verify each group's storefront section before starting the next
- Keep a running list of undo tokens (one per batch) mapped to what each batch covered
- At end-of-campaign, undo the batches in reverse order (5, 4, 3, 2, 1) so the storefront transitions cleanly
Special cases
Variable products
Passing a variable-product's parent ID to wc_bulk_price_update updates the parent's stored regular_price / sale_price fields, but WooCommerce derives the display price from variations at render time. For a real variable-product campaign, either pass variation IDs directly to wc_bulk_price_update (each variation is a separate post with its own ID and its own regular/sale prices), or use the free-tier wc_batch_update_variations tool which takes a parent product ID and variation-level update rows in one call.
Percentage-off vs fixed-price
The tool takes literal price values, not percentages. Have your AI compute the percentage-off math client-side and pass the resulting prices in the updates array. Rounding is your call (round to .99, round to nearest integer, keep raw decimal — whatever your pricing psychology prefers).
Scheduled sales
wc_bulk_price_update writes prices immediately. If you want prices to activate at a future date, use WooCommerce's built-in scheduled sale-price fields per product — this tool doesn't touch _sale_price_dates_from or _sale_price_dates_to meta. For scheduled bulk campaigns, cron-schedule two Royal MCP calls: one to set prices at start-time, one to restore at end-time.
Site with WC Subscriptions
Existing subscription prices are calculated from the subscription's own recorded price, not the product's current price. Changing a product's regular_price or sale_price via this tool does NOT retroactively change what active subscribers pay on their next renewal — that's WC Subscriptions' by-design behavior. For new subscribers signing up during your campaign, they'll get the sale price locked into their subscription record.
Still Stuck? Two-Step Support Path
If the price campaign isn't running the way you expected, work through these two steps in order.
Step 1: Start with the Royal MCP Troubleshooting Guide
Royal MCP Troubleshooting — Start Here covers MCP-layer issues.
For Pro-specific patterns:
- Undo token errors — if step 5 fails or your token expired
- Pro tool returned upgrade prompt with valid license
Step 2: Email priority support
If you've worked through Start Here and the relevant Pro-specific doc and the issue still isn't resolved, email priority support from your purchase email address at support@royalplugins.com. Priority email support is included with your license — typical response within 24 hours. Never include your license key in email; we look it up from your purchase address.
- Your hosting provider
- Royal MCP Pro version + WooCommerce version from WP Admin → Plugins
- Whether HPOS (High-Performance Order Storage) is enabled — check WP Admin → WooCommerce → Settings → Advanced → Features
- WC Subscriptions version if installed
- Active caching plugins (LiteSpeed Cache, WP Rocket, W3 Total Cache, SpeedyCache, ForgeCache, none)
- Which MCP client
- The full updates array you passed (or a redacted-to-shape sample if it contains sensitive pricing you'd rather not share — we just need to see the shape)
- The full response including
updated/skipped/errorsarrays - Whether step 5 (undo) was attempted and what the undo response was
- Screenshot of the Pro tool row in Audit Log with View Details expanded