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 Function showInChat Function labelField Function 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
Performance : Dynamic functions are called for each item, so keep them lightweight
Consistency : Use consistent logic patterns across your application
Fallbacks : Always provide fallback values for unexpected data
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
Use consistent spacing : Leave gaps between order values (1, 10, 20) to allow for future insertions
Group related contexts : Give similar contexts adjacent order values
Prioritize by importance : Most relevant context should have lower order values
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:
Boolean (Default) Number (Custom Threshold) Object (Full Configuration) 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
Shopping Cart
Task Management
File Selection
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.
When no labelField
is specified, the function looks for labels in this order:
title
field
label
field
name
field
id
field
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
:
Single Value → Single Entry Array → Array of Entries Single-Item Array → Array 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:
React Component
Agent Context
// Example 1: User profile (single value)
const [ user ] = useState ({ id: '123' , name: 'Alice' , role: 'admin' });
useSubscribeStateToAgentContext ( 'user' , ( userData ) => ({
currentUser: userData , // Single object
}), { labelField: 'name' });
React Component
Agent Context
// 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' });
React Component
Agent Context
// 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
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
}
);
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.