What happens when two people or apps change the same Shopify product before an approved update is published?

How do Shopify teams and agencies handle overlapping product changes?

For example:

  • Operator A prepares a product update and sends it for review.
  • The change is approved.
  • Before it is published, Operator B, the client, another app, a CSV import, or an automation changes the same product.
  • The originally approved update is then ready to go live.

At that point, what happens in your workflow?

Do you:

  • publish the approved version anyway;
  • compare it with the current live state first;
  • invalidate the approval and review the change again;
  • let the latest edit win;
  • detect the conflict only after something looks wrong?

I’m particularly interested in cases where both changes affect the same field, such as price, title, description, metafields, images, or variant data.

For agencies managing multiple client stores, how do you prevent an approved change from becoming stale between review and publication?

And what tools or processes do you use to detect that the live product has changed since the approval was given?

We treat an approval as a snapshot, not a publish ticket that stays valid forever.

Before anything goes live we always re-pull the current product (or at least the fields in the change set) and compare it to what was approved. If the same field moved since approval, the approval is invalid and it goes back through review with both diffs side by side. If only unrelated fields changed, we can usually publish the approved fields on top of the live product without a full re-review, but we still log that a concurrent edit happened.

“Latest edit wins” without a compare is how you ship stale titles over a price change someone else just fixed, so we don’t do that. Apps and CSV imports are treated the same as a human: if they touched a field in the approved set after the approval timestamp, it’s a conflict.

Practically that means:

  1. Record approval time + the exact field values approved.
  2. On publish, fetch live values again.
  3. Same-field drift = re-review. Unrelated drift = publish approved fields only.
  4. Prefer one writer per product when a campaign is in flight (lock in a sheet or pause the sync app for that SKU).

We don’t have a native Shopify “product checkout” for this, so the compare step is all on the workflow side.

Hi @CommerceGov Welcome To Shopify Community So The Most setups just let the latest edit win by default since Shopify doesn’t track “approved but not yet published” as a separate state, it’s a real gap. The safer pattern is re-fetching the live product right before publish and diffing it against what was originally approved, if the same fields changed, flag for re-review instead of auto-publishing. Some agencies also lean on Shopify’s activity log to catch drift, but that’s reactive, not preventive.

Hi there @CommerceGov
In our process, the approval applies to the specific product state that was reviewed, not just the change requested. Before releasing a new version, we compare the current product to the accepted one. We hold off on publication and re-validate the update if the pertinent fields have been modified.

For the bulk of a catalog, we also maintain a changelog with timestamps and ownership, and we don’t overlap bulk edits with review periods. This is particularly true for price, stock, variants, and metafields, as a unrelated update has the potential to wipe out prior approved work.

Good question, and one that doesn’t have a tidy platform-level answer, because Shopify has no native concept of “pending approval” or optimistic locking on products. Every write to a product via the Admin API or CSV is last-write-wins. There’s no conflict detection baked in.

So the gap has to be filled by workflow and tooling. Here’s how the problem is typically handled in agency and multi-operator setups:

Snapshot-before-publish
Before applying the approved change, take a fresh read of the live product and compare it field-by-field against what was read at approval time. If anything has changed, surface the diff to whoever is about to publish. This is the most reliable approach but it requires a tool that holds the “approved state” snapshot and can compare it to the current state. Most teams don’t have this by default.

Field-scoped locking
Rather than locking the whole product, track which fields are “in review”. If a pending approval covers price and title, and an intervening change touches only the description, that’s safe to let through. If it touches price, flag it. This is fiddly to implement but much less disruptive than blanket invalidation.

Invalidate-on-any-change
Simpler and safer for regulated contexts (price accuracy, legal copy). Any change to the product after approval is issued resets the review. More rework, but no risk of publishing a stale-approved version over a live change.

Audit-log comparison
Shopify’s productVariantUpdate and related mutations return the updated resource. Apps that log every write (their own writes, plus anything they can detect via webhooks) can reconstruct a change log per product. The PRODUCTS_UPDATE webhook fires on any change to the product regardless of source, so you can track “did anything touch this product between approval time and publish time” just by listening.

Accept last-write-wins with visibility
Some agencies simply publish the approved version and treat conflicts as an ops problem to catch in QA. This works at low volume; it falls apart when multiple parties have access.

On the “what tools” part: most teams doing this at scale are either building it themselves on top of the webhook stream, or using a PIM (Akeneo, Plytix, etc.) as the source of truth with Shopify as the output, which sidesteps the conflict issue because the PIM owns approval and pushes final state. For teams that want field-level backup and restore without a full PIM, Product Save & Sync (disclosure: I built it) snapshots full product state including variants, metafields, metaobject references, and images, and can restore from any prior snapshot. That gives you the “compare live state to approved snapshot” primitive, though the workflow guard (detect conflict before publish) would still need to sit on top.

The re-fetch and diff approach above is the right shape, but there is a hole in it worth naming, because it bit us.

Diffing only the fields in your change set assumes every write path is field-scoped. Some are not. productSet treats list-type fields as the complete list, so sending a product without its metafields, tags or options removes the ones you left out rather than preserving them. CSV import has its own version of this: leave the option columns out and existing variants get deleted. Which means Operator A’s approved change can touch nothing but the description, pass a clean field-level comparison, and still wipe what another app wrote while the approval sat in the queue. The conflict never appears in your diff because it was never in your change set.

Two things follow. Diff the whole product rather than the change set whenever the publish path is an import or a set-style mutation. And treat apps as operators: they write on schedules nobody is watching, so a review window measured in hours is plenty of time for a feed or a tagging job to land.

The forensics afterwards are also thinner than people expect. The store activity log is view-only, cannot be exported, and shows at most 250 results, so “who changed this and when” runs out quickly. Whatever snapshot you took before publishing is usually the only record that outlives the week.

Disclosure: snapshots are what I sell (StoreVault), so discount accordingly - though the whole-product diff costs nothing and needs no app at all.

Everyone’s right that re-fetching and diffing beats letting the latest edit win, so I’ll add the two things that bit us rather than repeat it.

If you detect drift from updated_at alone, you’ll get false conflicts constantly. It moves for inventory syncs and for any app touching an unrelated metafield, so an approval that only changed the description looks stale when nothing you reviewed actually moved. Compare the fields in your change set, not the timestamp.

The other is ordering. We consume products/update across a few hundred stores, and bulk jobs fire them in bursts. Shopify doesn’t guarantee ordering within a topic and retries happen, so a stale payload can land after a newer one. Order by X-Shopify-Triggered-At or the payload’s updated_at and discard anything older than what you’ve stored.

The re-fetch step still needs a race-condition test: have a second operator change the price after the final comparison but before the approved write. A clean read is not, by itself, a lock.

For the review screen, I would keep three values per affected field: original / approved / live. A useful decision table is:

  • Live equals approved: already at the intended value; don’t blindly write it again.
  • Live equals original: eligible for the next publishing check, not guaranteed conflict-free.
  • Live differs from both: stop that item and show the reviewer both changes.
  • Write timed out: outcome unknown; read back before deciding whether a retry is safe.

For a CSV or other whole-record write, apply this to everything the payload may change, not only the field the operator intended to edit. Coordinate writers during the publishing window and verify the result afterwards; a later scan cannot prevent an earlier overwrite.

Disclosure: I develop CatalogDelta, a read-only checker, not a locking/approval system. This is AI-assisted test guidance, not a report of a client incident.