Diagnose traffic losses
For the queries that lost the most clicks, says *why* - and each cause implies a different action, so the class matters more than the number.
Tool name: diagnose_traffic_losses
Cost: Free - not billed, and does not draw on a plan quota.
Read-only
This tool only reads. It never creates or changes anything, so an assistant may call it without asking you first.
For the queries that lost the most clicks, says why - and each cause implies a different action, so the class matters more than the number. It locates the period the series actually stepped and decomposes around that, not across calendar windows, because a mid-window rank drop is smeared by a window average and its impressions loss then reads as falling demand.
The classes: A ranking - outranked in place, strengthen the page. A′ page swap - a different page of yours took the slot, so consolidate rather than update. B CTR - ranking intact, clicks gone, the cheapest fix in the set (title/meta/schema). C visibility - impressions gone with the ranking intact and demand ruled out; a residual here, because no search-console feed exposes SERP composition at any grain, so an AI Overview or a feature block cannot be seen from this data. Run explain_query_serp on a case C (or case B) query to see it: it fetches the SERP live and diffs it against the last one stored. Only the workspace's ~100 weekly-captured queries have a stored baseline; outside that set the first call establishes one. D - a rank loss across the page-one boundary; the impressions followed the ranking, so never report it as demand. E demand - fell on both engines with position flat.
Two things to carry into any answer built on this. A case E row supports 'fewer people searched this phrasing' and not 'fewer people searched for this' - nothing here looks at neighbouring queries, so a reformulation is indistinguishable. And 'this is demand, not you' is a useful result, not a failure: it stops a content cycle being spent on a page that is fine.
This tool attributes a cause, so the row ceiling matters more here than anywhere else on this surface: a period Google truncated is an impressions fall with nothing behind it, and the cross-engine control does not catch it either, since Bing's own feeds are top-N and a query missing from both looks like two engines agreeing. Google stores only about 5,000 rows per day per single-dimension breakdown once a day is more than roughly three days old, so on a large site these rows are a top-N by impressions rather than a complete list. When that applies to the window you asked for, the response says so in unsupportedInputs with the dates - there is nothing to fetch, so read a missing row as 'not in the top ~5,000', never as 'zero'.
How it decides
A drop in clicks looks the same whatever caused it, so the tool works through the causes in order and stops at the first one that fits. It runs the checks that cost nothing before the ones that cost anything.
The first step is not a question but a measurement: find the week or month the numbers actually stepped, and compare before against after. Comparing whole calendar windows averages a mid-window drop away, and a ranking loss hidden that way reads as falling demand.
The last question is why the tool reads both engines. A query that fell on Google and held on Bing did not lose its audience - something changed on Google's results page. Falling on both is the one signature that points at demand.
What each answer means
| What you see | What it means | What to do |
|---|---|---|
| Ranking slipped, same page of yours | A competitor overtook you in place. | Strengthen that page. |
| Ranking slipped, and a different page of yours now ranks | Two of your pages are competing for one slot. | Consolidate them. Updating both makes it worse. |
| Ranking slipped off page one | A ranking loss. The drop in how often you were shown is the result of that, not evidence that interest fell. | Treat it as a ranking problem, not a demand one. |
| Ranking held, shown as often, fewer clicks | Something took the click without taking the position - an AI Overview or featured snippet, a title Google rewrote, or a competitor with a better-written result. | Usually the cheapest fix on the list: rewrite the title and description, target the snippet, add schema. |
| Ranking held, shown less often, Google only | You lost space on the results page itself. | Run explain_query_serp on the query - this is the one cause no search-console data can show. |
| Ranking held, shown less often, on both engines | Fewer people are searching this phrasing. | Reallocate the effort. Check whether it recurs each year before rewriting anything. |
“Nothing to do” is a real answer
The last two rows are the ones an analyst is most tempted to skip. Being told this is demand, not you is worth as much as a fixable problem - it stops a content cycle being spent on a page that is fine.
One caveat on that last row: nothing here looks at neighbouring queries, so it cannot tell falling interest apart from people searching a different wording for the same thing. It supports "fewer people searched this phrasing" - not "fewer people searched for this".
Parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
site | string | Yes | The site to look up, as a bare domain ("example.com") or a URL - scheme, www., path and trailing slash are all stripped. Must resolve to a workspace the caller owns; call list_sites first if unsure of the exact domain. |
granularity | "weekly" | "monthly" | No | Length of one period in the series. Daily is deliberately not offered: these tools ask whether a change was sustained across periods, and a one-day period turns that into weekday seasonality. Defaults to "monthly". |
periods | integer | No | How many whole periods to read. Defaults to 13 monthly periods, the shortest series the seasonal-versus-secular test can run on; shorter windows still diagnose every other class and report seasonality "untested". Defaults to 13. |
to | string | No | Last day of the current window (YYYY-MM-DD). Defaults to the most recent date either engine has data for. |
minImpressions | integer | No | Minimum impressions over the whole window per query. Defaults to 50. |
excludeBranded | boolean | No | Exclude branded queries. On by default: branded queries carry far higher CTR at the same position, so leaving them in pulls the expected-CTR curve upward and under-reports the CTR class on exactly the non-branded queries that matter. Defaults to true. |
checkPageSwap | boolean | No | Run the page↔query join to separate class A from A′. Costs two extra lake scans and is skipped automatically when no row reached class A; turn it off to keep the call cheap and accept pageSwap.status: "unknown". Defaults to true. |
limit | integer | No | How many of the biggest losers to diagnose. Kept small - each row is a paragraph of evidence, not a table row. Defaults to 10. |
searchVertical | "web" | "image" | "video" | No | Which Google search vertical to read: web (default), image, or video. Google-only - Bing Webmaster Tools does not split verticals on any traffic endpoint, so anything other than 'web' returns Google alone, with Bing excluded and declared in unsupportedInputs. A null or absent Bing figure then means 'not measurable', never 'zero'. Defaults to "web". |
Full input JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"site": {
"type": "string",
"minLength": 1,
"description": "The site to look up, as a bare domain (\"example.com\") or a URL - scheme, www., path and trailing slash are all stripped. Must resolve to a workspace the caller owns; call list_sites first if unsure of the exact domain."
},
"granularity": {
"default": "monthly",
"description": "Length of one period in the series. Daily is deliberately not offered: these tools ask whether a change was sustained across periods, and a one-day period turns that into weekday seasonality.",
"type": "string",
"enum": ["weekly", "monthly"]
},
"periods": {
"default": 13,
"description": "How many whole periods to read. Defaults to 13 monthly periods, the shortest series the seasonal-versus-secular test can run on; shorter windows still diagnose every other class and report seasonality \"untested\".",
"type": "integer",
"minimum": 2,
"maximum": 104
},
"to": {
"description": "Last day of the current window (YYYY-MM-DD). Defaults to the most recent date either engine has data for.",
"type": "string"
},
"minImpressions": {
"default": 50,
"description": "Minimum impressions over the whole window per query.",
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"excludeBranded": {
"default": true,
"description": "Exclude branded queries. On by default: branded queries carry far higher CTR at the same position, so leaving them in pulls the expected-CTR curve upward and under-reports the CTR class on exactly the non-branded queries that matter.",
"type": "boolean"
},
"checkPageSwap": {
"default": true,
"description": "Run the page↔query join to separate class A from A′. Costs two extra lake scans and is skipped automatically when no row reached class A; turn it off to keep the call cheap and accept `pageSwap.status: \"unknown\"`.",
"type": "boolean"
},
"limit": {
"default": 10,
"description": "How many of the biggest losers to diagnose. Kept small - each row is a paragraph of evidence, not a table row.",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 50
},
"searchVertical": {
"default": "web",
"description": "Which Google search vertical to read: web (default), image, or video. Google-only - Bing Webmaster Tools does not split verticals on any traffic endpoint, so anything other than 'web' returns Google alone, with Bing excluded and declared in `unsupportedInputs`. A null or absent Bing figure then means 'not measurable', never 'zero'.",
"type": "string",
"enum": ["web", "image", "video"]
}
},
"required": ["site"]
}Response
Full output JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"site": {
"type": "string"
},
"workspaceId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"searchVertical": {
"type": "string",
"enum": ["web", "image", "video"]
},
"periodWindow": {
"type": "object",
"properties": {
"granularity": {
"type": "string"
},
"periods": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"window": {
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string"
}
},
"required": ["start", "end"],
"additionalProperties": false
},
"periodStarts": {
"type": "array",
"items": {
"type": "string"
}
},
"excludedPartialPeriod": {
"anyOf": [
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string"
}
},
"required": ["start", "end"],
"additionalProperties": false
},
{
"type": "null"
}
]
}
},
"required": [
"granularity",
"periods",
"window",
"periodStarts",
"excludedPartialPeriod"
],
"additionalProperties": {}
},
"rows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"primaryEngine": {
"type": "string",
"enum": ["google", "bing"]
},
"case": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"caseLabel": {
"type": "string"
},
"confidence": {
"type": "string"
},
"recommendation": {
"type": "string"
},
"explanation": {
"type": "string"
},
"estimatedClicksLost": {
"type": "number"
},
"change": {
"type": "object",
"properties": {
"changepoint": {
"type": "object",
"properties": {
"periodStart": {
"type": "string"
},
"before": {
"type": "object",
"properties": {
"periods": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"clicks": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"impressions": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"position": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"ctr": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"clicksPerPeriod": {
"type": "number"
},
"impressionsPerPeriod": {
"type": "number"
}
},
"required": [
"periods",
"clicks",
"impressions",
"position",
"ctr",
"clicksPerPeriod",
"impressionsPerPeriod"
],
"additionalProperties": {}
},
"after": {
"type": "object",
"properties": {
"periods": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"clicks": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"impressions": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"position": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"ctr": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"clicksPerPeriod": {
"type": "number"
},
"impressionsPerPeriod": {
"type": "number"
}
},
"required": [
"periods",
"clicks",
"impressions",
"position",
"ctr",
"clicksPerPeriod",
"impressionsPerPeriod"
],
"additionalProperties": {}
}
},
"required": ["periodStart", "before", "after"],
"additionalProperties": {}
},
"deltaClicksPerPeriod": {
"type": "number"
},
"deltaImpressionsPerPeriod": {
"type": "number"
},
"deltaPosition": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"deltaCtr": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"decomposition": {
"type": "object",
"properties": {
"impressionsTerm": {
"type": "number"
},
"ctrTerm": {
"type": "number"
}
},
"required": ["impressionsTerm", "ctrTerm"],
"additionalProperties": {}
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"periodStart": {
"type": "string"
},
"clicks": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"impressions": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"position": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"periodStart",
"clicks",
"impressions",
"position"
],
"additionalProperties": {}
}
}
},
"required": [
"changepoint",
"deltaClicksPerPeriod",
"deltaImpressionsPerPeriod",
"deltaPosition",
"deltaCtr",
"decomposition",
"series"
],
"additionalProperties": {}
},
"crossEngine": {
"type": "object",
"properties": {
"control": {
"type": "string"
},
"otherEngine": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"otherDeltaImpressionsPerPeriod": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"control",
"otherDeltaImpressionsPerPeriod"
],
"additionalProperties": {}
},
"expectedCtr": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"actualCtr": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"pageSwap": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"beforeTopPage": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"afterTopPage": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": ["status"],
"additionalProperties": {}
},
"seasonality": {
"type": "string"
}
},
"required": [
"query",
"primaryEngine",
"case",
"caseLabel",
"confidence",
"recommendation",
"explanation",
"estimatedClicksLost",
"change",
"crossEngine",
"expectedCtr",
"actualCtr",
"pageSwap",
"seasonality"
],
"additionalProperties": {}
}
},
"unsupportedInputs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"engine": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reason": {
"type": "string"
}
},
"required": ["field", "reason"],
"additionalProperties": {}
}
},
"lastSyncedAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"site",
"workspaceId",
"searchVertical",
"periodWindow",
"rows",
"unsupportedInputs"
],
"additionalProperties": false
}Find emerging keywords
Queries with sustained or accelerating growth over several whole periods - find_content_decay's classifier with the sign flipped.
Analyze site sections
Aggregates Google + Bing page performance into URL directories (the leading `sectionDepth` path segments) and compares each section's current total against the immediately preceding period.