Cedar’s useSubscribeStateToAgentContext function allows you to automatically make any Cedar-registered state available to AI agents as context. This enables agents to understand your app’s current state and provide more relevant, contextual responses.
Prerequisite – The state you want to subscribe must first be registered in Cedar using either useCedarState or useRegisterState.

useSubscribeStateToAgentContext Overview

The useSubscribeStateToAgentContext function subscribes to local state changes and automatically updates the agent’s input context whenever the state changes. This means your AI agent always has access to the most up-to-date information from your application, including any Zod schemas defined for the state.

Function Signature

function useSubscribeStateToAgentContext<T>(
	stateKey: string,
	mapFn: (state: T) => Record<string, any>,
	options?: {
		icon?: ReactNode | ((item: ElementType<T>) => ReactNode);
		color?: string;
		labelField?: string | ((item: ElementType<T>) => string);
		order?: number;
		showInChat?: boolean | ((entry: ContextEntry) => boolean);
		collapse?:
			| boolean
			| number
			| {
					threshold: number;
					label?: string;
					icon?: ReactNode;
			  };
	}
): void;

Parameters

  • stateKey: string - The registered state key that we want to subscribe to
  • mapFn: (state: T) => Record<string, any> - Function that maps your state to context entries
  • options (optional) - Configuration for visual representation and label extraction:
    • icon?: ReactNode | ((item: ElementType<T>) => ReactNode) - Icon to display for this context. Can be a static React element or a function that returns an icon based on each item
    • color?: string - Hex color for visual styling
    • labelField?: string | ((item: ElementType<T>) => string) - How to extract labels from your data. Can be a field name or a function
    • order?: number - Display order for context badges (lower numbers appear first)
    • showInChat?: boolean | ((entry: ContextEntry) => boolean) - Whether to show context badges in chat. Can be a boolean or a function to filter specific entries (default = true)
    • collapse?: boolean | number | { threshold: number; label?: string; icon?: ReactNode } - Collapse multiple entries into a single badge. Can be boolean (default threshold 5), number (custom threshold), or object with full configuration

Basic Usage Example

Here’s a simple example with a todo list:
import { useCedarState, useSubscribeStateToAgentContext } from 'cedar-os';
import { CheckCircle } from 'lucide-react';

function TodoApp() {
	// 1) Register the state in Cedar
	const [todos, setTodos] = useCedarState(
		'todos',
		[
			{ id: 1, text: 'Buy groceries', completed: false },
			{ id: 2, text: 'Walk the dog', completed: true },
		],
		'Todo items'
	);

	// 2) Subscribe that state to input context
	useSubscribeStateToAgentContext(
		'todos',
		(todoList) => ({
			todos: todoList, // Key 'todos' will be available to the agent
		}),
		{
			icon: <CheckCircle />,
			color: '#10B981', // Green color
			labelField: 'text', // Use the 'text' field as label for each todo
		}
	);

	return (
		<div>
			<TodoList todos={todos} onToggle={setTodos} />
			<ChatInput /> {/* Agent can now see todos in context */}
		</div>
	);
}

Complex State Example

Here’s a more advanced example from the Product Roadmap demo:
import { useRegisterState, useSubscribeStateToAgentContext } from 'cedar-os';
import { Box } from 'lucide-react';
import { Node } from 'reactflow';

interface FeatureNodeData {
	title: string;
	status: 'planned' | 'in-progress' | 'completed';
	priority: 'low' | 'medium' | 'high';
	description?: string;
}

