$npx -y skills add 40RTY-ai/shopify-admin-skills --skill shopify-admin-bundle-availability-checkRead-only: for native bundle products and metafield-defined bundles, verifies every component variant has sufficient stock to fulfill the bundle's effective availability.
| 1 | ## Purpose |
| 2 | Walks every product flagged as a bundle (either via Shopify's native `requiresComponents` mechanic or a `bundle.components` metafield convention), then verifies each component variant has sufficient inventory to back the bundle's quantity ratio. Surfaces bundles that are listed as in-stock on the storefront but cannot actually be fulfilled because one component has run out. Read-only — no mutations. |
| 3 | |
| 4 | ## Prerequisites |
| 5 | - Authenticated Shopify CLI session: `shopify store auth --store <domain> --scopes read_products,read_inventory` |
| 6 | - API scopes: `read_products`, `read_inventory` |
| 7 | |
| 8 | ## Parameters |
| 9 | |
| 10 | | Parameter | Type | Required | Default | Description | |
| 11 | |-----------|------|----------|---------|-------------| |
| 12 | | store | string | yes | — | Store domain (e.g., mystore.myshopify.com) | |
| 13 | | metafield_namespace | string | no | bundle | Metafield namespace where bundle component definitions live | |
| 14 | | metafield_key | string | no | components | Metafield key that holds the JSON list of `{variantId, quantity}` | |
| 15 | | safety_stock | integer | no | 0 | Treat component as out-of-stock if on-hand minus this buffer is below required | |
| 16 | | only_listed | bool | no | true | Only check bundle products with status `ACTIVE` | |
| 17 | | format | string | no | human | Output format: `human` or `json` | |
| 18 | |
| 19 | ## Safety |
| 20 | |
| 21 | > ℹ️ Read-only skill — no mutations are executed. Safe to run at any time. The skill reads inventory and metafields only; it never adjusts component quantities or bundle availability. |
| 22 | |
| 23 | ## Workflow Steps |
| 24 | |
| 25 | 1. **OPERATION:** `products` — query |
| 26 | **Inputs:** `first: 250`, `query: "metafield:<namespace>.<key>:* OR product_type:bundle"`, select `requiresSellingPlan`, `status`, `metafield(namespace, key)`, `variants`, pagination cursor |
| 27 | **Expected output:** Bundle products and their parent variants; paginate until `hasNextPage: false` |
| 28 | |
| 29 | 2. For each bundle, parse component list. Native bundles use `productVariant.requiresComponents` and `productVariant.productVariantComponents`. Metafield bundles parse JSON value into `[{variantId, quantity}]`. |
| 30 | |
| 31 | 3. **OPERATION:** `productVariants` — query |
| 32 | **Inputs:** Batched IDs of all unique component variants, select `inventoryQuantity`, `inventoryItem { id }`, `product { title }` |
| 33 | **Expected output:** On-hand quantity per component |
| 34 | |
| 35 | 4. **OPERATION:** `inventoryItems` — query |
| 36 | **Inputs:** Batched component inventory item IDs, select `tracked`, `inventoryLevels(first: 25) { quantities }` |
| 37 | **Expected output:** Per-location quantity for each component |
| 38 | |
| 39 | 5. For each bundle, compute `max_buildable_units = floor(min over components of (component_on_hand - safety_stock) / required_qty)`. Flag bundles where `max_buildable_units == 0` (broken bundle) or `< min_listed_inventory_threshold`. |
| 40 | |
| 41 | ## GraphQL Operations |
| 42 | |
| 43 | ```graphql |
| 44 | # products:query — validated against api_version 2025-01 |
| 45 | query BundleProducts($query: String!, $after: String, $namespace: String!, $key: String!) { |
| 46 | products(first: 250, after: $after, query: $query) { |
| 47 | edges { |
| 48 | node { |
| 49 | id |
| 50 | title |
| 51 | status |
| 52 | productType |
| 53 | metafield(namespace: $namespace, key: $key) { |
| 54 | id |
| 55 | value |
| 56 | type |
| 57 | } |
| 58 | variants(first: 100) { |
| 59 | edges { |
| 60 | node { |
| 61 | id |
| 62 | title |
| 63 | sku |
| 64 | inventoryQuantity |
| 65 | requiresComponents |
| 66 | productVariantComponents(first: 50) { |
| 67 | edges { |
| 68 | node { |
| 69 | quantity |
| 70 | productVariant { |
| 71 | id |
| 72 | sku |
| 73 | inventoryQuantity |
| 74 | product { |
| 75 | id |
| 76 | title |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | pageInfo { |
| 88 | hasNextPage |
| 89 | endCursor |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | ```graphql |
| 96 | # productVariants:query — validated against api_version 2025-01 |
| 97 | query ComponentVariantStock($ids: [ID!]!) { |
| 98 | nodes(ids: $ids) { |
| 99 | ... on ProductVariant { |
| 100 | id |
| 101 | sku |
| 102 | inventoryQuantity |
| 103 | product { |
| 104 | id |
| 105 | title |
| 106 | } |
| 107 | inventoryItem { |
| 108 | id |
| 109 | tracked |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | ```graphql |
| 117 | # inventoryItems:query — validated against api_version 2025-01 |
| 118 | query ComponentInventoryLevels($ids: [ID!]!) { |
| 119 | nodes(ids: $ids) { |
| 120 | ... on InventoryItem { |
| 121 | id |
| 122 | tracked |
| 123 | inventoryLevels(first: 25) { |
| 124 | edges { |