Themes
Themes group brand sentiment by subject (e.g. "Pricing", "Ease of Use") within a project. A project must opt in via brandThemeSentimentCaptureEnabled (see Projects) for theme sentiment to be captured on its prompt runs.
List theme sentiment
Retrieve theme sentiment for a project, aggregated per theme for the given brand:
query GetAiVisibilityThemeSentiments(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandId: ObjectID!
$dateRange: AiVisibilityDateRangeInput
) {
getAiVisibilityThemeSentiments(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandId: $aiVisibilityBrandId
dateRange: $dateRange
first: 15
) {
nodes {
id
rawId
upstreamThemeId
canonicalThemeId
title
description
status
unresolved
avgSentiment
totalMentions
runsCount
totalClaims
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
A theme can be unresolved -- it carries sentiment but no title/description/status yet, while the theme's details are still being resolved. An empty or unresolved series is a normal state, not an error.
Each theme's totalClaims is the number of quoted statements behind it in the date range -- the size of the list getAiVisibilityThemeSentimentClaims returns for that theme. It is 0 for sentiment captured before claims shipped, and the list can be ordered by it (orderBy: [{ field: totalClaims, direction: DESC }]).
Compare brands across themes
Retrieve the themes-by-brands sentiment grid for a project in one query. Pass aiVisibilityBrandIds to restrict the columns; omit it for every brand with theme sentiment in scope:
query GetAiVisibilityThemeSentimentMatrix(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandIds: [ObjectID!]
$dateRange: AiVisibilityDateRangeInput
) {
getAiVisibilityThemeSentimentMatrix(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandIds: $aiVisibilityBrandIds
dateRange: $dateRange
) {
themes {
rawId
title
unresolved
}
brands {
rawId
name
type
primary
}
cells {
aiVisibilityThemeRawId
aiVisibilityBrandRawId
avgSentiment
totalMentions
runsCount
totalClaims
}
truncated
}
}
cells is sparse: a missing (theme, brand) pair means that brand had no sentiment on that theme in the date range. Each cell carries the same aggregates as a theme sentiment list item, including totalClaims -- the number of quoted statements behind that brand and theme, matching the list's totalClaims for the same brand and filters. Rows are capped at the 200 themes with the most sentiment rows; truncated is true when more themes were in scope, so narrow the date range, topic or prompt to see the rest.
List the claims behind a theme
getAiVisibilityThemeSentiments tells you how a theme scores; getAiVisibilityThemeSentimentClaims shows what was actually said. It returns every claim behind one theme for a brand as a flat, paginated list -- the statements the AI made about the brand on that theme, quoted verbatim from the answer, newest answers first by default:
query GetAiVisibilityThemeSentimentClaims(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandId: ObjectID!
$aiVisibilityThemeId: ObjectID!
$dateRange: AiVisibilityDateRangeInput
) {
getAiVisibilityThemeSentimentClaims(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandId: $aiVisibilityBrandId
aiVisibilityThemeId: $aiVisibilityThemeId
dateRange: $dateRange
polarities: [Negative]
first: 25
) {
nodes {
rawId
quote
polarity
promptText
aiProviderType
aiVisibilityPromptRunId
aiVisibilityThemeSentimentRawId
createdAt
textSpan {
start
end
}
citations {
position
url
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
Each claim carries:
quote-- the statement as written in the answer, often a clause rather than a full sentence. A statement that pulls both ways ("great but pricey") arrives as two claims.polarity--Positive,NeutralorNegative.Neutralmeans the statement carries no valence, never that it is balanced. Filter withpolaritiesto return only some of them, or order bypolarity(negative first when ascending).promptText,aiProviderType,createdAt-- the prompt whose answer made the claim, the AI platform that answered, and the prompt run's date.aiVisibilityPromptRunId-- the prompt run whose answer made the claim; pass it togetAiVisibilityPromptRunDetailsto read the claim in context.aiVisibilityThemeSentimentRawId-- the scored answer (therawIdof agetAiVisibilityThemeSentimentDetailsrow) the claim is evidence for.textSpan-- character offsets of the quote within the prompt run's full answer text (fullAnswerTextongetAiVisibilityPromptRunDetails), like a mention'stextSpan.citationPositionsandcitations-- the answer's citations placed in the claim's enclosing sentence, as positions and resolved to their URLs. An empty list means the provider cited nothing inline for that sentence, which is common and not an error.
totalCount counts claims, not answers, and matches the theme's totalClaims for the same filters.
Drill into the scored answers behind a theme
getAiVisibilityThemeSentimentDetails returns the same evidence grouped by scored answer: one row per answer behind the theme, with the answer's 0-100 sentiment and sentimentJustification, and its claims when you select them:
query GetAiVisibilityThemeSentimentDetails(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandId: ObjectID!
$aiVisibilityThemeId: ObjectID!
$dateRange: AiVisibilityDateRangeInput
) {
getAiVisibilityThemeSentimentDetails(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandId: $aiVisibilityBrandId
aiVisibilityThemeId: $aiVisibilityThemeId
dateRange: $dateRange
first: 15
) {
nodes {
rawId
sentiment
sentimentJustification
brandName
aiProviderType
aiVisibilityPromptRunId
createdAt
claims {
position
quote
polarity
promptText
textSpan {
start
end
}
citationPositions
citations {
position
url
}
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
An answer's 0-100 sentiment is a holistic judgement of the whole passage, not an average of its claim polarities -- "great but pricey" scores high, not medium. The two can legitimately disagree, so do not recompute one from the other. Claims are only requested from the database when you select claims; answers scored before claims were captured return an empty list.
Merge themes
When two or more themes represent the same or closely related subject, you can request a merge:
mutation MergeAiVisibilityThemes(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityThemeIds: [ObjectID!]!
) {
mergeAiVisibilityThemes(
input: {
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityThemeIds: $aiVisibilityThemeIds
}
) {
queued
themes {
id
rawId
upstreamThemeId
canonicalThemeId
status
}
}
}
Unlike brand merges, you don't choose a source and target: submit the group of theme IDs to merge, and the resulting theme may be one of the ones you submitted or a new theme covering all of them. The merge also runs asynchronously: this mutation only queues the request and returns the requested themes as they stood before the merge, not the merged result. There is currently no reliable way to detect when it has taken effect -- the themes returned by getAiVisibilityThemeSentiments may not reflect it for some time.
This action cannot be undone.
Validation rules
- At least two distinct theme IDs are required
- All themes must belong to the specified project
- Safe to call again for a pair that already resolved to the same theme -- returns
queued: falseand does not re-queue a merge