function SelectedNodesPanel() {
	const [selected, setSelected] = useState<Node<FeatureNodeData>[]>([]);

	// Register the react-flow selection array in Cedar
	useRegisterState({
		key: 'selectedNodes',
		value: selected,
		description: 'Currently selected nodes in the canvas',
	});

	// Subscribe selected nodes to input context
	useSubscribeStateToAgentContext(
		'selectedNodes',
		(nodes: Node<FeatureNodeData>[]) => ({
			selectedNodes: nodes.map((node) => ({
				id: node.id,
				title: node.data.title,
				status: node.data.status,
				priority: node.data.priority,
				description: node.data.description,
			})),
		}),
		{
			// Dynamic icons based on status
			icon: (node) => {
				switch (node.status) {
					case 'completed':
						return '✅';
					case 'in-progress':
						return '🔄';
					case 'planned':
						return '📋';
					default:
						return <Box />;
				}
			},
			color: '#8B5CF6', // Purple color for selected nodes
			labelField: 'title', // Use title field for labels
			// Only show high priority items in chat
			showInChat: (entry) => {
				const node = entry.data;
				return node.priority === 'high';
			},
			// Collapse when more than 3 nodes selected
			collapse: {
				threshold: 3,
				label: '{count} Selected Features',
				icon: <Box />,
			},
		}
	);

	// Update selection when user selects nodes
	useOnSelectionChange({
		onChange: ({ nodes }) => setSelected(nodes),
	});

	return (
		<div>
			<h4>Selected Nodes</h4>
			{selected.map((node) => (
				<div key={node.id}>{node.data.title}</div>
			))}
		</div>
	);
}

Using the labelField Option

The labelField option allows you to specify how labels should be extracted from your data. This is especially useful when your data has custom field names or when you need custom label logic.

labelField as a String

Specify which field to use as the label:
const [users, setUsers] = useState([
	{ userId: 'u1', fullName: 'John Doe', email: 'john@example.com' },
	{ userId: 'u2', fullName: 'Jane Smith', email: 'jane@example.com' },
]);

useSubscribeStateToAgentContext(
	users,
	(userList) => ({ activeUsers: userList }),
	{
		labelField: 'fullName', // Will use fullName as the label
		icon: <User />,
		color: '#3B82F6',
	}
);

labelField as a Function

Use a function for custom label generation:
const [products, setProducts] = useState([
	{ sku: 'P001', name: 'Laptop', price: 999, inStock: true },
	{ sku: 'P002', name: 'Mouse', price: 29, inStock: false },
]);

useSubscribeStateToAgentContext(
	products,
	(productList) => ({ inventory: productList }),
	{
		labelField: (product) =>
			`${product.name} (${product.inStock ? 'In Stock' : 'Out of Stock'})`,
		icon: <Package />,
		color: '#10B981',
	}
);

Dynamic Icons and Conditional Display

Cedar supports dynamic behavior for both icons and chat display through function-based options. This allows you to customize the appearance and visibility of context entries based on their actual data.

Dynamic Icon Functions

The icon option can accept a function that receives each item and returns an appropriate icon:
import { CheckCircle, Clock, AlertCircle, Archive } from 'lucide-react';

const [tasks, setTasks] = useState([
	{ id: 1, title: 'Design Review', status: 'completed' },
	{ id: 2, title: 'Code Implementation', status: 'in-progress' },
	{ id: 3, title: 'Testing', status: 'pending' },
	{ id: 4, title: 'Documentation', status: 'archived' },
]);

useSubscribeStateToAgentContext(
	'tasks',
	(taskList) => ({ activeTasks: taskList }),
	{
		labelField: 'title',
		color: '#3B82F6',
		// Dynamic icon based on task status
		icon: (task) => {
			switch (task.status) {
				case 'completed':
					return <CheckCircle className='text-green-500' />;
				case 'in-progress':
					return <Clock className='text-blue-500' />;
				case 'pending':
					return <AlertCircle className='text-yellow-500' />;
				case 'archived':
					return <Archive className='text-gray-500' />;
				default:
					return <Clock />;
			}
		},
	}
);

Conditional Chat Display with showInChat

The showInChat option can be a function that determines whether specific entries should appear as badges in the chat UI:
const [products, setProducts] = useState([
	{ id: 1, name: 'Laptop', category: 'electronics', inStock: true },
	{ id: 2, name: 'Notebook', category: 'office', inStock: false },
	{ id: 3, name: 'Phone', category: 'electronics', inStock: true },
]);

