Page Runs (Content Evaluation)
Page runs are content evaluation crawls that analyze URLs cited by AI providers. They measure content quality, relevance, brand positioning, and other metrics to help you understand why certain pages are (or aren't) being cited.
How page runs work
Page runs can be triggered in two ways:
-
Automatically -- When a prompt run finishes and the project has
autoCrawlEnabled = true, citation URLs matching your brand's domains are automatically crawled. URLs are skipped if they were recently crawled within thefreshnessThresholdDayswindow. -
Manually -- Trigger a content evaluation for any URL via the API.
Each page run uses the Single Page Requester (SPR) to crawl the URL and run content evaluation metrics.
Status lifecycle
status is an AiVisibilityPageRunStatus enum, not free text:
| Status | Terminal | Description |
|---|---|---|
Pending | No | Created and waiting to be crawled |
Crawling | No | Currently being crawled by SPR |
Completed | Yes | Successfully crawled with metrics |
Failed | Yes | Crawl failed (see failureCode / failureReason) |
Each run also exposes isTerminal, which is true once the run has settled. Prefer it over comparing status
against a list of your own: if a status is added later, isTerminal keeps classifying runs correctly and your
polling loop still terminates. Poll until isTerminal is true rather than deciding a run has finished because
some amount of time has passed -- a run is finished when the API says so, not when a timer expires. A run that
never settles is a fault rather than a slow evaluation, and re-triggering the URL reports it (see
Trigger a page run).
A settled run never changes on its own. The one exception is a retry of a failed copywriting-agent evaluation
sent with the same idempotencyKey: that reuses the existing run rather than creating a second one, so the run
you already hold goes back to Pending. It only happens when you ask for it.
Why a run failed
A failed run carries failureCode, an
AiVisibilityPageRunFailureCode enum. Branch on it
instead of matching failureReason text, which is prose and can be reworded at any time:
| Failure code | Meaning |
|---|---|
CrawlRequestUnconfirmed | The crawl request could not be confirmed; anything that started is not linked |
ProcessingExhaustedRetries | Processing was retried to its limit without reaching a result |
ProcessingFailed | The crawl itself failed; failureReason carries the underlying reason as prose |
CrawlRequestUnconfirmed is worth handling separately from the other two. It deliberately does not say whether
a crawl started: the API cannot tell a request that was never published from one whose confirmation was lost.
What it does guarantee is that nothing which did start will ever report back to this run, so a page may have been
crawled and a credit spent. Treat it as a fault to escalate rather than something to retry silently.
failureCode is null unless status is Failed. It can also be null on a failed run whose failure was not
categorised: runs that failed without recording any reason, and older runs whose recorded reason cannot be
resolved to exactly one code. Treat a null code as "not categorised" rather than as "did not fail",
and fall back to failureReason for those.
List page runs
Retrieve page runs for a project. Optionally filter by URL — the URL is canonicalised before matching, so any spelling of a page returns every run of that page, whichever spelling each run was requested with (see Page Identity):
query GetAiVisibilityPageRuns(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
) {
getAiVisibilityPageRuns(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
first: 20
) {
nodes {
rawId
url
status
isTerminal
precisionScore
recallScore
qualityScore
trustScore
brandMentionScore
brandSentimentScore
brandPositionScore
failureCode
failureReason
createdAt
updatedAt
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
search narrows the list to runs whose page URL contains the given text, case-insensitively. It matches the canonical URL, and the search text is canonicalised the same way, so a pasted URL finds its runs whatever its scheme, www., default port, trailing slashes, #fragment or tracking parameters. It combines with the other filters, and totalCount counts only the matching runs.
Latest completed run in a period
Filter by statuses and by a createdAt date range (both ends inclusive, at most 730 days) to fetch only the runs you need. totalCount counts only the matching runs. With the default createdAt DESC ordering and first: 1, this returns the page's latest completed run in the period:
query GetLatestCompletedPageRun($accountId: ObjectID!, $aiVisibilityProjectId: ObjectID!, $url: String!) {
getAiVisibilityPageRuns(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
url: $url
statuses: [Completed]
createdAt: { start: "2026-09-01T00:00:00Z", end: "2026-09-30T23:59:59Z" }
first: 1
) {
nodes {
id
createdAt
qualityScore
}
totalCount
}
}
An empty statuses list matches no runs.
Score fields
Completed page runs include these numeric scores (0-100 scale):
| Score | Description |
|---|---|
precisionScore | How precisely the content matches the intended topic |
recallScore | How comprehensively the content covers the topic |
uniquenessScore | How unique the content is compared to other sources |
qualityScore | Overall content quality |
trustScore | Trustworthiness of the content |
brandMentionScore | How well the brand is mentioned in the content |
brandSentimentScore | Brand sentiment within the content |
brandPositionScore | How prominently the brand is positioned |
topicalOpportunityScore | Opportunity to improve topical coverage |
evergreenHealthScore | How well the content maintains relevance over time |
qdfScore | Query Deserves Freshness score |
gscQueryScore | Google Search Console query relevance |
Additional fields include boolean metrics (e.g. hasPageChanged, domainInResults), text analysis fields (e.g. primaryPageIntent, topicalOpportunityReasoning), and detailed JSONB evaluation arrays.
Trigger a page run
Manually trigger a content evaluation for a URL:
mutation TriggerAiVisibilityPageRun(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$url: String!
) {
triggerAiVisibilityPageRun(
input: {
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
url: $url
}
) {
aiVisibilityPageRun {
rawId
url
status
isTerminal
failureCode
failureReason
sprRequestId
createdAt
}
alreadyInProgress
}
}
Triggering a page run consumes ContentEvals credits from your account. The mutation validates credit availability before creating the run, and fails with AccountCreditsDepleted when none are left.
alreadyInProgress is true when an evaluation was already running for this URL, in which case nothing new
was started, no further credit was committed, and aiVisibilityPageRun is that existing run. Only one evaluation
runs per URL at a time, so a repeat trigger converges on the one run instead of failing -- treat it as success.
Previously this case returned an error, and it now succeeds even when the account has no credits left, since
nothing new is started.
The run you get back is not necessarily one this user started. Automatic evaluations -- those created from a citation or from SERP discovery -- occupy the same per-URL slot, so surface this as "this page is already being evaluated" rather than as something started in another tab.
If the existing run has been unfinished for far longer than an evaluation takes, the mutation raises
AI_VISIBILITY_PAGE_RUN_ALREADY_IN_PROGRESS instead of converging, with the run's id and status in the error
extensions. That run is stuck and is blocking new evaluations of the URL; surface it rather than retrying, since
retrying cannot clear it.
An evaluation's credit is only charged when it finishes, so availableCredits(creditAllocationType: ContentEvals)
still counts the credit of an evaluation that is currently running. To show a balance that accounts for those, pair
it with the runs whose isTerminal is false.
Page scores (aggregated)
Get aggregated scores across multiple page runs, one row per page. Useful for understanding average content quality over time. getAiVisibilityEvaluatedPages returns them as a connection, for listing, searching and sorting evaluated pages without fetching them all:
query GetAiVisibilityEvaluatedPages(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandId: ObjectID
$dateRange: AiVisibilityDateRangeInput
$search: String
$after: String
) {
getAiVisibilityEvaluatedPages(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandId: $aiVisibilityBrandId
dateRange: $dateRange
search: $search
first: 20
after: $after
orderBy: [{ field: avgQualityScore, direction: ASC }]
) {
nodes {
pageId
canonicalUrl
url
title
avgQualityScore
avgTrustScore
avgVisibilityIndex
totalRuns
latestRunAt
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
Each node returns average values for each score field, along with totalRuns and latestRunAt.
first(up to 100) andafterpage through the results;totalCountis the number of evaluated pages matching the filters.orderBysorts bylatestRunAt(the default, newest first),totalRuns,url(the canonical URL) or any of the averaged scores. Pages with no value for the score being sorted on come last in either direction.searchmatches the canonical URL case-insensitively, as described in Page Identity.urlsrestricts the results to up to 100 pages. Each URL is canonicalised before matching, so any spelling of a page selects it. A URL with no completed page run indateRangeis absent from the results rather than returned empty.pageIdsrestricts the results to up to 100 pages by thepageIdof pages returned by any AI Visibility query, as described in Page Identity. It combines withurls. An emptyurlsorpageIdslist matches no pages.aiVisibilityBrandIdcomputes the citation-based metrics (avgCitationQualityScore,avgBrandPosition,totalBrandCitations,avgVisibilityIndex,topicNames,aiProviderTypes) for that brand, including its merged variants.comparisonDateRangepopulates each node'spreviousPeriodwith the same page's scores over that window, when you select fields insidepreviousPeriod.
To fetch the scores of pages you already have, pass their URLs in one request instead of one request per page:
query GetAiVisibilityPageScoresForUrls(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$urls: [String!]
) {
getAiVisibilityEvaluatedPages(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
urls: $urls
first: 100
) {
nodes {
pageId
canonicalUrl
avgQualityScore
totalRuns
}
totalCount
}
}
getAiVisibilityPageScores is deprecated in favour of getAiVisibilityEvaluatedPages. To migrate, pass url as urls: [url] and pageId as pageIds: [pageId], and read the rows from nodes. Without a page filter, the deprecated query returns up to 10,000 pages in one response, while the connection returns one page of results at a time.
Automatic crawling setup
To enable automatic page run creation when AI providers cite your brand's pages:
- Set
autoCrawlEnabled: trueon your project (see Projects) - Optionally adjust
freshnessThresholdDays(default: 7 days)
The system will:
- Filter citation URLs to match your primary brand's domains (including subdomains)
- Skip URLs that have a recent page run within the freshness threshold
- Skip URLs that already have an in-flight page run (pending or crawling)
Schema reference
AiVisibilityPageRun-- Page run type with all metric fieldsAiVisibilityPageScore-- Aggregated page score typegetAiVisibilityEvaluatedPages-- Paginated page scores- Page Identity -- the
pageIdthat links a run to its page across every other AI Visibility query AiVisibilityPageRunStatus-- Status enumAiVisibilityPageRunFailureCode-- Failure code enum