Produce a defensible, prospect-specific inventory-leak diagnostic from public data, then send it as the cold email itself.
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.
brand.com — no https://, no trailing slash), and the best contact email from the Contact Research Workflow. Everything else flows from these three fields.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."
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.
/**
* 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";
}
TEST_URL value and run the function. A VA can do that in 90 seconds.
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).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?"
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.
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.
facebook.com/ads/library. Country: All. Ad category: All. Search the brand name. Click their page.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.
One word on the cover of the report: Light, Moderate, or Severe. This is judgement, not maths. The rules below are guidelines, not gospel.
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)
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.
We pulled your public product catalog and cross-referenced it against your active Meta ad presence. Specifically:
—
A few of the products currently in your active catalog with stock issues:
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.
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
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.
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.
[Store name] — inventory leak in your active ad catalogNoticed something in [store.com]'s product feed[Store name] — 11 products in your ad rotation appear OOSReply 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.