useSubscribeStateToAgentContext(
	'products',
	(productList) => ({ inventory: productList }),
	{
		labelField: 'name',
		icon: <Package />,
		color: '#10B981',
		// Only show products that are in stock in the chat UI
		showInChat: (entry) => {
			const product = entry.data;
			return product.inStock === true;
		},
	}
);

Advanced Dynamic Configuration Example

Here’s a comprehensive example combining dynamic icons, conditional display, and collapsing from the Product Roadmap demo:
import { Box } from 'lucide-react';

interface FeatureNodeData {
	title: string;
	status: 'done' | 'in progress' | 'planned' | 'backlog';
	priority: 'high' | 'medium' | 'low';
}

const [selectedNodes, setSelectedNodes] = useState<Node<FeatureNodeData>[]>([]);

useSubscribeStateToAgentContext(
	'selectedNodes',
	(nodes) => ({ selectedNodes: nodes }),
	{
		// Dynamic icons based on node status
		icon: (node) => {
			const status = node?.data?.status;
			switch (status) {
				case 'done':
					return '✅';
				case 'in progress':
					return '🔄';
				case 'planned':
					return '📋';
				case 'backlog':
					return '📝';
				default:
					return <Box />;
			}
		},
		color: '#8B5CF6',
		labelField: (node) => node?.data?.title,
		// Only show nodes that are not in backlog status in chat context
		showInChat: (entry) => {
			const node = entry.data;
			return node?.data?.status !== 'backlog';
		},
		order: 2,
		// Collapse into a single badge when more than 5 nodes are selected
		collapse: {
			threshold: 5,
			label: '{count} Selected Nodes',
			icon: <Box />,
		},
	}
);

Function Parameter Types

When using function-based options, the parameters are strongly typed:
icon: (item: ElementType<T>) => ReactNode
  • item - Individual item from your data array (or the single item if not an array)
  • Returns - Any valid React node (JSX element, string, emoji, etc.)

Best Practices for Dynamic Options

  1. Performance: Dynamic functions are called for each item, so keep them lightweight
  2. Consistency: Use consistent logic patterns across your application
  3. Fallbacks: Always provide fallback values for unexpected data
  4. Type Safety: Leverage TypeScript for better development experience
// ✅ Good: Lightweight with fallbacks
icon: (task) => {
	const status = task?.status || 'unknown';
	const iconMap = {
		completed: '✅',
		pending: '⏳',
		failed: '❌',
		unknown: '❓',
	};
	return iconMap[status];
},

// ❌ Avoid: Heavy computations or API calls
icon: (task) => {
	// Don't do expensive operations here
	const complexCalculation = performHeavyComputation(task);
	return getIconFromAPI(complexCalculation); // Async operations won't work
},

Single Values Support

useSubscribeInputContext now supports single values (not just arrays):
const [count, setCount] = useState(42);
const [isEnabled, setIsEnabled] = useState(true);
const [userName, setUserName] = useState('Alice');

// Subscribe a number
useSubscribeStateToAgentContext(count, (value) => ({ itemCount: value }), {
	icon: <Hash />,
	color: '#F59E0B',
});

// Subscribe a boolean
useSubscribeStateToAgentContext(
	isEnabled,
	(value) => ({ featureEnabled: value }),
	{
		icon: <ToggleLeft />,
		color: '#10B981',
	}
);

// Subscribe a string with custom label
useSubscribeStateToAgentContext(userName, (value) => ({ currentUser: value }), {
	labelField: (name) => `User: ${name}`,
	icon: <User />,
	color: '#8B5CF6',
});

Controlling Display Order

The order property allows you to control the display order of context badges in the UI. This is useful when you have multiple context subscriptions and want to prioritize their visibility.

How Order Works

  • Lower numbers appear first: order: 1 will appear before order: 10
  • Default behavior: Items without an order are treated as having the maximum order value
  • Stable sorting: Items with the same order maintain their original relative position

Basic Order Example

