SOP // Cold Outreach v1

Leak Report Cockpit

Produce a defensible, prospect-specific inventory-leak diagnostic from public data, then send it as the cold email itself.

PHASE 01

Prospect Intake

Pending

Lock in who you're working on before you do any analysis. Most contamination of results comes from running half the workflow on Brand A and half on Brand B because you got distracted between phases.

Fill in the prospect bar at the top of this page
Store name, URL (just brand.com — no https://, no trailing slash), and the best contact email from the Contact Research Workflow. Everything else flows from these three fields.
Confirm fit against the Scoring Sheet
Open your Prospect Scoring Sheet. Confirm this prospect is in the $50k–$300k revenue band, the niche is in scope (apparel non-sports, shoes, pets, outdoor, survival, etc.), and there's evidence they're running ads. If not — stop, don't waste a report on the wrong prospect.
Quick eyeball of the homepage
Open the store. 60 seconds, no more. Looking for: are they actually a Shopify store (footer usually gives it away), is the catalog the right shape (high-variant, multi-SKU), are there visible signs of OOS issues already (greyed-out variants on category pages, "sold out" labels everywhere)? If the store looks dead or abandoned, skip.
Reference — what makes a prospect worth a report

The whole point of this workflow is that you're producing real diagnostic work before the prospect knows you exist. That investment is wasted on a bad prospect. The filter at intake should be ruthless:

Worth it: Independent Shopify store, 50–500+ SKUs with variants, active Meta or Google ads (check Meta Ad Library entry exists), real operator behind it (LinkedIn-findable), revenue band fits.

Skip: Dropshipping flag (AliExpress-style products, generic descriptions), no variants (single-SKU products like art prints), no LinkedIn-findable owner, store last updated months ago, or any whiff of "we ran out of money."

PHASE 02

Automated Catalog Analysis

Pending

Run the Apps Script function against the store's URL. It hits the public /products.json endpoint, paginates through the whole catalog, and identifies products with stock issues. This is the boring counting work that should never be done by hand.

First time only — install the Apps Script function
Open your existing Apps Script project (the one running the Scoring Sheet importer). Add the function below. Save. Done. You only do this once.
/**
 * Leak Report — automated catalog analysis from public /products.json
 * Returns structured analysis of OOS and partially-OOS products.
 *
 * Usage from Apps Script editor:
 *   1. Set TEST_URL below to the prospect's store URL
 *   2. Run analyzeProspect_run
 *   3. Check the Logs (View > Logs) for the results
 *
 * Usage from a sheet cell:
 *   =analyzeProspectInline("brand.com")
 */

function analyzeProspect_run() {
  var TEST_URL = "examplestore.com"; // <-- change me

  var result = analyzeProspect(TEST_URL);
  Logger.log(formatReport(result));
}

function analyzeProspect(storeUrl) {
  var baseUrl = storeUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
  baseUrl = "https://" + baseUrl;

  var allProducts = [];
  var page = 1;
  var maxPages = 20; // safety: caps at 5000 products

  while (page <= maxPages) {
    var url = baseUrl + "/products.json?limit=250&page=" + page;
    try {
      var response = UrlFetchApp.fetch(url, {
        muteHttpExceptions: true,
        followRedirects: true
      });

      if (response.getResponseCode() !== 200) {
        if (page === 1) {
          return { error: "Could not fetch /products.json — status " + response.getResponseCode() + ". Store may have it disabled or require auth.", url: baseUrl };
        }
        break;
      }

      var data = JSON.parse(response.getContentText());
      if (!data.products || data.products.length === 0) break;

      allProducts = allProducts.concat(data.products);
      if (data.products.length < 250) break;

      page++;
      Utilities.sleep(300); // polite rate limiting
    } catch (e) {
      return { error: "Fetch failed: " + e.message, url: baseUrl };
    }
  }

  // Analyze
  var fullyOOS = [];
  var partiallyOOS = [];
  var dominantVariantGone = []; // proxy for "you can technically buy it but the main sizes are gone"
  var noVariantData = [];

  allProducts.forEach(function(product) {
    if (!product.variants || product.variants.length === 0) {
      noVariantData.push(product.title);
      return;
    }

    var totalVariants = product.variants.length;
    var availableVariants = product.variants.filter(function(v) { return v.available; }).length;
    var unavailableVariants = totalVariants - availableVariants;

    var productUrl = baseUrl + "/products/" + product.handle;

    if (availableVariants === 0) {
      fullyOOS.push({
        title: product.title,
        url: productUrl,
        variants: totalVariants
      });
    } else if (unavailableVariants / totalVariants >= 0.6) {
      // 60%+ of variants gone — "only the awkward sizes left"
      partiallyOOS.push({
        title: product.title,
        url: productUrl,
        available: availableVariants,
        total: totalVariants
      });
    } else if (unavailableVariants > 0 && totalVariants >= 4) {
      dominantVariantGone.push({
        title: product.title,
        url: productUrl,
        available: availableVariants,
        total: totalVariants
      });
    }
  });

  return {
    storeUrl: baseUrl,
    totalProducts: allProducts.length,
    fullyOOS: fullyOOS,
    partiallyOOS: partiallyOOS,
    dominantVariantGone: dominantVariantGone,
    pagesScanned: page,
    timestamp: new Date().toISOString()
  };
}

function formatReport(r) {
  if (r.error) {
    return "ERROR: " + r.error + "\nStore: " + r.url;
  }

  var out = [];
  out.push("=== LEAK REPORT ANALYSIS ===");
  out.push("Store: " + r.storeUrl);
  out.push("Total products in active catalog: " + r.totalProducts);
  out.push("");
  out.push("FULLY OUT OF STOCK (still in active catalog):");
  out.push("  Count: " + r.fullyOOS.length);
  r.fullyOOS.slice(0, 20).forEach(function(p) {
    out.push("  - " + p.title + " (" + p.variants + " variants, all OOS)");
    out.push("    " + p.url);
  });
  if (r.fullyOOS.length > 20) out.push("  ... and " + (r.fullyOOS.length - 20) + " more");

  out.push("");
  out.push("PARTIALLY OOS — 60%+ of variants gone:");
  out.push("  Count: " + r.partiallyOOS.length);
  r.partiallyOOS.slice(0, 10).forEach(function(p) {
    out.push("  - " + p.title + " (" + p.available + "/" + p.total + " variants available)");
    out.push("    " + p.url);
  });

  out.push("");
  out.push("SOME VARIANTS MISSING (lighter signal):");
  out.push("  Count: " + r.dominantVariantGone.length);

  return out.join("\n");
}

// Inline cell version — paste a URL, get a summary number
function analyzeProspectInline(storeUrl) {
  var r = analyzeProspect(storeUrl);
  if (r.error) return "ERROR: " + r.error;
  return r.fullyOOS.length + " fully OOS | " + r.partiallyOOS.length + " mostly OOS | " + r.totalProducts + " total";
}
One-time install only You do this once. After that, every prospect just needs you to change the TEST_URL value and run the function. A VA can do that in 90 seconds.
Run the analysis for this prospect
Open Apps Script. Change TEST_URL to this prospect's store URL. Click Run. Wait 10–30 seconds (depends on catalog size). Open Logs (View → Logs, or Ctrl+Enter).
Paste the headline numbers below
From the Logs output, capture the three counts. These drive the rest of the report.
Pick the 5 most pointed examples for the report
From the fully-OOS list, pick 5 products that will land hardest. Bias toward: hero-looking products (the kind likely to be in ad rotation), seasonal items, or anything that's clearly been gone a while. Paste them below — one per line, with the product URL.
Reference — what /products.json shows and doesn't show

What it shows: Every active, published product in the Shopify storefront, with variant-level available booleans. This is the same data layer their ad catalog feeds from — if a product is here, it's a candidate for being in Advantage+ rotation unless explicitly excluded.

What it doesn't show: Draft products (those exist in admin but aren't published), archived products, B2B-restricted products, and on some stores it's been deliberately disabled (you'll get a 404 or password page — handle gracefully, skip the prospect or fall back to manual).

