- The rule, stated in
CLAUDE.md: search volume is NOT a gate. The three hard gates are a winnable SERP verified by live capture, an ownable unique data point, and a true premise. Volume only orders the queue. - This repository's pipeline distinguishes
volume_status: measured(Google Ads reports a real number),below_floor(real demand exists but is under the reporting threshold, not zero), andnot_submitted(never looked up). Collapsing the second into the third is a scoring bug this pipeline explicitly refuses to make. - Five incidents from this repository's own data, verified live: a 116-keyword category rejected on SERP composition while its named gate keywords came back
below_floor; a 192-keyword family dropped because a vendor already owns the exact-match reference pages, not chiefly because of volume; three pages with measured volume up to 590/month pruned as HARD SERPs; 50 published pages carryingvolume_status: below_floortoday; and 173 rows blocked reversibly, every row's reason stating "nothing about this row is defective," on measured real-world yield, a completely different number from search volume that still isn't allowed to gate anything either. - As of the 2026-09-03 queue snapshot: 160 published, 8,182 pruned, 711 blocked, 260 queued, 29 clustered: 9,342 candidate rows total. Only 209 of those rows have ever had a Google Ads number attached; 1,559 sit at
below_floor. The pipeline builds pages from both categories.
01 The obvious algorithm we don't run
The default way to run programmatic SEO is not complicated: pull a keyword list, sort by search volume, build pages for the top of the list, skip the bottom. It is the algorithm every pSEO course teaches and the one most tooling assumes. Our pipeline reads search volume for every candidate keyword, and then does not let that number decide whether a page gets written.
This repository is private, so what follows are receipts you cannot click: file paths, commit hashes, and queue rows quoted verbatim, that you're welcome to ask us to walk through live. Every specific number below was computed directly against docs/seo/data/queue.json while writing this, not copied from an earlier summary, precisely because that file changes every time the pipeline runs and a stale number would be its own small violation of the rule this article is about.
02 The rule, and the three gates that replace it
CLAUDE.md states it under a heading that names the date it was learned the hard way:
Search volume is NOT a gate. The three hard gates are: (1) a winnable SERP, verified against a live capture in connect_serp.json; (2) an ownable unique data point no sibling page also claims; (3) a true premise. Volume only orders the queue. Null-volume rows with EASY SERPs are legitimate builds; 260/mo rows on vendor-owned SERPs are not.
Read that last sentence twice, because it is doing the actual work: a keyword nobody can even measure can be a legitimate build, and a keyword with real, countable monthly searches can be a bad one. The two variables (is it true and winnable, and how many people search it) are orthogonal. A pipeline that gates on volume is optimizing for a number that has no relationship to whether the page it funds can rank or whether the page can be honest. This article is the queue data that shows the rule surviving contact with a 9,342-row corpus rather than staying a sentence in a style guide.
03 "Zero" is three different facts
The rule only holds if the pipeline can tell the difference between "we know demand is zero" and "we don't know." CLAUDE.md names three states, carried per row as volume_status:
| Status | What it means |
|---|---|
measured | Google Ads reports an actual number for this keyword. |
below_floor | The API returned null: real demand exists but sits under the reporting threshold. Not zero. |
not_submitted | Never looked up. DataForSEO batches roughly 1,000 keywords per call, so CLAUDE.md is blunt about this one: "there is no excuse for not_submitted on a queued row." |
The distinction matters because a naive pipeline treats a null volume as a kill signal: no measurable demand, don't build it. Collapsing below_floor into a scored zero would silently re-introduce a volume gate through the back door of the scoring function, even while the written rule says volume isn't one. Computed live against the current queue: 209 rows carry measured, 1,559 carry below_floor, and 40 carry not_submitted. The rest of this article is what those numbers were allowed, and not allowed, to do to the corpus.
04 A category rejected while volume said nothing
On 2026-07-26, the pipeline evaluated a full candidate category (a Shopify GraphQL cookbook plus a REST-to-GraphQL mapping cluster, 116 keywords) and rejected it. docs/seo/graphql-category-verdict.md records the run in full, and its own retraction table states the finding as plainly as it can be stated:
The category can ride the Feb-2025 REST deprecation forcing event. [Retracted.] Volume ordered nothing here. The category is rejected on SERP composition and on the schema-generated-docs structure, per the CLAUDE.md rule that volume is never a gate.
The volume evidence, read on its own, was genuinely ambiguous: both keywords the earlier research doc had named as the category's gate (shopify rest to graphql migration and shopify graphql cookbook) came back below_floor, and per the rule above, below_floor does not kill a category by itself. What killed it was the SERP: 32 of 45 live captures scored HARD, and the reason generalizes past this one category: Shopify's GraphQL reference documentation is itself generated per object, per query, per mutation, so any entity-name keyword a mapping page could target already has three or four exact-match canonical URLs on shopify.dev before the pipeline writes a word. "A pSEO category cannot out-scale the vendor's own schema-generated docs on the vendor's own type names" is the doc's own stated takeaway, and it is a SERP-composition fact, not a volume fact. A five-to-six-page salvage survived, failure-mode pages the reference docs are silent on, deferred behind five other categories, still ungenerated as of this writing.
05 A keyword family dropped for the wrong stated reason
It would be easy to read the opposite incident into this repository's own /flow spec if you only skim it: 192 candidate keywords of the shape "shopify flow <trigger or action name>" were generated from the full Shopify Flow catalog, submitted for volume, and the spec records that "the overwhelming majority returned below the Ads reporting floor," right before stating the family is dropped. Read that as cause and effect and you'd conclude volume killed 192 rows outright, which is exactly the reading this article exists to correct.
The spec's actual sentence names a second, load-bearing fact in the same breath: help.shopify.com "publishes an exact-match reference page for every item in the family." That is the same shape of failure as the GraphQL category above: a vendor that already owns the SERP for every member of an entire keyword family, structurally, regardless of what any individual keyword's volume turns out to be. And the spec does not treat low volume as disqualifying on its own: "individual members may be built only on their own measured non-HARD capture — two were measured, and they split." Two keywords from the family of 192 were checked on their own SERP merits, independent of the family-level volume finding, and one cleared while the other didn't. The family was dropped because the vendor's reference docs make the whole shape structurally unwinnable, the same disqualifier the GraphQL category failed on eleven days later. Volume was the trigger that made someone look, not the reason the look came back negative.
06 Three pages with real volume, pruned anyway
If volume can't save a category, it also can't save an individual page. Three rows in the live queue carry a measured, non-trivial monthly volume and a pruned status, with the prune reason quoted verbatim:
| Keyword | Measured volume | prune_reason |
|---|---|---|
yotpo review | 590/mo | "serp-hard: measured live capture 2026-07-26 scores HARD (H2-front-page-lock)." |
klaviyo review | 590/mo | "serp-hard: measured live capture 2026-07-26 scores HARD (H6-entity-mismatch)." |
loox review | 390/mo | "serp-hard: measured live capture 2026-07-26 scores HARD (H2-front-page-lock)." |
590 searches a month is not a small number in this corpus; it ties for the highest measured volume of any review-template keyword in the queue. Both rows were pruned the same day, for the same reason as a 10/mo keyword would be: the vendor and app-store results already lock the front page, and no amount of demand changes what's sitting in positions one through ten. The gate reads the SERP, not the forecast.
07 Fifty pages with no measurable volume, live
The other direction holds just as literally. Filtering the live queue for rows carrying volume_status: below_floor and status: published returns 50 rows today, spread across six sections:
| Section | Published, below_floor |
|---|---|
alternatives | 13 |
reviews | 14 |
vs | 9 |
pricing | 7 |
connect | 4 |
build | 3 |
Every one of these 50 pages is live on the internet right now, and Google Ads has never reported a measurable search volume for any of them. They published because they cleared the three gates that actually apply: a winnable SERP, a unique fact, a true premise, with no exception written anywhere for the volume column. This is not a loophole the pipeline tolerates; it's the rule from Section 02, applied to real rows instead of stated as a principle.
08 A second number that still isn't a gate
There's a second, easy-to-conflate number in this pipeline worth naming precisely, because this repository's own working log, docs/seo/yield-review.md, treats it with the exact same discipline as forecasted search volume, for a related but distinct reason. Yield is measured, post-publish performance: impressions per page over 90 days in Search Console, computed by scripts/pseo/yield.mjs against templates that are already live, not a forecast of demand for a keyword that hasn't been built yet.
On 2026-08-17, that instrument found the queue mix inverted against measured reality: the best-performing template (pricing, 833 impressions/page) had zero rows left to build, while the worst (vs, 96 impressions/page) held the largest backlog: 375 queued rows, every one of them volume_status: below_floor. The fix, recorded in commit cecf084, was to block 379 vs rows out of the active work pool. The mechanism chosen, and the reasoning for choosing it over the alternatives, is stated in the file directly: markBlocked over prune-queue.mjs, "because nothing about these rows is defective: 111 carry an EASY verdict and 261 MEDIUM, all with live captures. That is precisely why they are blocked (a priority decision, reversible) and not pruned (a defect no evidence repairs)." Every one of those 379 rows' own blocked_reason field carries a stated reversal condition and a review date: it is a queue-ordering decision written down as data, not a verdict.
The same instrument returned again on 2026-08-30 with a harder case: alternatives and review were both now measuring beneath pricing, and every remaining below_floor row in both templates (329 of them) was a candidate for the same treatment. This time the decision split, and the split is the part worth reading closely, because it shows queue-ordering is a judgment call, not an automatic formula run on a number: alternatives' 173 rows were blocked (median 12.5 impressions/page, 8 of 18 mature pages at zero), while review's were kept queued despite a similarly low median, because review carried the corpus's largest evidence base and was hosting demand no pricing page yet answered. Commit 3a4860f records the decision as taken; today's live queue shows alternatives at 173 blocked rows and review still carrying 148 queued. One verbatim blocked_reason, quoted in full because it is the clearest statement in this repository of what "orders, does not gate" means in practice:
template-deprioritised (measured yield rebalance, 2026-08-30; decided 2026-08-31).
[...] BLOCKED, NOT PRUNED: nothing about this row is defective — its capture is
live and it would pass every hard gate — so it goes in the reversible state, and
prune-queue.mjs cannot touch it. REVERSAL CONDITION: unblock when `npm run
pseo:yield` reports `alternatives` median impressions/page within 2x of the
best /apps template on a cohort of at least 20 mature pages, or when this row
acquires a measured (not below_floor) volume [...] Standing review is
docs/seo/yield-review.md, next dated 2026-09-16 — this block expires by review,
not by attrition.
Forecasted search volume and measured real-world yield are two different numbers, gathered at two different points in a page's life, and this pipeline refuses to let either one function as a hard gate. Both are read. Both order the queue. Neither is allowed to declare a page wrong.
09 What ordering-not-gating actually buys you
As of the 2026-09-03 snapshot of docs/seo/data/queue.json: 160 published, 8,182 pruned, 711 blocked, 260 queued, 29 clustered: 9,342 candidate rows in total. If this pipeline gated on volume, the 50 published pages in Section 07 would not exist, and the three pruned pages in Section 06 would be live and losing to a locked SERP right now, quietly telling whoever reads the analytics that programmatic SEO doesn't work here.
The actual failure mode a volume gate produces isn't "too few pages" or "too many": it's the wrong pages, confidently, because the number that decided them measured demand instead of measuring whether the page could ever win or whether it was telling the truth. Building on a winnable, honest, unique SERP that happens to have no measurable search volume costs the same amount of engineering effort as building on a 10,000/month keyword the vendor already owns outright, but only one of those two pages can rank, and volume alone cannot tell you which.
If you're evaluating a programmatic SEO pipeline, anyone's, including a prospective vendor's, the sharper question isn't "how do you find high-volume keywords." It's what happens to the null-volume ones with a winnable SERP, and what happens to the high-volume ones sitting under a vendor's own front page. If the answer to both is "the same thing," volume isn't ordering that pipeline's queue. It's still gating it, just with an extra sentence of denial in the README. That's the kind of system design we do.
Source table
| Claim | Source (this repo) |
|---|---|
| "Search volume is NOT a gate"; the three hard gates; volume only orders the queue | CLAUDE.md ("pSEO: what actually gates a page") |
Three-way volume_status distinction (measured / below_floor / not_submitted) and the "no excuse for not_submitted" rule | CLAUDE.md ("pSEO: what actually gates a page") |
Live counts: 209 measured, 1,559 below_floor, 40 not_submitted | docs/seo/data/queue.json (computed directly, 2026-09-03) |
GraphQL category (Cat 9, 116 keywords) rejected on SERP composition; "Volume ordered nothing here"; named gate keywords below_floor; 32/45 captures HARD | docs/seo/graphql-category-verdict.md §2.1-§2.3, §5; commit 639edbd |
| /flow trigger-action family (192 keywords) dropped for SERP-coverage reasons (help.shopify.com owns exact-match reference pages), not chiefly volume; two members shipped on individually measured non-HARD captures | docs/seo/flow-template-spec.md §2.1 (~line 279-284) |
yotpo review / klaviyo review (590/mo, measured) and loox review (390/mo, measured) pruned as HARD SERPs, verbatim prune_reason | docs/seo/data/queue.json (computed directly, 2026-09-03) |
50 published rows carrying volume_status: below_floor, by-section breakdown | docs/seo/data/queue.json (computed directly, 2026-09-03) |
2026-08-17 yield baseline (pricing 833 impr/page vs. vs 96); 379 vs rows blocked, reversible, "nothing about these rows is defective" | docs/seo/yield-review.md ("2026-08-17 — baseline", "2026-08-17b — the rebalance ACTED ON"); commit cecf084 |
2026-08-30/31 split decision: 173 alternatives rows blocked, review's rows kept queued on evidence-base and pricing-intent-demand grounds | docs/seo/yield-review.md ("STAGED, NOT EXECUTED — the 329-row below-floor block"); commit 3a4860f |
Verbatim blocked_reason for alternatives/ablestar-bulk-product-editor-alternatives; current live counts (alternatives 173 blocked, review 148 queued) | docs/seo/data/queue.json (read directly, 2026-09-03) |
| Current corpus totals: 160 published / 8,182 pruned / 711 blocked / 260 queued / 29 clustered = 9,342 | docs/seo/data/queue.json (computed directly, 2026-09-03) |
07 Frequently asked questions
- If not search volume, what actually decides whether a pSEO page gets built?
- Three hard gates, none of them volume: (1) a winnable SERP, verified against a live capture, not inferred from a rule; (2) an ownable, unique data point that no sibling page also claims, enforced by a machine-checked ledger; (3) a true premise: the page may never assert an absence its own captured search results contradict. Search volume is read, recorded, and used to order the queue when multiple rows are equally eligible. It is never checked as a condition of eligibility.
- Has a page with real, measurable search volume ever been rejected anyway?
- Yes. yotpo review (590/month, measured) and klaviyo review (590/month, measured) were both pruned with the reason "serp-hard: measured live capture 2026-07-26 scores HARD": the review SERP is locked by app-store and vendor-domain results regardless of how many people search the term. Volume told the pipeline the keyword was worth checking; the check itself is what killed it.
- Has a page with zero measurable search volume ever been published anyway?
- Yes: 50 of them, as of the 2026-09-03 queue snapshot, spanning six sections (alternatives, reviews, vs, pricing, connect, build). Every one carries volume_status: below_floor, meaning Google Ads returned no reportable number for the keyword. They published because their SERPs were winnable and their facts were unique and true. Those are the three gates that actually apply.
- Is queue prioritization by volume or by real-world yield the same thing as a gate?
- No, and this repository is explicit about the difference in its own data. In August 2026 the pipeline blocked 379 rows from a low-yield template on measured Search Console performance, then later blocked 173 more from a second template on the same basis, but every one of those rows carries a blocked_reason stating, verbatim, "nothing about this row is defective." They are held back from the work queue, not disqualified from ever publishing; the block is reversible and expires by scheduled review. That is what "orders the queue" means in practice: a row can be deprioritized for a measured performance reason without ever failing a truth gate.