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
| Column | On the parent | On a variation |
|---|---|---|
| Type | variable | variation |
| SKU | the parent's own SKU, which variations point at | the sellable SKU |
| Name | product name | optional; WooCommerce builds one if you leave it |
| Published | 1 live, 0 private, -1 draft | see below — this one bites |
| Parent | empty | the parent's SKU |
| Position | empty | order inside the product, from 0 |
| Regular price | empty | the price |
| Stock | empty | the quantity |
| Attribute 1 name | Size | Size, spelled identically |
| Attribute 1 value(s) | all values: S, M, L, XL | one value: L |
| Attribute 1 visible | 1 | 1 |
| Attribute 1 global | 0 local, 1 global | same 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
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.
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.
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.
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.
- From the product name. Simple and readable. It also means that the day the supplier renames Trail Jacket to Trail Jacket II, you get a second product instead of an updated one.
- From a variation SKU prefix. Survives renames, but depends on the supplier's codes being structured, which is not always true.
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.
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.