function PrioritizedContext() {
	const [criticalAlerts, setCriticalAlerts] = useState([]);
	const [normalTasks, setNormalTasks] = useState([]);
	const [archivedItems, setArchivedItems] = useState([]);

	// Critical alerts appear first (order: 1)
	useSubscribeStateToAgentContext(
		criticalAlerts,
		(alerts) => ({ criticalAlerts: alerts }),
		{
			icon: <AlertCircle />,
			color: '#EF4444',
			order: 1, // Highest priority - appears first
		}
	);

	// Normal tasks appear second (order: 10)
	useSubscribeStateToAgentContext(
		normalTasks,
		(tasks) => ({ activeTasks: tasks }),
		{
			icon: <CheckCircle />,
			color: '#3B82F6',
			order: 10, // Medium priority
		}
	);

	// Archived items appear last (order: 100)
	useSubscribeStateToAgentContext(
		archivedItems,
		(items) => ({ archivedItems: items }),
		{
			icon: <Archive />,
			color: '#6B7280',
			order: 100, // Low priority - appears last
		}
	);

	return <ChatInput />;
}

Complex Order Example with Mention Providers

When combining useSubscribeInputContext with mention providers, you can create a well-organized context display:
import { useStateBasedMentionProvider } from 'cedar-os';

function OrganizedWorkspace() {
	const [selectedNodes, setSelectedNodes] = useState([]);
	const [user, setUser] = useState(null);

	// User context appears first (order: 1)
	useSubscribeStateToAgentContext(
		user,
		(userData) => ({ currentUser: userData }),
		{
			icon: <User />,
			color: '#8B5CF6',
			labelField: 'name',
			order: 1, // User info always visible first
		}
	);

	// Selected items appear second (order: 5)
	useSubscribeStateToAgentContext(
		selectedNodes,
		(nodes) => ({ selectedNodes: nodes }),
		{
			icon: <Box />,
			color: '#F59E0B',
			labelField: 'title',
			order: 5, // Selection context after user
		}
	);

	// Mention provider for all nodes (order: 10)
	useStateBasedMentionProvider({
		stateKey: 'nodes',
		trigger: '@',
		labelField: 'title',
		description: 'All nodes',
		icon: <Box />,
		color: '#3B82F6',
		order: 10, // Available nodes after selections
	});

	// Mention provider for connections (order: 20)
	useStateBasedMentionProvider({
		stateKey: 'edges',
		trigger: '@',
		labelField: (edge) => `${edge.source}${edge.target}`,
		description: 'Connections',
		icon: <ArrowRight />,
		color: '#10B981',
		order: 20, // Connections appear last
	});

	return <ChatInput />;
}

Order Best Practices

  1. Use consistent spacing: Leave gaps between order values (1, 10, 20) to allow for future insertions
  2. Group related contexts: Give similar contexts adjacent order values
  3. Prioritize by importance: Most relevant context should have lower order values
  4. Document your ordering: Comment why certain items have specific orders
// Order schema for our app:
// 1-9: User and session data (critical context)
// 10-19: Current selections and active state
// 20-49: General application state
// 50-99: Historical or computed data
// 100+: Low priority or debug information

useSubscribeStateToAgentContext('sessionData', mapper, { order: 1 }); // Critical
useSubscribeStateToAgentContext('selectedItems', mapper, { order: 10 }); // Active
useSubscribeStateToAgentContext('appState', mapper, { order: 20 }); // General
useSubscribeStateToAgentContext('history', mapper, { order: 50 }); // Historical
useSubscribeStateToAgentContext('debugInfo', mapper, { order: 100 }); // Debug

Collapsing Multiple Entries

The collapse option allows you to automatically collapse multiple context entries into a single badge when the number of entries exceeds a threshold. This is particularly useful for managing UI clutter when dealing with large datasets.

Collapse Configuration Types

The collapse option supports three different configuration formats:
Set to true to enable collapsing with default settings:
useSubscribeStateToAgentContext(
	'selectedItems',
	(items) => ({ selectedItems: items }),
	{
		collapse: true, // Uses default threshold of 5
		icon: <Box />,
		color: '#3B82F6',
	}
);
When collapse: true, items will collapse into a single badge when more than 5 entries are present.

Collapse Label Templates

