In September, we killed six planned pages with a single API call.
The plan was a content cluster on multi-property CMS terms, aimed at teams that run many sites from one back end. Six pages, roughly three weeks of writing, already outlined. Before any of it was assigned, the agent that proposed the cluster sent the head terms to the Search Volume endpoint of Keywords Data API in one batched request for the United States, English.
curl -X POST "https://api.dataforseo.com/v3/keywords_data/google_ads/search_volume/live" \
-H "Authorization: Basic $DATAFORSEO_AUTH" \
-H "Content-Type: application/json" \
-d '[
{
"keywords": [
"cms for multiple websites",
"multi site cms",
"multi tenant cms",
"cms for multiple brands",
"headless cms for agencies",
"cms for multiple sites one dashboard"
],
"location_code": 2840,
"language_code": "en"
}
]'
What came back:
| Keywords | search_volume | competition_index | Notes |
|---|---|---|---|
| cms for multiple websites | 10 | 0 | |
| multi site cms | 50 | 30 | no CPC returned; top-of-page bids $10.90 to $23.44 |
| multi tenant cms | 20 | 51 | CPC $10.33 |
| cms for multiple brands | null | null | |
| headless cms for agencies | null | null | |
| cms for multiple sites one dashboard | null | null |
Three of the six terms returned null. The best of the rest was 50 searches a month. The cluster died before anyone opened a document, and the whole exercise cost one request.
Validation comes before commission.
Most teams check search volume after a piece is planned. The topic gets chosen in a meeting, someone pulls volume to confirm it, and the check becomes paperwork, because by then the decision has an owner and a defender. We inverted the order. Nothing gets commissioned until the volume call clears, and the agent that proposes the article is the same one that makes the call. There is no handoff, so there is nobody to talk out of a bad plan. The cluster above was the agent’s own idea. It ran the numbers and dropped it.
The cost asymmetry is what makes this work. A batched call is cents and seconds. Three weeks of writing is neither.
What the threshold actually is.
The gate is four fields read together: search_volume, competition_index, cpc, and monthly_searches. A term clears on the combination, and volume by itself has never been enough.
Three terms that cleared this year, same endpoint and same location and language settings: “headless cms” at 4,400 a month, competition index 27, CPC $34. “cms api” at 260 a month, competition index 9, CPC $42.33. “astro cms” at 480 a month, competition index 42, CPC $11.59, with a rising trend.
Put “cms api” at 260 a month next to “multi site cms” at 50 a month, and the rule becomes legible. 260 searches is not much traffic. A $42.33 CPC says every one of those searches is worth real money to somebody actively bidding on it, which is a stronger intent signal than raw volume. Competition index 9 says the paid market has not crowded it yet. “multi site cms” had a fifth of the volume, three times the competition index, and no CPC returned at all.
The bands we work with: under roughly 100 searches a month, a term cannot be the sole target of its own page, whatever else it has going for it. Between roughly 100 and 500, it clears only if CPC signals commercial intent and the trend is flat or rising. Above roughly 1,000, a workable competition index is usually enough on its own. Those boundaries are conventions, and the point of writing them down in advance is that they cannot be renegotiated later for a topic somebody likes.
The bar also moves by page type. A blog post is a day of work and can chase a low-volume, high-CPC term; if it underperforms, we lose a day. A comparison page or a framework landing page is a permanent infrastructure we maintain for years, so trend direction outranks headline volume there. A landing page built on a declining term is a maintenance obligation attached to a shrinking audience.
Reading decay out of monthly_searches.
This is the part most volume checks skip. search_volume is a twelve-month average. monthly_searches is the array from which the average was computed, with one entry per month, each containing year, month, and search_volume. The average hides the shape.
// decayProfile(result) -> { keyword, trend, early, late, months }
// result: one item from tasks[0].result of the search_volume/live response
function decayProfile(result) {
// Months with no data are dropped, not counted as zero or as stable.
const months = [...(result.monthly_searches || [])]
.filter((m) => typeof m.search_volume === 'number')
.sort((a, b) => a.year - b.year || a.month - b.month);
if (months.length < 8) {
return { keyword: result.keyword, trend: 'insufficient_data', months: months.length };
}
// Compare the oldest third with the newest third, so one month of wobble
// cannot flip the verdict the way a step count can.
const n = Math.max(3, Math.floor(months.length / 3));
const mean = (a) => a.reduce((s, m) => s + m.search_volume, 0) / a.length;
const early = mean(months.slice(0, n));
const late = mean(months.slice(-n));
return {
keyword: result.keyword,
trend: late <= early * 0.6 ? 'declining' : 'stable_or_rising',
early,
late,
months: months.length,
};
}
// multi site cms: search_volume 50, early 70, late 35 -> declining
// astro cms: search_volume 480, array climbing -> stable_or_rising
“multi site cms” is the clearest case we have. Headline volume is 50 a month. That is low, and plenty of teams would still build against it, because 50 a month on a term that describes your product exactly feels like a floor worth owning. The array says otherwise. It opens at 170 a month in September 2025 and ends at 10 a month in August 2026. The 50 is a trailing average propped up by months that are already gone. What the array shows is the tail end of a term that had mostly finished disappearing.
Two practical notes. Google Ads returns volume in coarse buckets, so a single-month wobble means nothing; what matters is a staircase that moves in one direction across eight or ten of the twelve entries. And check direction on the terms that pass, not only on the marginal ones. “astro cms” at 480 a month cleared partly because its array is climbing. A rising 480 and a falling 480 are the same headline number and completely different assets.
The one time we overrode the data.
Once, and I only have the one. Agent-related phrasings return null volume across the board for us: “agentic cms”, “cms for ai agents”, “claude code cms”. All null. Customers ask us about the subject directly, so ignoring it because Google Ads has no data on the phrasing yet would have been its own kind of error.
What we did was narrower than overriding the numbers. We stopped treating the subject as a keyword target and moved the material into the body of pages that target terms with real volume. No page was built to rank for “agentic cms”. The data killed the page. The topic survived somewhere else. That distinction is what keeps the rule enforceable, because once “this matters to us” starts producing standalone pages, the threshold stops meaning anything inside a quarter.
Notes on the call itself
We batch 10 to 15 terms per request rather than checking them one at a time. A single term in isolation has no reference frame; you cannot tell whether 260 a month is good or bad in a category until the sibling terms are sitting next to it in the same response. The endpoint bills per request rather than per keyword, so the batch is effectively free compared with the single lookup.
Watch for rows that come back byte-identical across different keywords. That means Google Ads is returning a combined value for a group of similar terms rather than separate values, which is documented behavior. We hit it on an image CDN cluster where several terms returned identical rows. Counting grouped terms as independent inflates the apparent opportunity, always in the direction you were hoping for.
Null volume is a result, and we read it as one. Three nulls in a six-term cluster told us more than any of the numbers did.
Cosmic is an AI-powered headless CMS, and the agent described here runs inside our own content workflow: it proposes the piece, makes the volume call, and writes only what clears.