The inferential leap: "Active in catalog + 100% of variants unavailable" doesn't strictly prove the product is being advertised right now. But on any store running Advantage+ Shopping with default catalog settings, it's the default behaviour for Meta to include it. A sharp prospect might push back — your honest reply is "I can't see your campaign-level catalog filters from outside, but every store I've seen this on had these products in active rotation. Want me to look properly with read-access?"

Reference — edge cases the script handles (and doesn't)

Handles: Pagination up to 5000 products. Polite rate limiting (300ms between pages). HTTP errors. Stores without /products.json access (returns an error you can read).

Doesn't handle: Stores using a non-Shopify platform (you'll get either an error or junk data — confirm Shopify before running). Stores with custom inventory logic where available is overridden by an app (rare). B2B stores requiring login.

Sanity check: If the script returns 0 OOS products across a 200+ product catalog, something's wrong. Either it's a brand-new store, a B2B store, or they've cleaned house recently. Sanity-check by visiting 2-3 random product pages and confirming what you see matches the data.

PHASE 03

Meta Ad Library Cross-Reference

Pending

This is the part the automation can't do, and it's the part that gives the report its sharpest edge. You're confirming they're actively running Meta ads, what kind, and whether any of the OOS products you flagged appear in visible creatives. 5–10 minutes per prospect.

