Skip to content

Provide Context from Widgets

Context-aware widgets can provide relevant information to the AI assistant automatically. This guide shows you how to implement context in your web components.

Before starting, you should understand:

Widgets provide context by implementing a getContextForLlm() method that returns an array of ContextSourceDef objects. The system calls this method to discover what context the widget can provide.

Add the method to your web component:

class OrderWidget extends HTMLElement {
private currentOrder: Order | null = null;
getContextForLlm(): ContextSourceDef[] | undefined {
// Return undefined or empty array if no context to provide
if (!this.currentOrder) {
return undefined;
}
return [{
// Unique identifier for this context source
sourceId: 'current-order',
// Description for LLM-based relevance filtering
llmInclusionDescription: 'Details about the order currently being viewed, including items, status, and shipping information',
// Origin: 'auto' for automatic inclusion, 'user' for user-requested
origin: 'auto',
// Display information for UI chips
title: `Order #${this.currentOrder.id}`,
description: `${this.currentOrder.items.length} items, ${this.currentOrder.status}`,
// The actual context data
data: this.currentOrder,
// Should this be added automatically when relevant?
addAutomatically: true,
// Optional: How long this context stays relevant (in milliseconds)
maxAgeMs: 5 * 60 * 1000 // 5 minutes
}];
}
}

When your widget's data changes, notify the system:

class OrderWidget extends HTMLElement {
private chatAppState: IChatAppState;
async loadOrder(orderId: string) {
this.currentOrder = await fetchOrder(orderId);
// Notify system that context has changed
this.chatAppState.updateWidgetContext(this.instanceId);
}
}
interface ContextSourceDef {
// Unique ID for tracking across conversation
sourceId: string;
// Description for LLM relevance filtering
llmInclusionDescription: string;
// How context was added: 'auto' or 'user'
origin: WidgetContextSourceOrigin;
// Display text for UI chip
title: string;
description: string;
// The actual context data (sent to LLM)
data: unknown;
}
interface ContextSourceDef {
// Should context be added automatically?
addAutomatically?: boolean; // Default: false
// How long context stays relevant (milliseconds)
maxAgeMs?: number; // Default: undefined (never expires)
// Lucide icon name for UI chip
lucideIconName?: string; // Example: 'package', 'user', 'chart'
}

Purpose: Unique identifier for deduplication and tracking.

