Support / Royal MCP Pro / Bulk Price Updates with Undo

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.

Why this over WooCommerce's built-in bulk edit

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-product edit_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 have edit_post on that product
  • skipped: [{ id, reason: "no_price_fields" }] — the update row didn't include regular_price OR sale_price (at least one is required)
  • errors: [{ id, reason: "not_found" }] — product ID doesn't exist
  • errors: [{ id: 0, reason: "invalid_product_id" }] — malformed row missing product_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_price and sale_price BEFORE mutating (line-level guarantee: the undo payload reflects true pre-state, not a race).
  • If regular_price is in the row, $product->set_regular_price() is called.
  • If sale_price is in the row, $product->set_sale_price() is called (passing null deletes 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_transients but page caches are a separate layer
  • Purge CDN cache (Cloudflare, BunnyCDN, etc.)
  • If using ForgeCache Pro, ask your AI to run fc_purge_url or fc_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

  1. Group products by category or by SKU prefix — whatever's semantically meaningful for your store
  2. Run wc_bulk_price_update per group with dry_run first, then commit
  3. Verify each group's storefront section before starting the next
  4. Keep a running list of undo tokens (one per batch) mapped to what each batch covered
  5. 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:

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.

What to include in your email
  • 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 / errors arrays
  • 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