The WooCommerce variable product CSV format, column by column

What WooCommerce's own product importer expects when you feed it variable products and variations, which columns actually decide the outcome, and which of the rules everyone repeats turns out not to be one.

Written 10 September 2026. Everything marked tested was checked by importing real files into a throwaway WooCommerce install and looking at the result, rather than by reading the documentation.

On this page

  1. A variable product is two different things
  2. The columns that decide the outcome
  3. Published, and the empty cell that breaks everything
  4. Do you really need two import passes?
  5. Local attributes or global ones
  6. Keeping re-imports from duplicating everything
  7. A minimal file that works
  8. What this page does not cover

A variable product is two different things

This is the part that trips people up, and everything else follows from it. A variable product in WooCommerce is not one record. It is a parent and a set of variations, and they hold different information.

The parent is a container. It has the name, the SKU, and the list of options that exist — every size, every colour. It has no price and no stock of its own, because nobody buys the parent.

A variation is a thing you can actually sell. It has its own SKU, its own price, its own stock, and exactly one value per attribute: size L, colour Red. It also carries a pointer back to its parent.

So a supplier file with one row per size becomes more rows than you started with, not fewer: one parent per product, plus one row per combination.

The columns that decide the outcome

The same column means different things on the two row types.
ColumnOn the parentOn a variation
Typevariablevariation
SKUthe parent's own SKU, which variations point atthe sellable SKU
Nameproduct nameoptional; WooCommerce builds one if you leave it
Published1 live, 0 private, -1 draftsee below — this one bites
Parentemptythe parent's SKU
Positionemptyorder inside the product, from 0
Regular priceemptythe price
Stockemptythe quantity
Attribute 1 nameSizeSize, spelled identically
Attribute 1 value(s)all values: S, M, L, XLone value: L
Attribute 1 visible11
Attribute 1 global0 local, 1 globalsame as the parent

A second attribute repeats the same four columns as Attribute 2 name and so on. The attribute name has to be spelled identically on the parent and on its variations, or the variation will not match the option it is meant to select.

Values are separated by commas, not pipes

WooCommerce's own schema writes multiple attribute values as Blue, Red, Green. The pipe character you see in a lot of forum answers comes from WP All Import, which is a different importer with a different convention. If a value itself contains a comma, quote the whole field the way CSV already requires.

Published, and the empty cell that breaks everything

The trap

An empty Published column is not the same as no column at all

If your file has no Published column, WooCommerce applies its default and your variations come out active. If the column exists but a variation's cell is empty, the variation is created inactive.

An inactive variation does not count. And since a parent has no price or stock of its own, a parent whose variations are all inactive shows up in the product list with no price and out of stock — even though the import reported no errors and the variations are sitting right there in the Variations tab.

This is the most misleading failure in the whole format, because nothing tells you it happened. If you write the column at all, write 1 on every variation row.

Tested: same file, same row order, that one cell changed. Empty produced priceless out-of-stock products; 1 produced a working catalogue.

On the parent, 1, 0 and -1 are three different things

The values are 1 published, 0 private, -1 draft. If you are importing a supplier catalogue into a client's shop, the one you almost certainly want is -1. Private does not mean tucked away for later: the products are in the catalogue already, visible to everyone who manages the shop. Draft is the state that leaves the decision to publish with the person doing the import.

Tested, both ways: 0 produced products listed as Private, and -1 produced Draft. It is easy to assume 0 means draft, because zero reads like "not published". It does not.

Do you really need two import passes?

The advice you will find almost everywhere is to import the parents first, wait, then import the variations. The reasoning is sound. A variation row points at its parent through the Parent column, which holds the parent's SKU, and WooCommerce has to find that SKU already in the database when it reads the row. A variation whose parent does not exist yet has nothing to attach to.

But that is a statement about order, not about the number of files. Put the parent a few rows above its variations in a single file and the dependency is satisfied just the same.

Tested

One file works

A single CSV of 121 rows — 12 parents, each immediately followed by its own variations, 109 of them in total — imported in one pass. Twelve variable products, every variation attached, prices and stock correct.

121 rows is comfortably more than one processing batch, so the parent-to-variation link survives across batch boundaries too. That was the technical worry behind the two-pass advice, and it did not materialise.

Two conditions matter. Each parent must be followed by its own variations rather than interleaving several products, and every variation row needs Published = 1 as described above. The first attempt failed on that empty cell, not on the ordering.

Local attributes or global ones

Attribute 1 global = 0 creates the attribute locally, on that product alone. It imports cleanly and it is the simplest thing to generate, which is why most exports use it.

The cost shows up later, in the shop rather than in the import. Local attributes are not shared taxonomy terms, so layered navigation — the "filter by size" widgets a clothing shop usually wants — has nothing to filter on. Converting a catalogue from local to global attributes after the fact is tedious. If the shop needs filters, it is far cheaper to decide before the first import than after the thousandth product.

Keeping re-imports from duplicating everything

WooCommerce matches on SKU. Import the same file twice and, if the SKUs are identical both times, the second pass updates the products instead of creating a second set. That makes the parent SKU the hinge of the whole thing, because unlike variation SKUs it usually is not in the supplier file: something has to invent it.

Whatever invents it has to be deterministic — the same product must produce the same parent SKU on every export — and the interesting question is what it derives from.

There is no free option here. Pick one, know which failure you are buying, and check it by importing the same file twice into a scratch install before you do it on a real shop.

Tested: re-importing an identical file left the product count unchanged.

A minimal file that works

Type,SKU,Name,Published,Parent,Position,Regular price,Stock,Attribute 1 name,Attribute 1 value(s),Attribute 1 visible,Attribute 1 global
variable,air-helmet,Air Helmet,-1,,,,,Size,"M, L, XL",1,0
variation,HLM-M,,1,air-helmet,0,59.90,12,Size,M,1,0
variation,HLM-L,,1,air-helmet,1,59.90,40,Size,L,1,0
variation,HLM-XL,,1,air-helmet,2,59.90,7,Size,XL,1,0

In WordPress: Products → All Products → Import. The importer maps these headers on its own, and the mapping screen is worth reading rather than clicking through. Anything left on "Do not import" is telling you something about your file.

What this page does not cover

Categories and images. Both have columns in the format, both are usually present in supplier files, and neither is discussed here because neither was part of what was tested. Downloadable and virtual products, tax classes and shipping classes are also out of scope.