Score SEO opportunities
One prioritized queue over four tools that each answer a different question: find_low_hanging_keywords, find_ctr_opportunities, find_content_decay and find_content_gaps.
Tool name: score_seo_opportunities
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.
One prioritized queue over four tools that each answer a different question: find_low_hanging_keywords, find_ctr_opportunities, find_content_decay and find_content_gaps. Use it to answer "what should I work on" - call the individual tools when you already know which kind of problem you are chasing, since each carries thresholds this one fixes at their defaults.
Every row is priced in one unit: estimatedClicks, the clicks per windowDays at stake if the finding were fixed. Three things about that number you must carry into any answer. It takes the maximum of overlapping signals on one entity, never the sum - a query that is both low-hanging and under-clicking is one recovery, not two - so do not add the signals up. Its basis says what it assumes, and estimateBases in the response spells each one out: a decay row is clicks that were measurably earned and lost, while the other three are projections onto the site's own CTR curve. And a null estimatedClicks means the site's data could not price that row, not that it is worth zero - those rows sort last.
The queue mixes queries and pages (entityType), because the sources work on different objects; the same string as a query and as a page is two rows. sources reports which types were evaluated, which you excluded, and how many candidates each produced - say so rather than implying the queue is exhaustive. That matters most for Google's row ceiling: a ranked queue reads as "these are the best available actions" in a way a filtered list does not, so when the response declares a truncated window, say the queue is a top-N of what was visible rather than of the site. 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'.
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. |
windowDays | integer | No | Length of the current window, in days. Defaults to 28. |
to | string | No | Last day of the current window (YYYY-MM-DD). Defaults to the most recent date either engine has data for. |
types | string | No | Comma-separated opportunity types to evaluate: low_hanging, ctr, decay, gap. Narrow it to keep the call cheap - low_hanging/ctr/gap share one lake scan, and decay costs a second. Defaults to "low_hanging,ctr,decay,gap". |
minImpressions | integer | No | Shared floor: entities below this many impressions over the window are not opportunities on any source. Defaults to 10. |
excludeBranded | boolean | No | Exclude branded queries. On by default - you cannot optimize your way up your own brand SERP, and their outlier CTR raises the target every projection here is measured against. Defaults to true. |
decayBy | "query" | "page" | No | Dimension the decay source reads. Pages by default, matching find_content_decay: a decaying page is the thing you act on. Defaults to "page". |
decayGranularity | "weekly" | "monthly" | No | Period length for the decay source only. Defaults to "monthly". |
decayPeriods | integer | No | How many whole periods the decay source reads. Defaults to 6. |
checkCannibalization | boolean | No | Run the page↔query join so a gap row several of your own pages already split is recommended as a consolidation rather than as a page to create - opposite advice. Costs one extra lake read, and is skipped automatically when no gap row was found. Defaults to true. |
limit | integer | No | How many opportunities to return, highest first. Defaults to 25. |
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."
},
"windowDays": {
"default": 28,
"description": "Length of the current window, in days.",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 365
},
"to": {
"description": "Last day of the current window (YYYY-MM-DD). Defaults to the most recent date either engine has data for.",
"type": "string"
},
"types": {
"default": "low_hanging,ctr,decay,gap",
"description": "Comma-separated opportunity types to evaluate: low_hanging, ctr, decay, gap. Narrow it to keep the call cheap - low_hanging/ctr/gap share one lake scan, and decay costs a second.",
"type": "string"
},
"minImpressions": {
"default": 10,
"description": "Shared floor: entities below this many impressions over the window are not opportunities on any source.",
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"excludeBranded": {
"default": true,
"description": "Exclude branded queries. On by default - you cannot optimize your way up your own brand SERP, and their outlier CTR raises the target every projection here is measured against.",
"type": "boolean"
},
"decayBy": {
"default": "page",
"description": "Dimension the decay source reads. Pages by default, matching find_content_decay: a decaying page is the thing you act on.",
"type": "string",
"enum": ["query", "page"]
},
"decayGranularity": {
"default": "monthly",
"description": "Period length for the decay source only.",
"type": "string",
"enum": ["weekly", "monthly"]
},
"decayPeriods": {
"default": 6,
"description": "How many whole periods the decay source reads.",
"type": "integer",
"minimum": 2,
"maximum": 104
},
"checkCannibalization": {
"default": true,
"description": "Run the page↔query join so a `gap` row several of your own pages already split is recommended as a consolidation rather than as a page to create - opposite advice. Costs one extra lake read, and is skipped automatically when no gap row was found.",
"type": "boolean"
},
"limit": {
"default": 25,
"description": "How many opportunities to return, highest first.",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 200
},
"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"]
},
"currentWindow": {
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string"
}
},
"required": ["start", "end"],
"additionalProperties": false
},
"periodWindow": {
"anyOf": [
{
"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": {}
},
{
"type": "null"
}
]
},
"rows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entityType": {
"type": "string"
},
"label": {
"type": "string"
},
"estimatedClicks": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"leadingKind": {
"type": "string"
},
"confidence": {
"type": "string"
},
"recommendation": {
"type": "string"
},
"kinds": {
"type": "array",
"items": {
"type": "string"
}
},
"signals": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"engine": {
"type": "string"
},
"estimatedClicks": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"basis": {
"type": "string"
},
"confidence": {
"type": "string"
},
"detail": {
"type": "string"
},
"position": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"impressions": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"clicks": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"kind",
"engine",
"estimatedClicks",
"basis",
"confidence",
"detail",
"position",
"impressions",
"clicks"
],
"additionalProperties": {}
}
},
"totalImpressions": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"totalClicks": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"entityType",
"label",
"estimatedClicks",
"leadingKind",
"confidence",
"recommendation",
"kinds",
"signals",
"totalImpressions",
"totalClicks"
],
"additionalProperties": {}
}
},
"sources": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"entityType": {
"type": "string"
},
"status": {
"type": "string"
},
"candidates": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"pricedCandidates": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"kind",
"entityType",
"status",
"candidates",
"pricedCandidates"
],
"additionalProperties": {}
}
},
"estimateBases": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"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",
"currentWindow",
"periodWindow",
"rows",
"sources",
"estimateBases",
"unsupportedInputs"
],
"additionalProperties": false
}Get workspace brand terms
The brand terms a workspace matches queries and AI answers against: `brandTerms` is the effective list (title plus confirmed aliases) and is what analyze_branded_vs_nonbranded and find_low_hanging_keywords actually use.
Generate search performance report
A Google + Bing performance report for one site or several, assembled by calling the other tools and embedding their responses whole - so a section here is the same answer the tool gives on its own, and you can read it the same way, `unsup…