Skip to main content

Deep Linked Citations

This guide explains how to update a custom chat frontend that uses Glean's /chat REST API to take advantage of deep-linked (direct-quote) citations, while remaining backward compatible with existing responses.

Overview

Deep-linked citations upgrade Glean Chat's citations from document-level to text-level:

  • Citations can now include exact snippets from the source document that support the preceding statement, rather than just pointing to the document as a whole.
  • In the /chat response, this appears as a new optional referenceRanges[].snippets[] array attached to each inline citation fragment.
Backward Compatible

If your integration ignores these new fields, everything continues to work. The change is additive and backward compatible. This guide is about how to opt in and render the richer experience.

Prerequisites

Deep-linked citations are available when all of the following are true:

Agentic Loop Runtime

The /chat request must be running on Agentic Loop (the same runtime used by the Glean UI)

Supported LLM Configuration

The deployment must use a supported LLM configuration for Agentic Loop

note

Deep-linked citations are provided for answers grounded in enterprise content (company documents, email, etc.), not for pure world-knowledge or web search answers. If these conditions are not met, your existing citation UI continues to function as today.

Response Format

Baseline Structure (Unchanged)

Each chat response message still looks like:

{
"messages": [
{
"author": "GLEAN_AI",
"fragments": [
{ "text": "Some generated text. " },
{
"citation": {
"sourceDocument": {
"id": "DOC_ID",
"datasource": "gdrive",
"title": "Document title",
"url": "https://docs.google.com/..."
}
}
}
],
"citations": [ /* existing top-level citations, unchanged */ ]
}
]
}

Your existing integration likely uses:

  • fragments[].text to render the assistant answer
  • fragments[].citation.sourceDocument and/or citations[] to render citation pills

All of that remains valid.

New referenceRanges Field

When deep-linked citations are available for a given citation, the fragment's citation now also includes reference ranges with snippets:

{
"citation": {
"sourceDocument": {
"id": "GMAILNATIVE_THREAD_197cc5e9c51a13e1",
"datasource": "gmailnative",
"connectorType": "FEDERATED_SEARCH",
"title": "Request to get added to ChatGPT Teams to try Codex",
"url": "https://mail.google.com/mail/u/#inbox/197cc5e9c51a13e1"
},
"referenceRanges": [
{
"textRange": {
"startIndex": 50,
"endIndex": 120,
"type": "LINK"
},
"snippets": [
{
"text": "I'd like to try out Codex for engineering tasks — is it possible to get added",
"pageNumber": 1
},
{
"text": "to the ChatGPT Teams account?"
}
]
}
]
}
}

Key points:

FieldDescription
referenceRanges[]Optional array. Many citations will continue to be document-level only.
textRangeDescribes which part of the assistant response the citation is attached to (existing behavior).
snippets[]New: An array of direct-quote snippets from the source document that support the statement.
snippet.textThe quoted text from the source document.
snippet.pageNumberOptional page number where the snippet appears.

Each snippet is a SearchResultSnippet object from the public search/documents API, shared with other Glean surfaces. At minimum it has text: string, but may also include pageNumber: number and other metadata such as snippet identifiers.

note

Top-level citations[] on the message remain unchanged and still hold document-level info. They are not required to implement deep-linking but may be useful for your existing UI logic.

Migration Options

Minimal Migration

Stay compatible without UI changes—just update your types to tolerate the new fields

Full Migration

Render deep-linked citations with hover previews showing direct quotes

Minimal Migration

If you only want to stay compatible without changing your UI:

  1. Update your deserialization model to tolerate the new fields:

    • The citation object now may contain a referenceRanges array with nested snippets
    • If you are using a generated client or strict type definitions, regenerate from the latest OpenAPI spec so these fields are known but optional
  2. Ignore referenceRanges / snippets for now. The behavior will match pre-deep-linked citations. This is a good first step while you plan UI updates.

Full Migration

The goal is to mirror the Glean UI behavior: hovering a citation shows the direct quote that backs the statement, plus surrounding context and (where available) a page number and preview.

High-level flow for each assistant message:

  1. Render fragments[].text as you do today
  2. For each fragments[].citation:
    • Always show the document-level citation pill (title, datasource, icon, etc.) based on sourceDocument—this is unchanged
    • If referenceRanges[].snippets[] is present, use those snippets to populate the hover card / popover with direct quotes and surrounding context
    • Optionally show the pageNumber when present
  3. Clicking the citation can continue to open sourceDocument.url

Fetching Document Content

To display surrounding text around each snippet, fetch the full document text via /getdocuments using the cited document IDs:

POST /rest/api/v1/getdocuments

{
"documentSpecs": [{ "id": "DOC_ID" }],
"includeFields": ["DOCUMENT_CONTENT"]
}

The response includes content.fullTextList which you can join to get the full document text, then search for each snippet's text within that string to derive preceding and following context.

Code Examples

type Snippet = { text: string; pageNumber?: number }