Open the Meta Ad Library entry
Go to facebook.com/ads/library. Country: All. Ad category: All. Search the brand name. Click their page.
Record what's running
Capture the basics. If they have no active ads, the report needs a different angle (a "you've stopped running ads, here's why that might be" angle — out of scope for this v1 workflow, skip the prospect for now).
DPA fingerprint check — is the catalog machinery actually running?
EAES fixes the specific leak where a store auto-feeds its catalog into ads, so OOS products get advertised on autopilot. These three tells confirm catalog/Advantage+ ads are live (not just hand-built static creatives). The more that read YES, the stronger the fit. Click each ad's See ad details to confirm where unsure.
Match your flagged OOS products against the live ads
A direct hit — a product you flagged as fully OOS appearing by name in an active ad — is the killer detail that makes a report undeniable. To find them fast: on the Ad Library page, select all (Ctrl/Cmd+A), copy, and paste below. The matcher checks your Phase 2 OOS list against the ad text. A match is gold; a non-match proves nothing — Advantage+ rotates the catalog dynamically, so a product not shown right now may still be in rotation. Confirmed matches drop into your notes automatically.
Notes — direct matches and anything else worth citing:
Reference — reading the Ad Library like an operator

The Ad Library shows the creative side of what's running, not the campaign structure. You can't see budget, audience, or catalog filters from outside. What you can read:

Catalog ads: Multi-image carousels showing product shots with prices overlaid are almost always Advantage+ Shopping Campaigns or catalog-driven DPA. If you see these, you're confirmed in your territory.

Single creative ads: Lifestyle shots, video, or single images. These are usually traffic or engagement campaigns, not catalog. Their presence doesn't confirm Advantage+ but also doesn't deny it — it just means you saw the brand-level creative side.

No ads at all: Either they've paused, or their page isn't connected to ads (rare). Either way, no active Meta ad spend = no leak to report on. Skip.

The sporting analogy: you're not seeing the playbook, you're seeing the highlight reel. Highlights tell you which competition they're playing in, which is enough for our purposes.

PHASE 04

Severity Scoring

Pending

One word on the cover of the report: Light, Moderate, or Severe. This is judgement, not maths. The rules below are guidelines, not gospel.

Light
A few leaks
<5% of catalog fully OOS
<10% mostly OOS
Moderate
Real money
5–15% fully OOS
10–25% mostly OOS
Severe
Burning budget
>15% fully OOS
>25% mostly OOS
The honest qualifier You don't know their ad spend, you don't know their campaign structure, and you don't know what catalog filters they have set. Severity is a label on the symptom you can see, not a diagnosis of total cost. Don't oversell. The report should say what the symptom is and let the prospect connect the cost dots — that's a better conversation than you telling them.
Reference — adjusting severity for context

Bump up one severity grade if any of these are true:

— Catalog ads are clearly visible in their Ad Library (catalog-driven spend is most exposed to this leak)
— Multiple hero-looking products are fully OOS (the products most likely to be in active rotation)
— Visible creative shows products you flagged as OOS

Bump down if:

