$npx -y skills add 40RTY-ai/shopify-admin-skills --skill shopify-admin-customer-mergeMerges duplicate customer records: invokes Shopify's native customer merge API where supported, otherwise consolidates the loser record's tags and notes into the winner via customerUpdate.
| 1 | ## Purpose |
| 2 | Resolves duplicate customer records identified by `duplicate-customer-finder`. Where the Shopify Admin API exposes `customerMerge` (a native merge that moves orders, addresses, subscriptions, and metafields onto a winner record), this skill calls it directly. When `customerMerge` is unavailable or fails for the given account pair, the skill falls back to consolidating searchable metadata — tags, notes, marketing consent — onto the winner via `customerUpdate`, then writes a clear annotation to the loser record so staff can complete the merge manually in Shopify Admin. |
| 3 | |
| 4 | ## Prerequisites |
| 5 | - Authenticated Shopify CLI session: `shopify store auth --store <domain> --scopes read_customers,write_customers` |
| 6 | - API scopes: `read_customers`, `write_customers` |
| 7 | |
| 8 | ## Parameters |
| 9 | |
| 10 | | Parameter | Type | Required | Default | Description | |
| 11 | |-----------|------|----------|---------|-------------| |
| 12 | | store | string | yes | — | Store domain (e.g., mystore.myshopify.com) | |
| 13 | | format | string | no | human | Output format: `human` or `json` | |
| 14 | | dry_run | bool | no | true | Preview merge plan without executing mutations | |
| 15 | | customer_winner_id | string | yes | — | GID of the customer record to keep (e.g., `gid://shopify/Customer/12345`) | |
| 16 | | customer_loser_id | string | yes | — | GID of the customer record to merge into the winner | |
| 17 | | use_native_merge | bool | no | true | Try `customerMerge` first; if it fails or is unavailable, fall back to consolidation via `customerUpdate` | |
| 18 | | merge_tags | bool | no | true | Union the loser's tags onto the winner | |
| 19 | | merge_note | bool | no | true | Append the loser's note to the winner (with timestamp prefix) | |
| 20 | | annotate_loser | bool | no | true | Write a note on the loser record pointing to the winner GID for manual cleanup | |
| 21 | |
| 22 | ## Safety |
| 23 | |
| 24 | > ⚠️ Steps 2–4 execute mutations that modify customer records. `customerMerge` is irreversible — once orders and addresses are moved to the winner, the loser record is closed and cannot be split back. Run with `dry_run: true` first to confirm winner/loser GIDs and the merge plan. The default is `dry_run: true`. Always verify both records belong to the same human (matching email, phone, name) using `duplicate-customer-finder` output before committing. Do not merge a customer with active subscriptions or unfulfilled orders without confirming downstream systems will follow the new owner GID. |
| 25 | |
| 26 | ## Workflow Steps |
| 27 | |
| 28 | 1. **OPERATION:** `customer` — query (called twice: winner and loser) |
| 29 | **Inputs:** `id: <customer_id>`, select `id`, `displayName`, `firstName`, `lastName`, `defaultEmailAddress { emailAddress }`, `phone`, `tags`, `note`, `numberOfOrders`, `amountSpent`, `emailMarketingConsent { marketingState }`, `smsMarketingConsent { marketingState }`, `addresses(first: 25) { id }`, `createdAt` |
| 30 | **Expected output:** Both records' full identity payload — abort if either GID does not resolve |
| 31 | |
| 32 | 2. **OPERATION:** `customerMerge` — mutation (only if `use_native_merge: true` and not `dry_run`) |
| 33 | **Inputs:** `customerOneId: <customer_winner_id>`, `customerTwoId: <customer_loser_id>`, `overrideFields`: prefer winner's name/email/phone/locale/marketing-consent |
| 34 | **Expected output:** `job.id` (merge runs asynchronously), `userErrors`. If `userErrors` indicates merge is not supported for this pair (B2B, gift card holder, subscriber, etc.), proceed to step 3 fallback. |
| 35 | |
| 36 | 3. **OPERATION:** `customerUpdate` — mutation (winner) — fallback path or when `use_native_merge: false` |
| 37 | **Inputs:** `input.id: <customer_winner_id>`, `input.tags: <union of winner.tags and loser.tags>` (only if `merge_tags`), `input.note: <winner.note + "\n[YYYY-MM-DD] Merged from <loser_email>:\n" + loser.note>` (only if `merge_note`) |
| 38 | **Expected output:** `customer.id`, `customer.tags`, `customer.note`, `userErrors` |
| 39 | |
| 40 | 4. **OPERATION:** `customerUpdate` — mutation (loser) — only if `annotate_loser: true` |
| 41 | **Inputs:** `input.id: <customer_loser_id>`, `input.note: "<existing note>\n[YYYY-MM-DD] DUPLICATE — merge target: <customer_winner_id>. Manually close in Shopify Admin once orders are reviewed."`, `input.tags: <existing + ["duplicate", "merged-loser"]>` |
| 42 | **Expected output:** `customer.id`, `customer.tags`, `customer.note`, `userErrors` |
| 43 | |
| 44 | ## GraphQL Operations |
| 45 | |
| 46 | ```graphql |
| 47 | # customer:query — validated against api_version 2025-01 |
| 48 | query CustomerForMerge($id: ID!) { |
| 49 | customer(id: $id) { |
| 50 | id |
| 51 | displayName |
| 52 | firstName |
| 53 | lastName |
| 54 | defaultEmailAddress { emailAddress } |
| 55 | phone |
| 56 | tags |
| 57 | note |
| 58 | n |