Vip Customer Identifier

Identifies top-spending customers (top N% by lifetime value or order frequency) and exports a VIP candidate list; optionally tags qualified customers as VIPs.

shopify-admin-vip-customer-identifier


Purpose

Ranks customers by lifetime spend and order frequency, identifies the top N% (by value, frequency, or both), and outputs a CSV of VIP candidates. Optionally applies a VIP tag to qualified customers via customerUpdate. Used to build loyalty segments, prioritize white-glove support, or seed exclusive-access campaigns. The lifetime spend and order count are pulled directly from Shopify customer aggregates — no external CRM required.


Prerequisites

  • Authenticated Shopify CLI session: shopify store auth --store --scopes read_customers,read_orders,write_customers
  • API scopes: read_customers, read_orders, write_customers (only if tag_customers: true)

  • Parameters


    ParameterTypeRequiredDefaultDescription
    storestringyesStore domain (e.g., mystore.myshopify.com)
    formatstringnohumanOutput format: human or json
    dry_runboolnotruePreview VIP list without applying tags
    rank_bystringnospendRanking strategy: spend (lifetime value), frequency (order count), or both (composite score)
    top_pctfloatno5Top percentile to qualify as VIP (e.g., 5 = top 5%)
    min_ordersintegerno2Minimum lifetime orders to be eligible
    min_spendfloatno0Minimum lifetime spend (shop currency) to be eligible
    tag_customersboolnofalseIf true, apply VIP tag to qualified customers via customerUpdate
    tagstringnovipTag string applied when tag_customers: true

    Safety


    > ⚠️ When tag_customers: true, Step 3 executes customerUpdate mutations that mutate customer tag lists. Tags persist until manually removed. Run with dry_run: true first to confirm the VIP list and qualifying thresholds. The default is dry_run: true — you must explicitly set dry_run: false and tag_customers: true to apply tags.


    Workflow Steps


  • OPERATION: customers — query
  • Inputs: first: 250, query: "orders_count:>=", select id, displayName, defaultEmailAddress { emailAddress }, numberOfOrders, amountSpent { amount currencyCode }, tags, pagination cursor

    Expected output: All customers meeting min_orders threshold; paginate until hasNextPage: false


  • OPERATION: orders — query (only when ranking by frequency, for recency annotation)
  • Inputs: For each top candidate: query: "customer_id:", first: 1, sortKey: CREATED_AT, reverse: true

    Expected output: Most recent order per candidate to annotate the export


  • Filter to amountSpent.amount >= min_spend. Score each customer: spend → spend; frequency → orders; both → 0.6 × normalized spend + 0.4 × normalized frequency. Take the top top_pct%.

  • OPERATION: customerUpdate — mutation (only if tag_customers: true and dry_run: false)
  • Inputs: input: { id: , tags: [...existing_tags, ] }

    Expected output: customer.id, customer.tags, userErrors


    GraphQL Operations


    # customers:query — validated against api_version 2025-01
    query VIPCandidateCustomers($first: Int!, $after: String, $query: String) {
      customers(first: $first, after: $after, query: $query) {
        edges {
          node {
            id
            displayName
            firstName
            lastName
            defaultEmailAddress {
              emailAddress
            }
            numberOfOrders
            amountSpent {
              amount
              currencyCode
            }
            tags
            createdAt
          }
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
    

    # orders:query — validated against api_version 2025-01
    query VIPLastOrder($query: String!) {
      orders(first: 1, query: $query, sortKey: CREATED_AT, reverse: true) {
        edges {
          node {
            id
            name
            createdAt
            totalPriceSet {
              shopMoney { amount currencyCode }
            }
          }
        }
      }
    }
    

    # customerUpdate:mutation — validated against api_version 2025-01
    mutation CustomerUpdateVipTag($input: CustomerInput!) {
      customerUpdate(input: $input) {
        customer {
          id
          displayName
          tags
        }
        userErrors {
          field
          message
        }
      }
    }
    

    Session Tracking


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


    On start, emit:

    ╔══════════════════════════════════════════════╗
    ║  SKILL: VIP Customer Identifier              ║
    ║  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>
    

    If dry_run: true, prefix every mutation step with [DRY RUN] and do not execute it.


    On completion, emit:


    For format: human (default):

    ══════════════════════════════════════════════
    VIP CUSTOMER REPORT
      Customers scanned:    <n>
      Eligible (≥ min):     <n>
      VIPs (top <pct>%):    <n>
      Threshold spend:      $<amount>
      Threshold orders:     <n>
      Customers tagged:     <n>  (or "skipped — dry_run")
    
      Top 10 VIPs by <rank_by>:
        <name>  Spend: $<amount>  Orders: <n>  Last: <date>
      Output: vip_customers_<date>.csv
    ══════════════════════════════════════════════
    

    For format: json, emit:

    {
      "skill": "vip-customer-identifier",
      "store": "<domain>",
      "dry_run": true,
      "rank_by": "spend",
      "outcome": {
        "customers_scanned": 0,
        "eligible": 0,
        "vips_identified": 0,
        "threshold_spend": 0,
        "customers_tagged": 0,
        "errors": 0,
        "output_file": "vip_customers_<date>.csv"
      }
    }
    

    Output Format

    CSV file vip_customers_.csv with columns:

    customer_id, name, email, lifetime_spend, currency, orders_count, last_order_date, composite_score, rank, tag_applied


    Error Handling

    ErrorCauseRecovery
    THROTTLEDAPI rate limit exceededWait 2 seconds, retry up to 3 times
    userErrors on customerUpdateCustomer not found or tag conflictLog, skip, continue
    Fewer eligible than top_pct%Small customer baseLower min_orders/min_spend
    Multi-currency storescurrencyCode variesConvert via shop default before ranking

    Best Practices

  • Use rank_by: both to balance whales with loyalists — pure spend ranking can over-index on one-time large purchases.
  • Re-run quarterly with a date-stamped tag (e.g., vip-2026-Q2) so lapsed VIPs roll off rather than accumulating permanently.
  • Pair with customer-win-back — VIPs who become inactive should be flagged for high-priority re-engagement.
  • Run with dry_run: true first; review the threshold spend value to confirm the cutoff matches your VIP definition.