--- name: shopify-admin-product-affinity-cross-sell role: order-intelligence description: "Mine order history to find which products are most frequently bought together, then rank pairs by support, confidence, and lift to power bundles and cross-sell recommendations." toolkit: shopify-admin, shopify-admin-execution api_version: "2025-01" graphql_operations: - orders:query status: stable compatibility: Claude Code, Cursor, Codex, Gemini CLI --- ## Purpose Applies market basket analysis to your order history to surface product pairs that customers naturally buy together. For every co-purchased pair it calculates **support** (how often the pair appears), **confidence** (given product A, how likely is B?), and **lift** (how much more likely than chance). The output is actionable input for product bundles, "frequently bought together" widgets, cross-sell email flows, and homepage recommendations. Read-only — no mutations are executed. ## Prerequisites - Authenticated Shopify CLI session: `shopify auth login --store ` - API scopes: `read_orders`, `read_products` (validator-confirmed: line item `product` field traverses the product graph) ## Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | store | string | yes | — | Store domain (e.g., mystore.myshopify.com) | | format | string | no | human | Output format: `human` or `json` | | dry_run | bool | no | false | Preview operations without executing mutations | | date_range_start | string | yes | — | Start date in ISO 8601 (e.g., `2025-01-01`) | | date_range_end | string | yes | — | End date in ISO 8601 (e.g., `2025-03-31`) | | min_support | integer | no | 5 | Minimum number of orders a pair must co-appear in to be included | | min_confidence | float | no | 0.1 | Minimum P(B\|A) threshold (0–1) | | min_lift | float | no | 1.0 | Only include pairs where lift > this value (> 1 means non-random) | | top_n | integer | no | 20 | Number of top pairs to show in the ranked output | | sort_by | string | no | lift | Ranking metric: `lift`, `confidence`, or `support` | | exclude_tags | string | no | — | Comma-separated product tags to exclude (e.g., `gift-wrap,donation`) | ## Workflow Steps 1. **OPERATION:** `orders` — query **Inputs:** `first: 250`, `query: "created_at:>='' created_at:<=''"`, pagination cursor; select `lineItems` with `product { id, title }` and `quantity`; skip orders with a single line item **Expected output:** All multi-item orders in range; paginate until `hasNextPage: false`; build a product frequency map (`product_id → order_count`) and a pair frequency map (`(product_a_id, product_b_id) → co_occurrence_count`) 2. **In-memory analysis:** - For each order with ≥ 2 distinct products, enumerate every unique unordered pair and increment the pair counter - Compute metrics for each pair that meets `min_support`: - **Support** = `pair_count / total_orders` - **Confidence A→B** = `pair_count / count(orders containing A)` - **Confidence B→A** = `pair_count / count(orders containing B)` - **Lift** = `support / (P(A) × P(B))` - Filter by `min_confidence` and `min_lift`; sort by `sort_by`; truncate to `top_n` ## GraphQL Operations ```graphql # orders:query (multi-item basket analysis) — validated against api_version 2025-01 query OrdersForAffinityAnalysis($first: Int!, $after: String, $query: String) { orders(first: $first, after: $after, query: $query) { edges { node { id createdAt lineItems(first: 50) { edges { node { quantity product { id title tags } } } } } } pageInfo { hasNextPage endCursor } } } ``` ## Session Tracking **Claude MUST emit the following output at each stage. This is mandatory.** **On start**, emit: ``` ╔══════════════════════════════════════════════╗ ║ SKILL: product-affinity-cross-sell ║ ║ Store: ║ ║ Started: ║ ╚══════════════════════════════════════════════╝ ``` **After each step**, emit: ``` [N/TOTAL] → Params: → Result: ``` **On completion**, emit: For `format: human` (default): ``` ══════════════════════════════════════════════ OUTCOME SUMMARY Orders analysed: Unique products: Pairs evaluated: Pairs above threshold: Date range: to Sort by: Errors: 0 Output: product_affinity_.csv ══════════════════════════════════════════════ ``` Followed by an inline ranked table of the top `top_n` pairs: | Rank | Product A | Product B | Support | Conf A→B | Conf B→A | Lift | |------|-----------|-----------|---------|----------|----------|------| | 1 | ... | ... | ... | ...% | ...% | ... | For `format: json`, emit: ```json { "skill": "product-affinity-cross-sell", "store": "", "started_at": "", "completed_at": "", "dry_run": false, "steps": [ { "step": 1, "operation": "OrdersForAffinityAnalysis", "type": "query", "params_summary": " to ", "result_summary": " orders, multi-item baskets", "skipped": false } ], "outcome": { "orders_analysed": 0, "unique_products": 0, "pairs_evaluated": 0, "pairs_above_threshold": 0, "date_range_start": "", "date_range_end": "", "sort_by": "lift", "results": [], "errors": 0, "output_file": "product_affinity_.csv" } } ``` ## Output Format CSV file `product_affinity_.csv` with one row per qualifying pair: | Column | Description | |--------|-------------| | `rank` | Position in sorted output | | `product_a_id` | Shopify product GID for the first item | | `product_a_title` | Product A name | | `product_b_id` | Shopify product GID for the second item | | `product_b_title` | Product B name | | `co_occurrence_count` | Number of orders containing both products | | `support` | `co_occurrence_count / total_orders` | | `confidence_a_to_b` | P(B\|A) — likelihood of B given A is in cart | | `confidence_b_to_a` | P(A\|B) — likelihood of A given B is in cart | | `lift` | How much more likely than random co-occurrence | | `recommendation_type` | `bundle_candidate` if lift > 2, else `cross_sell` | ## Error Handling | Error | Cause | Recovery | |-------|-------|----------| | `THROTTLED` | API rate limit from paginating large order history | Wait 2 s, retry up to 3 times; narrow date range if persistent | | `product` is null on line item | Product was deleted after purchase | Skip that item from pair analysis; count the order in the total | | Zero pairs above threshold | Store has few multi-item orders or thresholds too strict | Lower `min_support` to 2 and `min_confidence` to 0.05, or widen date range | | Combinatorial explosion | Stores with very large line item counts per order | The pair-enumeration loop skips orders with > 20 distinct products to cap O(n²) growth | ## Best Practices 1. Use at least 90 days of order history — short windows produce noisy lift scores because the probability denominators are small. 2. **Lift > 2** is a strong bundle signal: customers are buying these products together at least twice as often as chance would predict. 3. **Confidence A→B > 30%** makes for a reliable "frequently bought with" widget: three in ten shoppers who buy A also buy B. 4. Filter out accessories and add-ons (like gift wrap or donation SKUs) with `exclude_tags` before ranking — they inflate support scores without being meaningful cross-sell pairs. 5. Use the `recommendation_type` column to split your output: `bundle_candidate` pairs are best for pre-built bundles or volume discounts; `cross_sell` pairs are better suited to cart upsells and post-purchase email recommendations. 6. Re-run quarterly — seasonal products enter and exit the top pairs list, and ignoring that produces stale recommendations.