Inventory Aging Report

Read-only: categorizes inventory into aging buckets (0-30, 31-60, 61-90, 90+ days) based on time since last sale or receipt.

shopify-admin-inventory-aging-report


Purpose

Categorizes all inventory into aging buckets based on how long items have been sitting without selling. Calculates carrying cost exposure by bucket to prioritize markdown or liquidation decisions. Goes deeper than dead-stock identification by providing aging granularity. Read-only — no mutations.


Prerequisites

  • Authenticated Shopify CLI session: shopify store auth --store --scopes read_orders,read_products,read_inventory
  • API scopes: read_orders, read_products, read_inventory

  • Parameters


    ParameterTypeRequiredDefaultDescription
    storestringyesStore domain
    bucketsstringno0-30,31-60,61-90,91-180,181+Comma-separated aging buckets in days
    carrying_cost_pctfloatno25Annual carrying cost as % of inventory value (industry avg 20-30%)
    vendor_filterstringnoScope to specific vendor
    formatstringnohumanOutput format: human or json

    Safety


    > ℹ️ Read-only skill — no mutations are executed. Safe to run at any time.


    Workflow Steps


  • OPERATION: productVariants — query
  • Inputs: first: 250, select id, sku, inventoryQuantity, inventoryItem { id, unitCost }, product { title, vendor, status }, pagination cursor

    Expected output: All variants with stock and cost data


  • Filter to variants with inventoryQuantity > 0

  • OPERATION: orders — query
  • Inputs: query: "created_at:>=''", first: 250, select createdAt, lineItems { variant { id }, quantity }, pagination cursor

    Expected output: Sales history to determine last-sold date per variant


  • For each stocked variant, determine aging:
  • Find most recent order containing this variant → last_sold_date
  • If never sold, use product creation date as proxy
  • Age = today - last_sold_date
  • Assign to aging bucket

  • OPERATION: inventoryItems — query
  • Inputs: Inventory item IDs for cost data

    Expected output: Unit costs for value calculation


  • Calculate per bucket:
  • Total units
  • Total value (units × unitCost)
  • Monthly carrying cost = (value × carrying_cost_pct / 100) / 12
  • % of total inventory value

  • GraphQL Operations


    # productVariants:query — validated against api_version 2025-01
    query VariantsWithStock($query: String, $after: String) {
      productVariants(first: 250, after: $after, query: $query) {
        edges {
          node {
            id
            sku
            inventoryQuantity
            product { id title vendor status createdAt }
            inventoryItem {
              id
              unitCost { amount currencyCode }
            }
          }
        }
        pageInfo { hasNextPage endCursor }
      }
    }
    

    # orders:query — validated against api_version 2025-01
    query RecentSales($query: String!, $after: String) {
      orders(first: 250, after: $after, query: $query) {
        edges {
          node {
            createdAt
            lineItems(first: 50) {
              edges {
                node {
                  quantity
                  variant { id }
                }
              }
            }
          }
        }
        pageInfo { hasNextPage endCursor }
      }
    }
    

    # inventoryItems:query — validated against api_version 2025-01
    query InventoryItemCosts($ids: [ID!]!) {
      nodes(ids: $ids) {
        ... on InventoryItem {
          id
          unitCost { amount currencyCode }
        }
      }
    }
    

    Session Tracking


    Claude MUST emit the following output at each stage. This is mandatory.


    On start, emit:

    ╔══════════════════════════════════════════════╗
    ║  SKILL: Inventory Aging Report               ║
    ║  Store: <store domain>                       ║
    ║  Started: <YYYY-MM-DD HH:MM UTC>             ║
    ╚══════════════════════════════════════════════╝
    

    After each step, emit:

    [N/TOTAL] <QUERY|MUTATION>  <OperationName>
              → Params: <brief summary of key inputs>
              → Result: <count or outcome>
    

    On completion, emit:


    For format: human (default):

    ══════════════════════════════════════════════
    INVENTORY AGING REPORT
      Total SKUs with stock:  <n>
      Total inventory value:  $<amount>
      ─────────────────────────────
      AGING BUCKETS:
      0-30 days:    <n> SKUs  $<value> (<pct>%)  ✅ Fresh
      31-60 days:   <n> SKUs  $<value> (<pct>%)  ⚠️ Watch
      61-90 days:   <n> SKUs  $<value> (<pct>%)  ⚠️ Aging
      91-180 days:  <n> SKUs  $<value> (<pct>%)  🔴 Stale
      181+ days:    <n> SKUs  $<value> (<pct>%)  🔴 Dead
    
      Monthly carrying cost: $<amount>
      Annual carrying cost:  $<amount>
    
      Top aging items by value:
        "<product>" SKU:<sku>  Age:<n>d  Qty:<n>  Value:$<n>
    
      Output: inventory_aging_<date>.csv
    ══════════════════════════════════════════════
    

    Output Format

    CSV file inventory_aging_.csv with columns:

    variant_id, sku, product_title, vendor, quantity, unit_cost, total_value, last_sold_date, age_days, aging_bucket, monthly_carrying_cost


    Error Handling

    ErrorCauseRecovery
    THROTTLEDAPI rate limit exceededWait 2 seconds, retry up to 3 times
    Missing unitCostNo COGS dataUse $0 for value — flag as "cost unknown"
    No sales historyNew product or never soldUse product creation date as aging start

    Best Practices

  • Use carrying_cost_pct: 25 as default (includes storage, insurance, opportunity cost, shrinkage).
  • Items in 90+ day buckets are strong candidates for markdowns — use bulk-price-adjustment.
  • Cross-reference with dead-stock-identifier and stock-velocity-report for a complete inventory health picture.
  • Run monthly to track aging trends and measure liquidation effectiveness.