— Their Ad Library shows mainly single-creative engagement ads, no obvious catalog work
— OOS products are clearly long-tail (obscure variants, last-season colours)
— You can see they're already managing OOS visibly on their site (sold-out badges, hidden variants)

PHASE 05

Report Assembly

Pending

The report writes itself from the inputs above. Read through it. If anything feels overstated, dial it back — the discipline is conservative-but-specific. If anything's missing context, edit the prose freehand before sending.

Brand.com

Inventory leak diagnostic · Generated today

Headline finding

Fill in the prospect details above to generate the report.

What we looked at

We pulled your public product catalog and cross-referenced it against your active Meta ad presence. Specifically:

  • Your active Shopify product feed (the same source your ad catalogs read from)
  • Your Meta Ad Library entry for visible active campaigns

What we found

Examples

A few of the products currently in your active catalog with stock issues:

  • Examples will populate here as you fill in the form above.

Why this is happening

Meta's Advantage+ Shopping Campaigns are designed to optimise spend across your whole product catalog, dynamically. The system reads from your Shopify product feed and includes any product that isn't explicitly excluded. When a product goes out of stock on the storefront, it doesn't automatically get excluded from the ad catalog — the algorithm keeps spending against it until it sees enough negative signal (no add-to-carts, no purchases) to deprioritise it. That signal lag is the leak. Meanwhile every click on that ad is a paid visit to a dead product page — a wasted dollar and a frustrated first-time visitor who probably won't come back.

What can be done about it

This is fixable without a major platform change — the mechanism is catalog hygiene and inventory state management, not algorithm engineering. The fix is to stop feeding the algorithm bad inputs. Reply to this email if you'd like to walk through what that looks like in practical terms for your specific setup.

— Andy

Reference — what to edit before sending

Edit aggressively if: Any number feels wrong (you're better off undershooting on the cold pitch). Any sentence sounds like marketing — strip it. Any technical claim you can't defend on a call — soften or remove.

Don't change: The headline finding format ("X products currently in your active ad catalog appear to be fully out of stock"). The "Why this is happening" paragraph — it's the mechanism explanation that does the credibility work.

Voice check before sending: Read the report aloud. If a sentence sounds like a stranger trying to sell something, rewrite it as if you were emailing a peer about something you noticed. The whole point of this format is that it doesn't sound like cold outreach — because it isn't, really. It's an unsolicited diagnostic.

PHASE 06

Send and Log

Pending

The report goes out as the body of a cold email — not as an attachment. The whole format depends on the prospect seeing the diagnostic immediately, without opening anything.

Email subject line — keep it specific
Three tested forms. Pick one. None of them are clever — clever subject lines look like cold outreach.
  • [Store name] — inventory leak in your active ad catalog
  • Noticed something in [store.com]'s product feed
  • [Store name] — 11 products in your ad rotation appear OOS
Compose in GoHighLevel (or your sending tool)
From your business sending domain only. Paste the report text from Phase 5 as the body. No attachment. No PDF. The diagnostic IS the email — that's the whole point. Add your CAN-SPAM compliant signature with business name and postal address.
Log the send in your tracking sheet
Open your prospect tracking sheet (or whatever you're using to log sends). Record: prospect name, URL, contact email, severity rating, OOS count, date sent. This is what lets you (and a VA) see the funnel state at a glance.
Mark this prospect complete and move to the next one
Reset the cockpit (top right) and start Phase 1 on the next prospect. Resist the urge to refresh and re-check this one — you've already done the work, it's gone.
Reference — what happens after the send

Reply expected: Reply rate on this format should be meaningfully higher than a generic cold pitch — somewhere around 8–15% in the first batch is a realistic expectation. Anyone who replies gets a real human response from you (not a VA), inside 24 hours. That response is where you offer the deeper look.

Follow-up: One follow-up email, 4–7 days after the original. Subject: RE: [original subject]. Body: short — "Sarah, just wanted to make sure this didn't get buried. Happy to leave it, but if the leak's still bugging you, the door's open." That's it. No third email.

No reply: Move on. The diagnostic was real work, but the format means even a "no reply" prospect has now seen you do real diagnostic work on their business. You're now a name that exists in their head. That's not nothing.