When using the object format, the label field supports template variables:
  • {count} - Replaced with the actual number of entries
  • Static text - Any other text is displayed as-is
// Examples of label templates
collapse: {
	threshold: 3,
	label: '{count} Items', // Displays "5 Items" when 5 entries
}

collapse: {
	threshold: 8,
	label: 'Multiple Selections ({count})', // Displays "Multiple Selections (12)"
}

collapse: {
	threshold: 5,
	label: 'Batch Selection', // Static label, no count shown
}

Real-World Collapse Examples

const [cartItems, setCartItems] = useState([
	{ id: 1, name: 'Laptop', price: 999 },
	{ id: 2, name: 'Mouse', price: 29 },
	{ id: 3, name: 'Keyboard', price: 79 },
	// ... more items
]);

useSubscribeStateToAgentContext(
'cart',
(items) => ({ cartItems: items }),
{
labelField: 'name',
icon: <ShoppingCart />,
color: '#10B981',
// Collapse when cart has more than 3 items
collapse: {
threshold: 3,
label: '{count} Items in Cart',
icon: <ShoppingBag />,
},
}
);

Dynamic Collapse Behavior

The collapse feature is dynamic and responsive to state changes:
function DynamicCollapseExample() {
	const [items, setItems] = useState([]);

	useSubscribeStateToAgentContext(
		'dynamicItems',
		(itemList) => ({ dynamicItems: itemList }),
		{
			collapse: {
				threshold: 4,
				label: 'Multiple Items ({count})',
				icon: <Package />,
			},
			icon: <Box />,
			color: '#8B5CF6',
		}
	);

	// When items.length <= 4: Shows individual badges
	// When items.length > 4: Shows single collapsed badge
	return (
		<div>
			<button onClick={() => setItems([...items, { id: Date.now() }])}>
				Add Item
			</button>
			<ChatInput /> {/* Badges automatically collapse/expand */}
		</div>
	);
}
Performance Tip: Collapsing is particularly useful for performance when dealing with large datasets, as it reduces the number of DOM elements rendered in the chat UI while still providing all the data to the agent.

Default Label Extraction

When no labelField is specified, the function looks for labels in this order:
  1. title field
  2. label field
  3. name field
  4. id field
  5. String representation of the value
When using useSubscribeInputContext, entries are automatically marked with source: 'subscription'.

Output Behavior and Structure

What Gets Sent to the Agent

When the agent receives context from useSubscribeStateToAgentContext, it gets a simplified structure containing only the essential data:
{
	"contextKey": {
		"source": "subscription",
		"data": {
			/* your actual data */
		}
	}
}
The agent only receives the source and data fields. Visual metadata like icon, color, and label are used only for UI display and are not sent to the agent.

Array Behavior

The output structure depends on how you pass data to useSubscribeStateToAgentContext:
When your mapFn returns a single non-array value, it’s stored as a single context entry:
// Input: Single object
useSubscribeStateToAgentContext('user', (userData) => ({
  currentUser: userData, // Single object
}));

// Agent receives:
{
  "currentUser": {
    "source": "subscription",
    "data": { "id": "123", "name": "John Doe" }
  }
}

Practical Examples

Here are real-world examples showing the input and output:
// Example 1: User profile (single value)
const [user] = useState({ id: '123', name: 'Alice', role: 'admin' });

useSubscribeStateToAgentContext('user', (userData) => ({
currentUser: userData, // Single object
}), { labelField: 'name' });

// Example 2: Shopping cart (array)
const [cart] = useState([
  { id: 'p1', name: 'Laptop', price: 999 },
  { id: 'p2', name: 'Mouse', price: 29 }
]);

useSubscribeStateToAgentContext('cart', (items) => ({
cartItems: items, // Array
}), { labelField: 'name' });

// Example 3: Single selected item (preserved as array)
const [selected] = useState([
  { id: 'node1', title: 'Important Feature', status: 'planned' }
]);

useSubscribeStateToAgentContext('selection', (nodes) => ({
selectedNodes: nodes, // Single-item array
}), { labelField: 'title' });

