August 30, 2026

Clean up messy product data after a Shopify import

Clean up messy product data after a Shopify import

The import finished. It said it succeeded, and it did — 300 products are in your catalogue that weren't there this morning. You scroll the Products list and it looks right.

The list view is the problem. It shows you a title, a photo, and a status, which is exactly the subset of fields an import almost never gets wrong. What it doesn't show you is the product with no description at all, or the one whose supplier feed came with a single image. Those are on the product pages, one click deep each.

Almost everything written about this is about duplicates — find them, archive them, delete them. Duplicates are worth clearing, but they aren't what an import mostly leaves behind. So we went and measured it: we read the public catalogues of 41 live Shopify storefronts, grouped every product by the day it was created, and compared the products that arrived in a batch against the ones that didn't.

Key takeaways

  • You can list your own import batches in one pass, with no app and no login. https://your-store.com/products.json?limit=250 returns every published product with its created_at date, description, and images. The script in the next section groups them by day, flags every day where 20 or more products appeared at once, and scores each of those days on three checks.
  • Products that arrived in a batch are measurably worse off than the rest of the catalogue — with one important boundary, in the next point. Across 20,604 products on 41 stores, products created on a batch day (20 or more in one day) were 6x more likely to have an empty description (4.4% vs 0.7%), 2.5x more likely to have a description under 200 characters (29.7% vs 11.9%), and 2.2x more likely to have one image or none (46.4% vs 21.0%).
  • It's the very big import days doing it, not "bigger batches" in general. Split the days into non-overlapping buckets and the middle of the range turns out to be the cleanest part of the whole sample: on days of 20–49 products, 1.9% of products have a thin description, against 12.6% on days under 10. Everything in the headline comes from days of 50 or more, where thin descriptions hit 38.4% and single-image products 56.4%.
  • Two of the five checks turned out not to be import damage at all. Blank product type and missing tags run at nearly the same rate inside batches and outside them. There's a section below on why that changes the order you fix things in.

Sample: 41 live Shopify storefronts — 20,604 products — read 2026-08-26, figures computed 2026-08-30. The stores came at random from a public list of owners who posted their own URL on the Shopify Community's Store Feedback board asking for critique, so read every share here as a reason to check your own store. Method at the end.

Your own import batches, in one pass

There is no filter in the Shopify admin for "description is empty" or "has only one image," and no view that groups products by the day they were created and tells you how that day turned out. So the list has to come from outside the admin — which is easy, because Shopify publishes every product on your storefront as JSON.

Paste this into a terminal and swap in your domain. It only reads.

# save as batches.py, then: python3 batches.py
import json, urllib.request
from collections import defaultdict

STORE = "your-store.com"    # swap in your own domain
BATCH = 20                  # a day with >= this many new products = a batch

by_day, page = defaultdict(list), 1
MAXPAGE = 40                                       # 40 x 250 = 10,000 products
while page <= MAXPAGE:
    url = f"https://{STORE}/products.json?limit=250&page={page}"
    prods = json.load(urllib.request.urlopen(url))["products"]
    if not prods:
        break                                      # stop on an EMPTY page, not a short one
    for p in prods:
        by_day[p["created_at"][:10]].append(p)     # store-local date, as returned
    page += 1
else:
    print(f"!! stopped at {MAXPAGE * 250} products - raise MAXPAGE. "
          f"The numbers below cover only part of your catalogue.\n")

# same three tests we ran on the 41 stores, so your output is comparable
empty  = lambda p: len(p.get("body_html") or "") == 0
thin   = lambda p: len(p.get("body_html") or "") < 200
oneimg = lambda p: len(p.get("images") or []) <= 1

def pct(rows, test):
    return 100.0 * sum(1 for r in rows if test(r)) / len(rows)

def row(label, rows):
    print(f"{label:<16}{len(rows):>6}{pct(rows,empty):>8.1f}%"
          f"{pct(rows,thin):>8.1f}%{pct(rows,oneimg):>9.1f}%")

batch_days = sorted((d for d, r in by_day.items() if len(r) >= BATCH),
                    key=lambda d: -len(by_day[d]))
in_batch  = [p for d in batch_days for p in by_day[d]]
out_batch = [p for d, r in by_day.items() if len(r) < BATCH for p in r]

print(f"{len(batch_days)} batch days · {len(in_batch)} of "
      f"{len(in_batch) + len(out_batch)} products came from one\n")
print(f"{'day':<16}{'n':>6}{'empty':>9}{'thin':>9}{'<=1 img':>10}")
for d in batch_days:
    row(d, by_day[d])
print()
if in_batch:  row("ALL BATCHES", in_batch)
if out_batch: row("everything else", out_batch)

