| title | Add Citations and Sensitivity Labels to Agent Messages |
|---|---|
| description | Learn how to add citations and Microsoft Purview sensitivity labels to Microsoft Teams agent messages using Teams SDK. |
| ms.topic | how-to |
| ms.localizationpriority | medium |
| ms.date | 07/14/2026 |
This article explains how to implement citations and sensitivity labels for agent responses that use enterprise content.
- Citations show where response content came from.
- Sensitivity labels show how response content should be handled.
For new implementations, send a Microsoft Purview sensitivity label ID in the message payload instead of relying on free-text label metadata.
Use this article to:
- Add citations to agent responses with Teams SDK.
- Add message-level and citation-level sensitivity labels.
- Decide when to use message-level versus citation-level labels.
- Understand rendering behavior and payload validation errors.
This article covers two related capabilities:
- Citations improve source transparency.
- Sensitivity labels improve confidentiality signaling.
Because these capabilities often appear together in AI-generated responses, this article shows them in a single implementation flow.
Note
Citations and sensitivity labels are supported for agent responses in personal chats, group chats, and channels, including GCC, GCC High, and DoD environments.
-
Use citations when your response is based on retrievable sources such as files, messages, and emails.
-
Use sensitivity labels when your response contains sensitive content and you need a consistent confidentiality signal.
If your response includes citations from sensitive sources, add citation-level labels and a message-level label.
In practice, these two capabilities solve different trust questions for users:
- Citations answer "Where did this come from?"
- Sensitivity labels answer "How carefully should I handle this?"
Use them together for high-value scenarios such as policy summaries, legal/compliance responses, or responses derived from internal documents.
For agents built with Teams SDK, use addCitation() to add in-text references and citation metadata.
After you add citations, continue to add sensitivity labels if your response contains sensitive content.
Each citation links an in-text marker (for example, [1]) to structured source metadata. This helps users inspect the source quickly and decide whether to trust or validate the response.
In Teams, citations render as inline footnote-style references such as [1] and [2]. When users hover over a citation, Teams shows the source title, abstract, and URL.
In grounded agent flows, citation positions are usually assigned during retrieval or tool execution. Preserve those positions when you build the final reply so the inline reference and citation metadata stay aligned.
In the Teams SDK TypeScript guidance, citations are typically attached to the final message activity after the app determines which citation markers are present in the generated text. This is especially useful for streamed responses, where the app emits text incrementally and then adds citations to the final marker activity.
Keep citation data concise and stable:
- Use a readable
nameso users recognize the source immediately. - Include
urlonly when users are allowed to navigate to the source. - Keep
abstractshort and descriptive. - Use
keywordsfor discoverability, not as extra summary text. - Keep citation
positionvalues stable throughout the turn.
For streamed agent responses, a common TypeScript pattern is:
- Stream the generated text to the user.
- Collect the final response text for the turn.
- Extract the citation markers that appear in that final text.
- Attach only those citations to the final activity.
This avoids emitting citations for sources that were retrieved but never referenced in the response.
The following example shows the pattern at a high level:
function attachUsedCitations(activity: MessageActivity, fullText: string, citations: CitationEntry[]) {
const usedPositions = new Set(
[...fullText.matchAll(/\[(\d+)\]/g)].map((match) => Number(match[1]))
);
for (const citation of citations) {
if (!usedPositions.has(citation.position)) {
continue;
}
activity.addCitation(citation.position, {
name: citation.title ?? `Source ${citation.position}`,
abstract: citation.snippet ?? "No description available.",
url: citation.url,
});
}
}
const finalMarker = new MessageActivity().addAiGenerated().addFeedback("custom");
attachUsedCitations(finalMarker, fullText, collectedCitations);
stream.emit(finalMarker);Use this pattern if your agent builds responses from retrieval or tool outputs and streams the result before sending the final message metadata.
app.message(/citation/i, async ({ send }) => {
const appearance = {
name: "Contoso Design Spec",
url: "https://example.com/spec",
abstract: "Design specification used to generate this response.",
keywords: ["design", "spec", "agent"],
icon: "Microsoft Word",
};
await send(
new MessageActivity("Here are the key updates from the spec [1]")
.addCitation(1, appearance)
);
});async Task SendCitations(IContext context)
{
var message = new MessageActivity
{
Text = "Here are the key updates from the spec [1]"
};
message.AddCitation(1, new CitationAppearance
{
Name = "Contoso Design Spec",
Url = "https://example.com/spec",
Abstract = "Design specification used to generate this response.",
Keywords = new List<string> { "design", "spec", "agent" },
Icon = CitationIcon.MicrosoftWord
});
await context.Send(message);
}@app.on_message_pattern(re.compile(r"citation", re.IGNORECASE))
async def add_citations(ctx: ActivityContext[MessageActivity]):
await ctx.send(
MessageActivityInput(
text="Here are the key updates from the spec [1]",
).add_citation(
position=1,
appearance=CitationAppearance(
name="Contoso Design Spec",
url="https://example.com/spec",
abstract="Design specification used to generate this response.",
keywords=["design", "spec", "agent"],
icon=CitationIconName.MICROSOFT_WORD,
),
)
)| Property | Type | Required | Description |
|---|---|---|---|
citation |
Object | Yes | Details of the citation. |
citation.@type |
String | Yes | Citation object type. Allowed value: Claim. |
citation.position |
Integer | Yes | Citation number. Must be unique in a message. |
citation.appearance |
Object | Yes | Citation appearance details. |
citation.appearance.@type |
String | Yes | Appearance object type. Allowed value: DigitalDocument. |
citation.appearance.name |
String | Yes | Citation title. Max 80 characters. |
citation.appearance.url |
String | No | Citation URL. |
citation.appearance.abstract |
String | No | Citation abstract. Max 160 characters. |
citation.appearance.keywords |
Array | No | Up to three keywords. Each keyword max 28 characters. |
citation.appearance.image.name |
String | No | Citation icon name. In Teams SDK addCitation() examples, this is provided as icon and mapped to the citation image metadata. |
These are the citation fields referenced in the snippets on this page.
If you need a full schema-level reference for every optional field, use the broader bot message formatting documentation in addition to this implementation guide.
When you generate the final response, attach only the citations whose positions actually appear in the response text. For example, if the response contains [1] and [3], attach citation metadata only for positions 1 and 3.
This keeps the response clean and avoids showing references the user never sees in the message body.
This pattern is especially important for grounded or streamed responses where your retrieval layer may produce more candidate sources than the final response uses.
If your agent streams text, collect the final text for the turn and then attach citations that match the positions present in that text.
This guidance aligns with the TypeScript Teams SDK in-depth guide, where citation attachment is treated as a final assembly step for the response activity.
Note
- A maximum of 20 citations are displayed in a message.
- Citations with Adaptive Cards are available in public developer preview.
Using tenant-defined Purview labels ensures that:
- Labels shown in agent responses stay consistent with labels used across Microsoft 365.
- Users see recognizable label names and descriptions.
- Agents avoid free-text label values that can be inconsistent.
This consistency matters because users often interpret the same label text across apps as the same policy intent. Sending Purview IDs helps prevent custom label text that looks valid but doesn't represent a tenant-defined classification.
For new implementations, send the sensitivity label ID in the message entity (usageInfo.@id).
If an app sends both legacy title/description fields and label ID, Teams uses the label ID.
For citation-level labeling, use the same ID model in citation usageInfo.
Treat the label ID as the authoritative contract value. The displayed title and description are resolved from that ID by the platform.
Note
Legacy title/description payloads remain available for backward compatibility, but they're not supported as the primary contract for new implementations.
Common ways to get sensitivity label IDs:
- From source content APIs that already return label metadata.
- From Microsoft Graph sensitivity label APIs.
For Graph API documentation, see:
If your source content isn't in Microsoft 365, apply labels carefully so the label signal remains accurate.
Don't set a label unless your app can reliably derive it from source content or a policy-backed mapping.
For most agent patterns, you should capture the label ID as close to the content retrieval step as possible and preserve it through your response generation pipeline.
The following example shows message-level labeling by ID.
Use message-level labeling when the response should be treated uniformly. This is the simplest pattern and is usually the right default for responses summarizing multiple sensitive sources.
await context.sendActivity({
type: ActivityTypes.Message,
text: "Hey, I'm a friendly AI bot. This message is generated through AI [1]",
entities: [
{
type: "https://schema.org/Message",
"@type": "Message",
"@context": "https://schema.org",
usageInfo: {
"@type": "CreativeWork",
"@id": "purview-label-id",
},
},
],
});The following example shows message-level and citation-level labeling by ID.
Use citation-level labeling when different sources in the same response have different classifications. This gives users finer context when reviewing individual references.
await context.sendActivity({
type: ActivityTypes.Message,
text: "Hey, I'm a friendly AI bot. This message is generated through AI [1]",
entities: [
{
type: "https://schema.org/Message",
"@type": "Message",
"@context": "https://schema.org",
usageInfo: {
"@type": "CreativeWork",
"@id": "purview-label-id",
},
citation: [
{
"@type": "Claim",
position: 1,
appearance: {
"@type": "DigitalDocument",
name: "Source document",
url: "https://example.com/source",
usageInfo: {
"@type": "CreativeWork",
"@id": "purview-label-id",
},
},
},
],
},
],
});Use message-level sensitivity labels when the whole response should be treated consistently.
Use citation-level labels when only specific references are sensitive.
If both are present, Teams can compute a top-level message label from available signals.
If you need a simple rule, use both levels and ensure the message-level label is at least as restrictive as the most sensitive citation.
This helps avoid under-labeling the overall response when one source is significantly more sensitive than others.
Users can hover over the sensitivity icon to view label details.
Teams may compute the top-level label from available sensitivity signals. If citation-level labels imply higher sensitivity, the displayed top-level label can reflect the highest applicable sensitivity.
Teams also indicates that the sensitivity label was set by the agent/bot.
From a user perspective, this preserves two important signals:
- The response has a tenant-recognized classification label.
- The label assignment came from the bot workflow, not from automatic document inheritance.
- Use source-of-truth label IDs from Microsoft 365 content whenever available.
- Use message-level labels when the full response is sensitive or when citation-level labeling is not practical.
- Use citation-level labels when specific sources have different sensitivity levels.
- If uncertain, include message-level and citation-level labels.
- Keep message-level label at least as restrictive as the most sensitive cited source.
Also consider operational practices:
- Log label ID resolution failures so you can detect broken source integrations quickly.
- Keep a defensive fallback path that omits labels instead of sending guessed labels.
- Validate label IDs before send to reduce avoidable 400 errors.
| Error code | Description |
|---|---|
| 400 | Multiple root message entities found under entities array. |
| 400 | Error parsing message entity from entities array. |
| 400 | Bot message with more than 20 citations. |
| 400 | The appearance object is empty. |
| 400 | Error while parsing citation entity with ID: X. |
| 400 | Citation-level usageInfo.@id doesn't match message-level usageInfo.@id in at least one instance. |
| 400 | Multiple citation-level usageInfo entries share the same @id but have conflicting metadata. |
For Graph API permissions and API-specific error details, see Microsoft Graph documentation.
For choosing label IDs, see Get Purview label IDs.
When troubleshooting, start in this order:
- Validate payload structure and required fields.
- Confirm message-level and citation-level IDs are consistent for the response.
- Verify the ID exists and is accessible in the current tenant context.