Why preserve array structure? This allows the agent to understand whether you’re working with a single item or a collection, even when that collection has only one item. This distinction can be important for generating appropriate responses.

Multiple Context Keys

When you return multiple keys from your mapFn, each follows the same behavior rules:
useSubscribeStateToAgentContext('appState', (state) => ({
	currentUser: state.user, // Single object → single entry
	activeTasks: state.tasks, // Array → array of entries
	selectedItems: [state.selected], // Single-item array → array
	preferences: state.prefs, // Single object → single entry
}));

// Agent receives all four keys with appropriate structures

Multiple State Subscriptions

You can subscribe multiple pieces of state:
function MyApp() {
	const [user, setUser] = useState(null);
	const [preferences, setPreferences] = useState({});
	const [currentPage, setCurrentPage] = useState('/dashboard');

	// Subscribe user data
	useSubscribeStateToAgentContext(
		user,
		(userData) => ({
			currentUser: userData
				? {
						id: userData.id,
						name: userData.name,
						role: userData.role,
				  }
				: null,
		}),
		{
			icon: <User />,
			color: '#3B82F6',
		}
	);

	// Subscribe preferences
	useSubscribeStateToAgentContext(
		preferences,
		(prefs) => ({
			userPreferences: prefs,
		}),
		{
			icon: <Settings />,
			color: '#6B7280',
		}
	);

	// Subscribe navigation state
	useSubscribeStateToAgentContext(
		currentPage,
		(page) => ({
			currentPage: {
				path: page,
				timestamp: new Date().toISOString(),
			},
		}),
		{
			icon: <Navigation />,
			color: '#F59E0B',
		}
	);

	return <YourAppContent />;
}

Best Practices

1. Transform Sensitive Data

Don’t expose sensitive information to the agent:
useSubscribeStateToAgentContext('userProfile', (profile) => ({
	user: {
		name: profile.name,
		tier: profile.subscriptionTier,
		preferences: profile.preferences,
		// Don't include: email, password, tokens, etc.
	},
}));

2. Use Meaningful Keys

Choose descriptive keys for your context:
useSubscribeStateToAgentContext('shoppingCart', (cart) => ({
	shoppingCart: cart.items, // Clear and descriptive
	cartTotal: cart.total,
	cartItemCount: cart.items.length,
}));

3. Optimize Large Data Sets

For large data sets, consider filtering or summarizing:
useSubscribeStateToAgentContext(allTransactions, (transactions) => ({
	recentTransactions: transactions
		.slice(0, 10) // Only last 10 transactions
		.map((t) => ({
			id: t.id,
			amount: t.amount,
			date: t.date,
			// Exclude detailed metadata
		})),
	transactionSummary: {
		total: transactions.length,
		totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
	},
}));

Visual Customization

The options parameter allows you to customize how the context appears in the UI:
import { Star, AlertCircle, CheckCircle } from 'lucide-react';

// Different colors and icons for different priorities
useSubscribeStateToAgentContext(
	highPriorityTasks,
	(tasks) => ({ highPriorityTasks: tasks }),
	{
		icon: <AlertCircle />,
		color: '#EF4444', // Red for high priority
	}
);

useSubscribeStateToAgentContext(
	completedTasks,
	(tasks) => ({ completedTasks: tasks }),
	{
		icon: <CheckCircle />,
		color: '#10B981', // Green for completed
	}
);

useSubscribeStateToAgentContext(
	starredItems,
	(items) => ({ starredItems: items }),
	{
		icon: <Star />,
		color: '#F59E0B', // Yellow for starred
	}
);

Integration with Chat Input

The subscribed context automatically becomes available to your AI agent when using Cedar’s chat components:
import { ChatInput } from 'cedar-os-components';

function MyChat() {
	// Your useSubscribeInputContext calls here...

	return (
		<div>
			{/* Context is automatically included when user sends messages */}
			<ChatInput placeholder='Ask me about your todos, selected nodes, or anything else...' />
		</div>
	);
}
The agent will receive the context in a structured format and can reference it when generating responses, making the conversation more contextual and relevant to your application’s current state.