You export metaobjects from your staging store, import them into production, and every row with a product reference fails. The file looks fine. Each row carries a value like gid://shopify/Product/8123456789012, which is a perfectly valid ID. It just belongs to the staging store.
GIDs are how Shopify stores relationships, and they’re the right thing for code to use. They’re the wrong thing to put in a spreadsheet that a person edits, or in a file that moves between stores. Here’s what each identifier is, where each one belongs, five ways GID-based imports go wrong, and how to resolve handles into GIDs at import time instead.
What a GID is
A GID (global ID) is the identifier Shopify’s GraphQL APIs use for every object. The format is always the same:
text
gid://shopify/{object_name}/{id}
gid://shopify/Product/8123456789012
gid://shopify/ProductVariant/45123456789012
gid://shopify/Collection/612345678901
gid://shopify/Metaobject/123456789
gid://shopify/MediaImage/987654321
Reference fields store GIDs. When a metaobject has a product_reference field, the value Shopify keeps is the product’s GID; a metaobject_reference keeps a gid://shopify/Metaobject/...; a file_reference keeps the GID of a file such as a MediaImage. Over the API, all of these are read and written as strings.
Shopify’s own data-modeling guide describes the GID as always unique and immutable. That’s exactly what makes it a good key inside one store, and exactly why it can’t travel: the number is assigned by the store that created the object, and no other store has it.
What a handle is
A handle is the human-readable identifier: slim-fit-chinos, mens-shirts, eco-friendly-badge. Products, collections and metaobject entries all have one.
- For metaobject entries, a handle is unique within a type. The Admin API looks an entry up by type and handle together:
metaobjectByHandle(handle: { type: "size_guide", handle: "slim-fit-chinos" }). - It’s generated from the entry’s display name, but you can set your own.
- Shopify’s guidance is to use handles for readable identifiers, URLs, and moving data between stores.
The catch is that handles can be edited. Shopify’s help docs warn against editing an entry’s handle once it’s in use, because anything that looks the entry up by handle (Liquid, for example) stops finding it. Reference fields aren’t affected, since they hold GIDs. So the rule is: handles are stable enough to import with, as long as nobody renames them halfway through.
| GID | Handle | |
|---|---|---|
| Example | gid://shopify/Product/8123456789012 |
slim-fit-chinos |
| Readable by people | No | Yes |
| Same across stores | No, assigned per store | Yes, if you create it with the same handle |
| Can change | No | Yes, if someone edits it |
| Where it belongs | Reference fields, API calls, your code | Spreadsheets, CSV imports, Liquid lookups, URLs |
Five ways GID-based imports break
1. Copying between stores
Staging, production, a regional expansion store: each one assigns its own IDs. The “Slim Fit Chinos” product is .../Product/8123456789012 on one and something entirely different on the other. A file full of GIDs exported from store A is a file full of dead references on store B.
2. Delete and recreate
Delete a product and import it again, and it comes back with a new GID. Every reference to the old one is now pointing at nothing. Shopify also removes deleted products and variants from list metafields that reference them, so for list fields the link isn’t just stale; it’s gone, and re-importing the product won’t restore it.
3. Spreadsheet number mangling
Someone strips the gid://shopify/Product/ prefix to “clean up” the column, leaving 8123456789012. Excel sees a 13-digit number, displays it in scientific notation (8.12346E+12), and when the file is saved as CSV that’s what gets written. The full ID is gone from the file. Keep the prefix, or better, don’t put IDs in the spreadsheet at all.
4. Handles stored in text fields
The reverse mistake: avoiding GIDs by storing a product handle in a single_line_text_field and treating it as a link. Shopify’s data-modeling docs warn against exactly this. Liquid and the Storefront API can’t resolve a text value into the object it names, so the theme gets a string, not a product. Relationships belong in reference fields. The handle belongs in your import file, and the tool turns it into a GID.
5. Entries that reference other entries
A metaobject_reference can only point at an entry that already exists. If your file creates a “Designer” entry and a “Collection story” entry that references it, the designer has to be created first. Shopify’s bulk mutation runner doesn’t process lines in order, so putting both in one run can fail on ordering alone, and a simple script that processes rows top to bottom fails whenever the referenced row happens to sit lower in the file.
The model that works: handles in the file, GIDs in the store
Keep human-readable keys in the file, and resolve them into GIDs at the moment of import:
- For every reference column, collect the distinct handles.
- Look each one up on the target store.
- If a handle doesn’t exist, fail that row before anything is written, with a message that names the handle.
- Write the resolved GIDs into the reference fields.
The lookups are short. For an entry:
graphql
query EntryId {
metaobjectByHandle(handle: { type: "size_guide", handle: "slim-fit-chinos" }) {
id
}
}
For a product:
graphql
query ProductId {
productByIdentifier(identifier: { handle: "slim-fit-chinos" }) {
id
}
}
For entry-to-entry references, import in two passes. Pass one creates or updates the entries that get referenced (designers, materials, size-chart rows). Pass two imports the entries that point at them. Because both passes key on handles, you can rerun either one safely: matching handles update instead of duplicating.
The same model is what makes a store-to-store move work. Recreate products and collections with the same handles, import entries keyed on handles, and every reference resolves to the new store’s GIDs without anyone ever seeing one.
A pre-import checklist for references
Run this before any import that touches reference fields:
- Every product and collection handle exists on the target store. Not the store you exported from; the one you’re importing into.
- No handles were edited since the last export. An edited entry handle creates a new entry instead of updating the old one. An edited product handle means your file points at a handle that no longer exists.
- Referenced entries are imported before the entries that reference them. Two files, two passes.
- File URLs are direct links to the file, ending in the file itself, not a cloud-drive share page.
- No bare numeric IDs in the spreadsheet. If a column of numbers has to be there, format it as Text before you paste.
When a reference does fail, the error should name the row and the value. No product found with handle linen-shirt-blue is something a merchant can fix in thirty seconds; a stack trace from a half-finished script isn’t.
For the full import workflow, see how to bulk import Shopify metaobjects via CSV. If you’re moving metaobjects to a new store, back up and restore by handle covers the order of operations. And for when to use which reference type in the first place, see structuring custom data on Shopify.