# Themes https://api-docs.lumar.io/docs/ai-visibility/ai-visibility-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](/docs/ai-visibility/ai-visibility-projects.md)) 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: ```graphql 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 } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityBrandId": "QWlWaXNpYmlsaXR5QnJhbmQx", "dateRange": { "start": "2025-01-01", "end": "2025-01-31" } } ``` 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`](#list-the-claims-behind-a-theme) 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: ```graphql 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 } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityBrandIds": ["QWlWaXNpYmlsaXR5QnJhbmQx"], "dateRange": { "start": "2025-01-01", "end": "2025-01-31" } } ``` `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: ```graphql 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 } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityBrandId": "QWlWaXNpYmlsaXR5QnJhbmQx", "aiVisibilityThemeId": "QWlWaXNpYmlsaXR5VGhlbWUx", "dateRange": { "start": "2025-01-01", "end": "2025-01-31" } } ``` 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`, `Neutral` or `Negative`. `Neutral` means the statement carries no valence, never that it is balanced. Filter with `polarities` to return only some of them, or order by `polarity` (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 to [`getAiVisibilityPromptRunDetails`](/docs/ai-visibility/ai-visibility-prompt-runs.md) to read the claim in context. - `aiVisibilityThemeSentimentRawId` -- the scored answer (the `rawId` of a [`getAiVisibilityThemeSentimentDetails`](#drill-into-the-scored-answers-behind-a-theme) row) the claim is evidence for. - `textSpan` -- character offsets of the quote within the prompt run's full answer text (`fullAnswerText` on `getAiVisibilityPromptRunDetails`), like a mention's `textSpan`. - `citationPositions` and `citations` -- 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: ```graphql 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 } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityBrandId": "QWlWaXNpYmlsaXR5QnJhbmQx", "aiVisibilityThemeId": "QWlWaXNpYmlsaXR5VGhlbWUx", "dateRange": { "start": "2025-01-01", "end": "2025-01-31" } } ``` :::note 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: ```graphql mutation MergeAiVisibilityThemes( $accountId: ObjectID! $aiVisibilityProjectId: ObjectID! $aiVisibilityThemeIds: [ObjectID!]! ) { mergeAiVisibilityThemes( input: { accountId: $accountId aiVisibilityProjectId: $aiVisibilityProjectId aiVisibilityThemeIds: $aiVisibilityThemeIds } ) { queued themes { id rawId upstreamThemeId canonicalThemeId status } } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityThemeIds": ["QWlWaXNpYmlsaXR5VGhlbWUx", "QWlWaXNpYmlsaXR5VGhlbWUy"] } ``` :::note Unlike [brand merges](/docs/ai-visibility/ai-visibility-brands.md#merge-brands), 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`](#list-theme-sentiment) may not reflect it for some time. ::: :::caution 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: false` and does not re-queue a merge