async function loadDocumentContent(docId: string): Promise<string> {
const resp = await fetch("/rest/api/v1/getdocuments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
documentSpecs: [{ id: docId }],
includeFields: ["DOCUMENT_CONTENT"],
}),
})
const data = await resp.json()
const doc = Object.values(data.documents)[0] as any
return (doc?.content?.fullTextList || []).join("\n\n")
}

function findSurroundingText(
fullText: string,
snippetText: string,
windowChars = 200,
): { preceding: string; cited: string; following: string } {
const idx = fullText.indexOf(snippetText)
if (idx === -1) {
return { preceding: "", cited: snippetText, following: "" }
}
const start = Math.max(0, idx - windowChars)
const end = Math.min(fullText.length, idx + snippetText.length + windowChars)
return {
preceding: fullText.slice(start, idx),
cited: fullText.slice(idx, idx + snippetText.length),
following: fullText.slice(idx + snippetText.length, end),
}
}

function CitationPopover({ citation }: { citation: any }) {
const [docContent, setDocContent] = useState<string | null>(null)

useEffect(() => {
if (citation.sourceDocument?.id) {
loadDocumentContent(citation.sourceDocument.id).then(setDocContent)
}
}, [citation.sourceDocument?.id])

const ranges = citation.referenceRanges ?? []

if (!ranges.length) {
// Fallback: document-level citation only
return <div>{citation.sourceDocument?.title}</div>
}

return (
<div className="citation-popover">
<div className="citation-header">
<strong>{citation.sourceDocument?.title}</strong>
{ranges[0]?.snippets?.[0]?.pageNumber != null && (
<span> · Page {ranges[0].snippets[0].pageNumber}</span>
)}
</div>
<div className="snippet-context">
{ranges.flatMap((range, idx) =>
(range.snippets || []).map((s, j) => {
if (!docContent) {
return <div key={`${idx}-${j}`}>{s.text}</div>
}
const { preceding, cited, following } = findSurroundingText(
docContent,
s.text,
)
// Fallback if snippet text couldn't be matched in document
if (!preceding && !following) {
return <div key={`${idx}-${j}`}>{s.text}</div>
}
return (
<p key={`${idx}-${j}`}>
{preceding && <span className="muted">...{preceding}</span>}
<mark>{cited}</mark>
{following && <span className="muted">{following}...</span>}
</p>
)
}),
)}
</div>
</div>
)
}

UX Guidelines

When Users See Deep-Linked Snippets

  • Not every citation pill will have snippet highlights—deep-linking only appears when the LLM confidently matches a snippet
  • When present, the hover popover should:
    • Highlight the cited snippet with emphasis (e.g., background color or bold)
    • Show a few lines before and after for context
    • Optionally show page number if provided
    • Optionally offer an "Expand" button for a larger preview

Limitations

LimitationDescription
Complex formattingTables, spreadsheets, code blocks, and heavy HTML may render as plain text in the snippet preview, which can look noisy or hard to read
Source navigationClicking a citation opens the underlying document. Jumping directly to the exact page/offset is not guaranteed for all sources—it's supported only in some cases (e.g., slide number for certain PPT sources, some page-number support for PDFs) and is still being expanded
CoverageDeep-linked citations are not provided for world-knowledge or web-search-only answers. Some citations may intentionally fall back to document-level when matching is uncertain, to avoid misleading users

Design your UI to gracefully fall back to document-level behavior when no referenceRanges / snippets are present.

Backward Compatibility

The /chat response format change is additive:

  • No existing fields are removed or renamed
  • New fields are optional and can be ignored safely
note

The migration from Chat V2 to Agentic Loop for the REST API is logically separate from deep-linked citations. However, deep-linked citations are only available on Agentic Loop. Customers may notice response quality differences when switching from Chat V2 to Agentic Loop, independent of citation changes.

Key points for API clients:

  • No immediate changes are required—you only need to update your UI if you want to surface direct-quote snippets
  • Regenerate from the latest OpenAPI spec for /chat and /getdocuments
  • Handle unknown/extra fields defensively to accommodate future enhancements (e.g., more snippet metadata, richer deep-link URLs)

Migration Checklist

Verify Server Configuration

Ensure your /chat requests are using Agentic Loop and an LLM configuration that supports deep-linked citations.

Update API Types

Regenerate your client types from the latest OpenAPI spec so ChatMessageCitation.referenceRanges[].snippets[] and SearchResultSnippet.pageNumber are present but optional.

Handle New Fields Defensively

Ensure your deserialization tolerates the new fields. Handle unknown/extra fields defensively to accommodate future enhancements.

Update Citation UI

Keep your existing document-level citation pills (title, icon, click → sourceDocument.url). If referenceRanges[].snippets[] exists, show snippet text in your hover/detail view with optional page number.

Add Contextual Preview (Recommended)

On hover or "expand", call /getdocuments with the cited document IDs and includeFields: ["DOCUMENT_CONTENT"]. Use the returned fullTextList to compute surrounding context and highlight each snippet.