Best Practices:

  • Use kebab-case: current-order, user-profile, chart-data
  • Make it globally unique across your application
  • Keep it stable (don't include dynamic IDs unless necessary)
  • For multiple instances, use: ${baseId}-${uniqueId}
// Good
sourceId: 'current-order'
sourceId: 'user-profile'
// Also good (when you need multiple instances)
sourceId: `chart-${this.chartId}`
sourceId: `order-${orderId}`
// Bad (too generic)
sourceId: 'data'
sourceId: 'context'

Purpose: Helps the filtering LLM determine if this context is relevant to the user's question.

Best Practices:

  • Be specific about what the context contains
  • Mention key fields or categories
  • Write for an LLM, not a human
  • 1-2 sentences is ideal
// Good
llmInclusionDescription: 'Order details including items, quantities, prices, shipping address, and current status'
llmInclusionDescription: 'User profile with account information, preferences, subscription tier, and billing details'
llmInclusionDescription: 'Time-series chart showing sales data over the past 30 days, broken down by region and product category'
// Bad (too vague)
llmInclusionDescription: 'Order information'
llmInclusionDescription: 'User data'

Purpose: Controls whether context appears automatically or requires user action.

When to use true (default for most widgets):

  • Spotlight widgets (always visible)
  • Canvas widgets (embedded in responses)
  • Any context that's clearly relevant to visible UI

When to use false:

  • Dialog widgets (temporary/modal)
  • Widgets with sensitive data requiring explicit consent
  • Context that's rarely relevant
// Spotlight widget - always visible, always relevant
addAutomatically: true
// Dialog widget - user opened it for a reason
addAutomatically: false

Purpose: Defines how long context stays relevant without changing.

When to set it:

  • Real-time data (stock prices, system status)
  • Time-sensitive information (recent orders, today's schedule)
  • Data that becomes stale quickly

When to omit it:

  • Stable reference data (user profile, settings)
  • Historical data that doesn't change
// Real-time stock prices - refresh every 30 seconds
maxAgeMs: 30 * 1000
// Recent orders - relevant for 5 minutes
maxAgeMs: 5 * 60 * 1000
// User profile - no expiration needed
maxAgeMs: undefined

A single widget can return multiple context items:

class DashboardWidget extends HTMLElement {
getContextForLlm(): ContextSourceDef[] | undefined {
const contexts: ContextSourceDef[] = [];
// Add metrics context
if (this.metrics) {
contexts.push({
sourceId: 'dashboard-metrics',
llmInclusionDescription: 'Key performance metrics including revenue, user count, and conversion rates',
origin: 'auto',
title: 'Dashboard Metrics',
description: 'Current KPIs',
data: this.metrics,
addAutomatically: true,
maxAgeMs: 60 * 1000 // 1 minute
});
}
// Add alerts context
if (this.alerts && this.alerts.length > 0) {
contexts.push({
sourceId: 'active-alerts',
llmInclusionDescription: 'Active system alerts and warnings requiring attention',
origin: 'auto',
title: 'Active Alerts',
description: `${this.alerts.length} alerts`,
data: this.alerts,
addAutomatically: true,
maxAgeMs: 30 * 1000 // 30 seconds
});
}
return contexts.length > 0 ? contexts : undefined;
}
}

Only provide context when it's relevant:

getContextForLlm(): ContextSourceDef[] | undefined {
// Don't provide context if widget is minimized
if (this.isMinimized) {
return undefined;
}
// Don't provide context if data is loading
if (this.isLoading) {
return undefined;
}
// Don't provide context if error state
if (this.hasError) {
return undefined;
}
return [{
// ... context definition
}];
}

Include metadata in your data for better LLM understanding:

getContextForLlm(): ContextSourceDef[] | undefined {
return [{
sourceId: 'order-details',
llmInclusionDescription: 'Complete order information',
origin: 'auto',
title: `Order #${this.order.id}`,
description: this.order.status,
data: {
// Include metadata for context
_metadata: {
orderDate: this.order.createdAt,
lastUpdated: this.order.updatedAt,
currency: this.order.currency
},
// Actual order data
...this.order
},
addAutomatically: true
}];
}

For widgets showing filtered or scoped data:

getContextForLlm(): ContextSourceDef[] | undefined {
return [{
sourceId: `orders-${this.filterType}`,
llmInclusionDescription: `List of ${this.filterType} orders with summary information`,
origin: 'auto',
title: `${this.filterType} Orders`,
description: `${this.orders.length} orders`,
data: {
filter: this.filterType,
dateRange: this.dateRange,
orders: this.orders
},
addAutomatically: true,
maxAgeMs: 2 * 60 * 1000 // 2 minutes
}];
}
  1. Open Developer Console and run:
// Get your widget instance
const widget = document.querySelector('your-widget');
// Check context
const context = widget.getContextForLlm();
console.log('Context:', context);
  1. Test in Chat UI:
    • Open chat input
    • Verify context chip appears
    • Click chip to see details
    • Ask a question and verify context is used
// Initial state - no context
getContextForLlm() {
return undefined; // Widget shows no chip
}
// Data loads
async connectedCallback() {
await this.loadData();
this.chatAppState.updateWidgetContext(this.instanceId);
// Now shows chip
}
// Data changes
async refreshData() {
await this.loadData();
this.chatAppState.updateWidgetContext(this.instanceId);
// Chip updates
}
// Widget removed
disconnectedCallback() {
// Chip automatically removed
}
// Always visible - provide context with high confidence
getContextForLlm(): ContextSourceDef[] | undefined {
return [{
sourceId: 'spotlight-data',
llmInclusionDescription: 'Data from the always-visible spotlight panel',
origin: 'auto',
title: 'Spotlight Data',
description: 'Current view',
data: this.data,
addAutomatically: true, // Spotlight is intentionally visible
maxAgeMs: undefined // Stable data
}];
}
// Embedded in responses - context is highly relevant
getContextForLlm(): ContextSourceDef[] | undefined {
return [{
sourceId: `canvas-${this.instanceId}`,
llmInclusionDescription: 'Data visualization embedded in the AI response',
origin: 'auto',
title: 'Chart Data',
description: this.chartType,
data: this.chartData,
addAutomatically: true, // AI put it there for a reason
maxAgeMs: 5 * 60 * 1000 // Relevant for duration of conversation
}];
}
// User explicitly opened - context is intentional
getContextForLlm(): ContextSourceDef[] | undefined {
return [{
sourceId: 'dialog-data',
llmInclusionDescription: 'Data from the dialog the user opened',
origin: 'user', // User action initiated this
title: 'Dialog Data',
description: 'User-requested',
data: this.data,
addAutomatically: false, // User should choose
maxAgeMs: undefined
}];
}

Check:

  1. Is getContextForLlm() returning a value?
  2. Is addAutomatically set to true?
  3. Did you call updateWidgetContext() after data loaded?
  4. Is the widget instance registered with chat app state?
// Debug logging
getContextForLlm(): ContextSourceDef[] | undefined {
const contexts = /* ... build contexts ... */;
console.log('[WidgetContext] Providing contexts:', contexts);
return contexts;
}

Check:

  1. Is llmInclusionDescription specific enough?
  2. Is the description relevant to the question?
  3. Try asking a more specific question
  4. Check if context was filtered out (see browser network tab)

Check:

  1. Set maxAgeMs for time-sensitive data
  2. Ensure sourceId is stable (not changing unnecessarily)
  3. Check if data is actually changing when you think it's stable

Widget context is also used by the Intent Router for:

  • Context-aware command matching: Commands can require specific context paths to be eligible
  • Template interpolation: Response templates and command data can reference context values
// Intent Router command that requires context
{
commandId: 'retry_job',
requiresContext: ['selectedJob.jobId'], // Only matches when job is selected
execution: {
mode: 'direct',
command: {
type: 'renderTag',
tagId: 'myapp.retry-dialog',
renderingContext: 'dialog',
data: {
jobId: '{{context.selectedJob.jobId}}' // Interpolated from context
}
},
responseTemplate: 'Retrying job {{context.selectedJob.name}}...'
}
}

For more on Intent Router commands, see Intent Router Guide →.