--- name: shopify-category-taxonomy description: >- Use when you assign the Shopify Standard Product Taxonomy category, "set the product category", "categorize the catalog", or fill the category / taxonomy metafields: the admin "Category metafields" panel (Color, Size, Material, Caffeine content), a.k.a. product category attributes / taxonomy attributes, including the attributes a Google feed asks for. Covers bulk classification, discovering a category's attributes and allowed values, minting the reserved value metaobjects, and writing them safely. Also: "category metafields show blank/null", "my Color attribute won't save". Not for bulk CSV data loads (Matrixify can't do this; see shopify-matrixify for bulk data). compatibility: >- Shopify Admin API 2025-07 (GraphQL). Needs read_products + write_products AND read_metaobjects + write_metaobjects. The metaobjects scopes are the #1 trap: without read_metaobjects the values return null and look unwritable. --- # Shopify Category Taxonomy Two jobs that chain together: **assign** a product's Standard Product Taxonomy category (Part 1), then **fill** the category metafields that category exposes (Part 2). You cannot do Part 2 until Part 1 is true: the "Category metafields" panel only exists once a product is assigned to a category. Bulk CSV data work lives in the sibling `shopify-matrixify`; the one thing Matrixify **cannot** do is mint the reserved value metaobjects Part 2 needs, which is the entire reason this skill exists. ## Store access **Lane A: custom-app token (scriptable).** Shopify admin → Settings → Apps and sales channels → Develop apps → create an app → grant the four scopes below → install → copy the Admin API access token. Export it; never write it to disk: ```bash export SHOPIFY_STORE="your-store.myshopify.com" export SHOPIFY_ACCESS_TOKEN="" # env, not disk curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \ -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \ -H "Content-Type: application/json" -d '{"query":"{ shop { name } }"}' ``` Minimum scopes: **`read_products`, `write_products`, `read_metaobjects`, `write_metaobjects`.** The metaobjects pair is not optional (see the null trap). **Lane B: Shopify CLI OAuth (no stored token).** `shopify store auth --store $SHOPIFY_STORE --scopes read_products,write_products,read_metaobjects,write_metaobjects` then `shopify store execute`. Good for token-less stores where the owner logs in interactively. **Toolkit preflight.** Lane B rides on the Shopify CLI: run `shopify version` first and install it if missing. For the full Admin GraphQL schema and validated execution, pair this skill with Shopify's official AI toolkit plugin. In Claude Code, check `claude plugin list`; if it isn't there: ```bash claude plugin marketplace add Shopify/Shopify-AI-Toolkit claude plugin install shopify-plugin@shopify-ai-toolkit ``` Recommended, not required: Lane A needs only curl and a token. The toolkit gives your agent the API; this skill gives it the playbook. ## Recipes, not scripts Treat every GraphQL block as a copy-and-adapt recipe your agent writes fresh, runs, and discards. Two rules keep that safe: 1. **Preview before you mutate.** Precede every write with a read-only count of the products in the target category and sanity-check it. A count far above expectation means stop and re-scope. 2. **Non-destructive defaults.** Fill missing only; a blank value in `metafieldsSet` **deletes** the metafield, so only emit non-blank values. Abort the run on first-batch `userErrors`. Verify against ground truth via the Admin API, not the storefront (CDN-cached, lies about freshness). Never print or commit the token. ## Part 1: Assign the category Set each product's `category` field to a Standard Product Taxonomy category via `productUpdate` (or the Matrixify `Category` column for a pure bulk load). **CRITICAL: verify every category ID against the official taxonomy source; never guess an ID.** A guessed taxonomy ID resolves to the *wrong* category or a dead node, and it fails silently: the product looks categorized but every downstream attribute, filter, and feed mapping is now wrong. Guessed IDs have burned real product feeds. Resolve the ID by `search:` against `taxonomy { categories }` (Part 2 step 1 shows the query), or from the official taxonomy repo linked in Provenance. Classification **signal priority** (highest to lowest confidence): **tags** (highest on stores that tag by category: a "Mango" tea tagged `black-tea` is a flavored black, not a fruit infusion, and the tag says so where the title misleads) > **product type** (good when the store populates it, many don't) > **title / vendor** (lexical fallback; a mono-line vendor pins the category, a multi-line one doesn't) > **description** (last resort; verbose and easy to mis-key on flavor words). Emit a confidence flag per product and split the catalog into high-confidence vs ambiguous (bundles, samplers, cross-category items). Ship the high-confidence rows; route the ambiguous ones to a human review sheet rather than guessing. See the SOP's pull-then-classify steps below. ```graphql # preview (read-only): how many products your classifier targets, before any write query { productsCount(query: "tag:black-tea status:active") { count } } # then, per high-confidence product, assign the category mutation Assign($id: ID!, $cat: ID!) { productUpdate(product: {id: $id, category: $cat}) { product { id category { id fullName } } userErrors { field message } } } ``` ## Part 2: Fill the category metafields A category metafield is **not** a plain text/choice value. It is a metafield in the reserved `shopify` namespace, of type `list.metaobject_reference`, whose value is a JSON array of **Metaobject GIDs**. Each of those metaobjects is a reserved taxonomy value (`type: "shopify--"`) with a `label` and a `taxonomy_reference` pointing at a global `TaxonomyValue`. The chain: ``` Product └─ metafield shopify. (list.metaobject_reference) └─ value: ["gid://shopify/Metaobject/…"] └─ Metaobject type: shopify-- label: "Decaf" └─ taxonomy_reference ─▶ TaxonomyValue/26137 (global) ``` These value metaobjects are **minted on demand**: a store only holds metaobjects for values it has actually used. A brand-new value must be created before you can reference it. That mint-then-reference step is what Matrixify cannot do. **Top 5 gotchas (fuller list in the reference):** 1. **The null/scope trap.** Without `read_metaobjects`, the value metaobjects return `null` from `node()`, `references`, and `metaobjectDefinitions`: the whole feature looks read-only. With the scope they resolve. Minting needs `write_metaobjects`. 2. **A `TaxonomyValue` GID is rejected on write.** `metafieldsSet` demands the **Metaobject** GID, not the taxonomy value GID (else "Value require that you select a metaobject"). The TaxonomyValue GID is only the `taxonomy_reference` you pass when *creating* the metaobject. 3. **The product must already be in the category** or the metafield doesn't render as a category metafield. Scope targets by `product.category`, not by product type or title. 4. **GIDs are store-specific.** Metaobject GIDs minted in one store do not port to another. Re-mint per store. 5. **Matrixify cannot mint these.** It can't create the reserved metaobjects and won't resolve value names. The Admin API is the only reliable path. ### SOP **1. Discover the category, its attributes, and allowed values.** ```graphql query { taxonomy { categories(first: 1, search: "Tea & Infusions > Tea") { nodes { id fullName attributes(first: 50) { nodes { ... on TaxonomyChoiceListAttribute { id name values(first: 50) { nodes { id name } } # id = TaxonomyValue GID } ... on TaxonomyMeasurementAttribute { id name } } } } } } } ``` Record per attribute: the **metafield key** (the attribute handle, lowercased-hyphenated, confirm it by reading an already-set product in step 6; the Color attribute's key is `color-pattern`, not `color`) and the **value name → TaxonomyValue GID** map. **2. Pull the catalog, scoped to the target category.** Do not classify blind: page every candidate product first, capturing the signals the classifier reads. ```graphql query($cursor: String) { products(first: 100, after: $cursor, query: "status:active") { pageInfo { hasNextPage endCursor } nodes { id handle title vendor productType tags category { id fullName } options { name optionValues { name } } metafields(namespace: "shopify", first: 25) { nodes { key jsonValue } } } } } ``` For a large catalog, run this as a `bulkOperationRunQuery` dump instead of live pagination; the dump doubles as your pre-write backup. **3. Classify into high-confidence vs ambiguous.** Apply the signal priority (tags first). Emit a confidence flag per product; the high-confidence rows ship, the ambiguous ones (bundles, samplers, cross-category, low-signal titles) route to a review spreadsheet a human corrects before you write anything. This split is what let one tea retailer ship **583 of 717** products high-confidence and hold the remaining 134 for review instead of guessing. Assign the category (Part 1) on the high-confidence rows now; then continue to mint/set below. **4. Mint each missing value metaobject (once per store).** Reuse an existing metaobject when its `label` already matches; only mint when genuinely absent. ```graphql mutation M($m: MetaobjectCreateInput!) { metaobjectCreate(metaobject: $m) { metaobject { id displayName } userErrors { field message code } } } # variables: # {"m":{"type":"shopify--caffeine-content","handle":"decaf", # "fields":[{"key":"label","value":"Decaf"}, # {"key":"taxonomy_reference","value":"gid://shopify/TaxonomyValue/26137"}]}} ``` Build a `value name → Metaobject GID` map from the results plus existing entries. **5. Apply via `metafieldsSet`, batches of ≤25 metafields per call.** ```graphql mutation S($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { id } userErrors { field message code } } } # each entry: {ownerId, namespace:"shopify", key:"tea-variety", # type:"list.metaobject_reference", # value:"[\"gid://shopify/Metaobject/…\"]"} ``` Emit only non-blank values. Abort on first-batch errors. **6. Verify by reading back resolved labels**, not by trusting the write's `userErrors: []`. ```graphql query { product(id: "gid://shopify/Product/…") { metafields(namespace: "shopify", first: 10) { nodes { key references(first: 5) { nodes { ... on Metaobject { displayName } } } } } } } ``` Spot-check several products; confirm every name resolves to what you intended. ## References - [references/gotchas-and-scoping.md](references/gotchas-and-scoping.md): gotchas 6-10 (freeform labels, per-type mint fields, multi-category scoping), which surface each attribute reaches (Google feed vs Search & Discovery vs JSON-LD), and two worked examples with real numbers. ## Provenance and maintenance Last verified: 2026-07-05. GraphQL pinned to Admin API 2025-07; Shopify deprecates versions on a rolling quarterly schedule, so verify field shapes against [shopify.dev](https://shopify.dev/docs/api/admin-graphql) and the metafields/metaobjects docs before trusting a version-specific claim. The category IDs and taxonomy values come from Shopify's [Standard Product Taxonomy](https://github.com/Shopify/product-taxonomy): the canonical source to verify an ID against; never guess one. Read-only re-verification a stranger can run (no writes): ```bash # 1. token reaches the store and reads a product's assigned category curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \ -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query":"{ products(first:1){ nodes{ category { id name } } } }"}' # 2. taxonomy discovery resolves (confirms the taxonomy API + your read scope) curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \ -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query":"{ taxonomy { categories(first:1, search:\"Tea\"){ nodes{ id fullName } } } }"}' ``` These skills capture operational lessons the docs don't; they are not a replacement for the reference.