The day rows tell you which import to go back to first — usually one or two days carry most of the damage, and they are rarely the most recent ones. The two summary rows tell you whether your batches are actually worse than the rest of your catalogue, or whether your whole store looks like that (in which case the import isn't the story, and a different fix applies).

That gives you a date, not a worklist. To turn the worst day into the actual list of products to open, add this after the script above — it prints handles, so every line is a URL you can paste:

WORST = "2024-06-18"                     # paste the day from the table above
for p in sorted(by_day[WORST], key=lambda p: len(p.get("body_html") or "")):
    body, imgs = len(p.get("body_html") or ""), len(p.get("images") or [])
    flags = []
    if body == 0:    flags.append("no description")
    elif body < 200: flags.append("thin description")
    if imgs <= 1:    flags.append("1 image or none")
    if flags:
        print(f"https://{STORE}/products/{p['handle']}  <- {', '.join(flags)}")

If you don't work in a terminal at all, the admin gets you part of the way: Products, sort by Created, and the batch sits together as a block. You still have to open them to see which are empty, but you know which block to open.

It reads the published catalogue — products still in draft, or unpublished from the Online Store channel, don't appear, so a batch you imported as drafts is invisible to it until you publish. created_at is the date the record was created, not the day you ran the import, so a feed app that trickles products in over a week won't cluster. And "under 200 characters" counts the raw body_html including its tags, so a description wrapped in <p> markup is being credited for those characters — it's a blunt instrument for finding empty and near-empty, not a measure of writing quality. All three tests are the ones we ran on the 41 stores, so your numbers land on the same scale as ours.

What the batches looked like across 41 stores

We ran the same grouping on all 41 stores: 20,604 products, every product assigned to the day its record was created, every day with 20 or more products marked as a batch.

That produced 132 batch days across 20 of the 41 stores, and those days account for 53.5% of every product we read — over half of these catalogues arrived in bulk. The largest single day in the sample was 805 products created at once, on a day in 2015; a decade later those records are still live. Import damage doesn't age out on its own.

Here is what a batch product looks like against everything else in the same stores:

Check From a batch day (n=11,014) Everything else (n=9,590) Ratio
Description completely empty 4.4% 0.7% 6.3x
Description under 200 characters 29.7% 11.9% 2.5x
One image or none 46.4% 21.0% 2.2x
No tags 21.6% 17.9% 1.2x
No product type 38.0% 35.0% 1.1x

Descriptions and images separate cleanly. Tags and product type barely move; those two rows are in the table because we ran the checks, not because they found anything.

For context on the store-wide baseline these came out of: across all 20,604 products, 2.7% have an empty description (553 products, spread across 13 of the 41 stores), 21.4% have one under 200 characters (4,407 products, 26 stores), and 34.6% carry one image or none (7,119 products, 34 stores). The median product in the sample has 3 images. So single-image products are not a rare pathology — they're a third of these catalogues — but they are concentrated in the batches.

It's the very big days, not "big days" in general

The 20-product threshold is arbitrary, so here is the same data with no threshold at all: every product sorted into a bucket by how many products its store created that same day — non-overlapping, so nothing is counted twice:

Products created that day Products in bucket Empty description Thin description One image or none
Fewer than 10 6,956 0.7% 12.6% 21.0%
10–19 2,634 0.9% 10.0% 20.9%
20–49 2,641 1.2% 1.9% 14.7%
50 or more 8,373 5.4% 38.4% 56.4%

The 20–49 bucket is the cleanest part of the entire sample — better on both description measures than the products a merchant added a handful at a time. The whole effect lives in the last row.

That row is 8,373 products, and 12 of the 41 stores had a day that size. So the finding is narrower than "imports leave a mess": days where 50 or more products landed at once look very different from everything else, and not many stores have one. A store that added 30 products on a Tuesday shows no sign of it in this data.

What this is not. It is an association measured on one sample, not a causal test — nobody ran an experiment, and 12 stores is not many. The plainest alternative explanation is selection: a day with 500 new products usually means a supplier's catalogue somebody else wrote, and those products would have been thin however they arrived. That reading fits the numbers as well as ours does. Either way the fix is the same and the list is the same; only the blame moves.

One field the import does not explain

Blank product_type is not import damage. Inside batches it's 38.0%; outside batches, in the same stores, 35.0%. Across the whole sample, 36.6% of products (7,536) have no product type, and — the detail that settles it — in 9 of the 41 stores it is blank on every single product. Nobody imports a catalogue in a way that leaves the field blank on the 400 imported products and on the 12 the owner added by hand last month. That's a store that has never used the field.

Missing tags is a weaker version of the same thing: 21.6% inside batches vs 17.9% outside, with 4 stores having no tags on anything.

Both are worth fixing if you use those fields for collections, filtering, or navigation. Neither belongs in an "undo the import" pass, and if you fold them in you'll spend your first hour on the field with the smallest effect. Fix the ones with a 6x gap first.

The cleanup order that matches the damage

Work down the batch, not across the catalogue, and go in this order — it's ordered by how short the list is and how bad the page is, not by how satisfying it feels.

1. Empty descriptions first. Rarest and worst: 2.7% of all products, 553 in the whole sample, and 13 of the 41 stores have at least one. A product page with no description at all shows a shopper a photo and a price and nothing else, and gives a search result snippet nothing to draw from. It's also the shortest list you'll ever have — for most stores this is an evening, not a project.

2. Single-image products in your biggest batch. The largest count by far (34.6% of everything, 46.4% inside batches, 56.4% inside 50-plus batches), and the only one on this list that can be blocked by something other than time: if you don't have a second photo, you can't add one. Do this batch by batch, worst day first, and treat "needs photography" as a separate queue from "the photos exist and nobody uploaded them."

3. Thin descriptions. 21.4% of all products, 29.7% inside batches. The judgement call is which of them are actually thin — an under-200-character check flags a genuinely one-line entry and a tight, complete description for a simple product identically. Open a handful from a batch before deciding the whole batch needs rewriting.

One check we did not split by batch, so don't blame the import for it: 10.3% of the products we read are live and out of stock (2,130 products across 31 of the 41 stores). We measured that store-wide and never compared batch to non-batch, so this article can't tell you whether an import caused yours. It's still worth a look after a supplier feed lands.

Fixing a batch in the Shopify admin

For a batch small enough to work through by hand, everything you need is native.

Open Products and use the sort control to order the list by when products were added, so an import sits together as a block. Select the ones you want to work on and open the bulk editor — Shopify's spreadsheet-style grid where you choose which columns to show and edit many products in one screen. It is genuinely good for fields that are short and repetitive: adding a product type, applying tags, fixing a vendor name across a batch.

It is much less good for descriptions, because the thing that takes the time isn't typing into the cell, it's deciding what goes in it — and that decision is per product, times 300.

Images have no shortcut at all. Adding a second photo is a per-product upload, and there is no native way to say "every product from Tuesday's import needs its supplier gallery attached."

Fixing a batch by CSV re-import

For a bigger batch, the round trip is: Products → Export the batch to CSV, fill in what's missing in a spreadsheet, then Import it back with the option to overwrite products that have the same handle. Matching happens on the handle, which is why that checkbox matters — without it you get a second copy of everything you just exported, and you've turned a thin-description problem into a duplicate-products problem.

The current export template labels the description column Description, while older exports call it Body (HTML); both still import fine, so an old template you've been reusing isn't the reason a column didn't take. And export only the batch rather than the whole catalogue — a full-catalogue round trip puts every product in the store at risk of an accidental overwrite for the sake of editing 300 of them.

The CSV route moves the same job into a different window. Filling 300 description cells in a spreadsheet is still writing 300 descriptions, and a spreadsheet is a worse place to see the product you're describing than the product page is.

Where the batch is bigger than an afternoon

For thirty products, the steps above are the whole answer, and nothing on this page needs anything from us.

The point where they stop working isn't a feature you're missing — it's that every one of those 300 products needs a small, specific decision made about it while you're looking at that particular product.

That per-product work is what Arvio does. It reads your live store the way the script above does — which days came in as batches, which products in them are empty, thin, or short on images — puts the emptiest and thinnest pages at the top, and then drafts the actual fix for each one: the description written against that product's own title, options, and variants, not a template with the name swapped in. Nothing is applied until you approve it, and anything you do approve can be undone.

Bulk editing is one of the things Arvio can do once it knows what's wrong with a particular product — which is the part the script above can't do for you. The script finds the list. What's left is the product pages.

Disclosure: Arvio is our own product, and it's paid — plans start at $9.90 per 30 days, with a 5-day trial. If you imported thirty products and have an afternoon, the admin and CSV routes above are the whole job.

FAQ

How do I find which products came from a bulk import in Shopify?

Group your products by the date their record was created and look for days with an unusual number of them. There's no view in the admin that does this, but https://your-store.com/products.json?limit=250 returns every published product with its created_at date, description, and images, with no login and no app — the script in the first section of this article groups them by day, flags every day with 20 or more products, and scores each of those days for empty descriptions, thin descriptions, and single-image products. Note that it only sees published products, so a batch still in draft won't show up until you publish it.

Why do imported products end up with empty descriptions so much more often?

We measured the association, not the cause, so treat what follows as the plausible mechanisms rather than a finding: a supplier or migration feed that has no description field at all; a column that didn't get mapped during the import; or a source catalogue whose descriptions were one line to begin with. There's also a selection explanation that fits the data equally well — stores that import 500 products at once are often reselling a catalogue somebody else wrote. The pattern itself is solid: across 41 stores, 4.4% of batch-created products had an empty description against 0.7% of the rest, and the gap widened as batches got bigger.

Does a thin product description actually matter?

We make no claim about rankings or traffic — that's not something this dataset can measure. The smaller claim is the checkable one: a shopper who lands on the page sees a photo, a price, and nothing that answers a question, and a search result snippet has nothing of yours to quote. If you have to choose, the 553 products in our sample with no description at all are the better place to start than descriptions that are merely short.

Can I fix a whole import batch with a CSV re-import?

Yes, and it's the right tool once the batch is too big for the admin. Export just that batch to CSV, fill in the missing fields, and re-import with the "overwrite products with the same handle" option enabled — matching is done on the handle, and without that option you'll create a second copy of every product instead of updating it. Export the batch rather than the whole catalogue, so a mistake in the spreadsheet can only affect the products you meant to touch. CSV doesn't help with the writing. That part doesn't get smaller.

Is one image per product actually a problem?

It's the most common gap we found and the hardest to close, which is a different thing from being the most urgent. Across 20,604 products, 34.6% had one image or none — rising to 46.4% for batch-created products and 56.4% inside the biggest batches — against a median of 3 images per product overall. Whether it's a problem depends on what you sell; a simple accessory may genuinely need one photo. Where it does matter, it's the one item on the cleanup list that time alone won't fix, because if the second photo doesn't exist somebody has to shoot it.

Should I fix blank product types and tags at the same time?

Only if you use those fields, and not as part of the import cleanup. Batch products are barely worse than the rest on either one — the numbers are in "One field the import does not explain" above, along with the store-level detail that settles it. They're worth filling in if collections, filters, or navigation depend on them; they just don't belong in the same pass as the fields where batch products are 2-6x worse.

Method

Sample. 41 live Shopify storefronts, holding 20,604 products between them — an average of about 500 each. The stores came at random from a public list of storefronts whose owners had posted their own URL on the Shopify Community's Store Feedback board asking for critique. Everything here came from /products.json, an endpoint any browser can fetch. Catalogues were read 2026-08-26; the figures in this article were computed from that snapshot on 2026-08-30.

How we got to 41. We read 720 topics from that board and pulled every store hostname out of the first post: 474 candidate domains. Asking each one for /products.json, 230 still returned a catalogue. From those 230 we drew 60 at random (random.Random(20260826).sample(sorted(hosts), 60)), then dropped the ones holding fewer than 10 products and the ones whose catalogue ran past our read ceiling, which left 41.

Whether it looks like you. The biggest bias is survivorship: 244 of the 474 candidates never returned a catalogue — 118 no longer resolve, 56 return 404, 31 are frozen for non-payment, 18 answer with something that isn't JSON (a password page, or not Shopify any more), and 21 return some other error. Stores that post asking for feedback also skew newer and smaller. This is a public convenience frame, not a cross-section of Shopify. Read every share here as a reason to check your own store, not as a platform-wide rate.

What counts as a batch. A product is "in a batch" if 20 or more products in the same store share its created_at date. This is an inference: a merchant who added 25 products by hand in one sitting is counted as a batch, and an import that trickles in over several days is not counted at all. Because the threshold is a judgement, all three cuts are published above (10, 20, 50), and the direction of the effect is the same at all three.

What was counted. Descriptions are measured as the character length of body_html including its markup — "empty" means zero characters, "thin" means under 200 characters, and the thin bucket includes the empty ones. That's enough to separate empty and near-empty entries; it is not a measure of writing quality or word count. Images are len(images) from the same endpoint, with no variant-level deduplication. The endpoint returns published products only. It does not expose image alt text, so nothing here says anything about alt attributes. Stores are never named, and no store's data was read from anywhere but its public storefront.


Written by Adot Technologies Inc, the team behind Arvio: AI Store Operator — it reads your live store, finds which products came in as a batch and what each one is missing, and drafts the fix for every one of them for your approval.

Arvio: AI Store Operator

Other AI tools give you a to-do list.
Arvio's AI store agent does the work.

Arvio scans your Shopify store daily, finds what needs fixing — SEO, prices, stock, product content — and drafts the fix. You approve it, Arvio ships it. Every edit can be undone.

Install on the Shopify App Store
4.9★ rated on Shopify App StoreFree trial availableEvery edit reversible