Cedar-OS + Mastra Integration Guide
A complete reference for building agentic applications with Cedar-OS (frontend state management) and Mastra (backend agent orchestration). This guide shows you how to wire backend AI agents to frontend UI updates with zero boilerplate.
The Core Pattern
Cedar-OS enables backend tools to auto-update frontend state through structured responses. Instead of manually orchestrating UI updates in agent code, your Mastra tools return a special object and Cedar handles the rest.
// Backend tool returns this { type: 'setState' as const, stateKey: 'currentFile', setterKey: 'showFile', args: { path: '/src/app.tsx', content: '...' }, _raw: { success: true } } // Cedar automatically calls the registered state setter // UI updates immediately—no additional agent logic needed
Request flow:
- User sends message via Cedar chat interface
- Cedar forwards to Mastra backend agent
- Agent executes tools
- Tools return Cedar-structured responses
- Cedar intercepts and updates frontend state
- UI re-renders with new state
Frontend: State Management
Pattern 1: Custom State Setters with useRegisterState
Use this when you need custom logic for state updates (validation, transformations, side effects).
import { useRegisterState } from 'cedar-os'; import React from 'react'; import { z } from 'zod'; // 1. Define state with React.useState (NOT useCedarState) const [files, setFiles] = React.useState<FileNode[]>([]); // 2. Register state with Cedar and define custom setters useRegisterState({ key: 'files', description: 'All files in the current sandbox', value: files, setValue: setFiles, stateSetters: { updateFileTree: { name: 'updateFileTree', description: 'Update the file tree structure from sandbox', argsSchema: z.object({ files: z.array(z.any()).describe('Array of file nodes'), }), execute: (currentState, setValue, args: { files: FileNode[] }) => { console.log('[STATE SETTER] updateFileTree called with:', args.files); setValue(args.files); }, }, }, });
Critical rules:
- Use
React.useState+useRegisterStatefor states with custom setters - Use
useCedarStateONLY for simple states without custom setters - Never use both for the same state—it causes conflicts
- State key must be unique and match backend tool responses
- Setter key must match the
setterKeyin backend tool responses
Pattern 2: Frontend Tools with useRegisterFrontendTool
Use this sparingly—when the agent needs to explicitly call a UI manipulation tool. Prefer state setters for most cases.
import { useRegisterFrontendTool } from 'cedar-os'; import { z } from 'zod'; useRegisterFrontendTool({ name: 'show-file-content', description: 'Display the content of a file in the code preview', argsSchema: z.object({ path: z.string().describe('The path of the file'), content: z.string().describe('The content of the file'), language: z.string().optional().describe('Programming language'), }), execute: async (args) => { console.log('[FRONTEND TOOL] show-file-content called with:', args); const file: CodeFile = { path: args.path, content: args.content, language: args.language || 'text', lastModified: new Date().toISOString(), }; setCurrentFile(file); setViewMode('preview'); }, });
When to choose:
- State setters: Backend tools auto-update UI (preferred for most cases)
- Frontend tools: Agent explicitly calls tool to update UI (use sparingly)
Pattern 3: Subscribe State to Backend with useSubscribeStateToAgentContext
Send frontend state to the backend agent for context awareness.
import { useSubscribeStateToAgentContext } from 'cedar-os'; useSubscribeStateToAgentContext( 'sandbox', // Must match a registered state key (sandbox: SandboxInfo | null) => ({ 'sandbox-status': sandbox ? { id: sandbox.id, status: sandbox.status, url: sandbox.url, } : null, }), { showInChat: true, color: '#8B5CF6', } );
Rules:
- First parameter must match an existing
useRegisterStatekey - Transform function should return serializable data
- Avoid reactive dependencies (like
files.length) inside transform function
Frontend: Message Renderers
Custom renderers display Mastra streaming events (tool calls, results, etc.) in the chat UI.
Location: src/cedar/messageRenderers.tsx
import { createMessageRenderer, CustomMastraMessage } from 'cedar-os'; type ToolCallMessage = CustomMastraMessage<'tool-call'>; export const ToolCallRenderer = createMessageRenderer<ToolCallMessage>({ type: 'tool-call', namespace: 'mastra', render: (message) => { const toolName = message.payload?.toolName || 'Unknown Tool'; return ( <div className="flex items-center gap-2 p-2 text-sm text-blue-600"> <span>🔧</span> <span>Calling tool: <span className="font-semibold">{toolName}</span></span> </div> ); }, validateMessage: (msg): msg is ToolCallMessage => { return msg.type === 'tool-call' && 'payload' in msg; }, }); // Export all renderers as array export const messageRenderers = [ToolCallRenderer, ToolResultRenderer];
Register in layout:
// src/app/layout.tsx import { messageRenderers } from '@/cedar/messageRenderers'; <CedarCopilot messageRenderers={messageRenderers} // ... other props >
Rules:
- Use
CustomMastraMessage<T>for Mastra streaming events - Use
CustomMessage<T, P>for custom app-specific messages - Set
namespace: 'mastra'when overriding Mastra's default renderers - Always include
validateMessagefor proper type narrowing - Export as array from messageRenderers.tsx
Backend: Cedar-Compatible Tools
Creating Tools That Auto-Update UI
Location: src/backend/src/mastra/tools/*.ts
import { createTool } from '@mastra/core/tools'; import z from 'zod'; export const writeFile = createTool({ id: 'writeFile', description: 'Write a file to the sandbox', inputSchema: z.object({ sandboxId: z.string().describe('The sandboxId'), path: z.string().describe('File path'), content: z.string().describe('File content'), }), outputSchema: z .object({ success: z.boolean(), path: z.string(), }) .or(z.object({ error: z.string() })) .or( z.object({ type: z.literal('setState'), stateKey: z.string(), setterKey: z.string(), args: z.any(), _raw: z.any(), }), ), execute: async ({ context }) => { try { // Do the actual work await sandbox.files.write(context.path, context.content); // Return Cedar-structured response return { type: 'setState' as const, // CRITICAL: Use 'as const' stateKey: 'currentFile', // Must match frontend state key setterKey: 'showFile', // Must match state setter name args: { // Arguments for the setter path: context.path, content: context.content, }, _raw: { // Original response for backward compatibility success: true, path: context.path, }, }; } catch (e) { return { error: JSON.stringify(e) }; } }, });
Critical rules:
- Always use
type: 'setState' as const(literal type required) stateKeymust exactly match frontenduseRegisterStatekeysetterKeymust exactly match the state setter name- Include
_rawfor backward compatibility and debugging - Add Cedar response type to outputSchema union
- Return error object on failure:
{ error: string }
When to Use Cedar Responses
Use Cedar responses when:
- ✅ Tool should auto-update UI (read/write files, build file tree)
- ✅ Tool creates/updates resources that have UI representation
- ✅ Want seamless UI updates without agent orchestration
- ✅ Tool result directly maps to a state update
Don't use Cedar responses when:
- ❌ Tool is purely computational (no UI impact)
- ❌ Agent needs to interpret result before UI update
- ❌ Tool is a query that agent will process further
- ❌ Tool has side effects agent needs to coordinate
Common Cedar Tool Patterns
Pattern A: Auto-Display File Content
return { type: 'setState' as const, stateKey: 'currentFile', setterKey: 'showFile', args: { path: filePath, content: fileContent, }, _raw: { success: true, path: filePath }, };
Pattern B: Auto-Update File Tree
return { type: 'setState' as const, stateKey: 'files', setterKey: 'updateFileTree', args: { files: fileTreeArray, }, _raw: { files: fileTreeArray, rootPath: '/home/user' }, };
Pattern C: Auto-Update Sandbox Info with URL
return { type: 'setState' as const, stateKey: 'sandbox', setterKey: 'updateSandbox', args: { id: sandboxId, status: 'active' as const, port: 3000, url: `https://${host}`, startedAt: new Date().toISOString(), }, _raw: { sandboxId, port: 3000 }, };
Common Mistakes
❌ Mistake 1: Using Both useCedarState and useRegisterState
// WRONG - causes conflicts const [files, setFiles] = useCedarState({ key: 'files', initialValue: [] }); useRegisterState({ key: 'files', value: files, setValue: setFiles, ... });
✅ Solution:
// RIGHT - use one or the other const [files, setFiles] = React.useState<FileNode[]>([]); useRegisterState({ key: 'files', value: files, setValue: setFiles, ... });
❌ Mistake 2: Missing 'as const' on setState Type
// WRONG - type inference fails return { type: 'setState', // Inferred as string stateKey: 'files', ... };
✅ Solution:
// RIGHT return { type: 'setState' as const, // Literal type stateKey: 'files', ... };
❌ Mistake 3: Mismatched Keys
// Frontend useRegisterState({ key: 'currentFile', stateSetters: { showFile: ... } }); // Backend - WRONG keys return { type: 'setState' as const, stateKey: 'file', // Should be 'currentFile' setterKey: 'displayFile', // Should be 'showFile' ... };
✅ Solution:
// Backend - matching keys return { type: 'setState' as const, stateKey: 'currentFile', // Exact match setterKey: 'showFile', // Exact match ... };
❌ Mistake 4: Duplicate Frontend Tools and State Setters
// WRONG - duplication causes confusion useRegisterState({ key: 'files', stateSetters: { updateFileTree: ... } }); useRegisterFrontendTool({ name: 'update-file-tree', // Duplicates state setter execute: async (args) => setFiles(args.files) });
✅ Solution: Choose one approach (prefer state setters).
❌ Mistake 5: Wrong Message Type for Mastra Events
// WRONG - custom message type type ToolCallMessage = CustomMessage<'tool-call', { payload: any }>;
✅ Solution:
// RIGHT - Mastra message type type ToolCallMessage = CustomMastraMessage<'tool-call'>;
Debugging
Frontend Not Updating After Tool Call
-
Check console logs:
[STATE SETTER]prefix = state setter was called[FRONTEND TOOL]prefix = frontend tool was called- Neither = Cedar didn't recognize response
-
Verify state registration:
// Must have matching key useRegisterState({ key: 'currentFile', ... }) -
Verify backend response structure:
// Check debugger for tool result { type: 'setState', // Must be present stateKey: 'currentFile', // Must match frontend setterKey: 'showFile', // Must match frontend args: { ... } } -
Check for errors:
- Open Cedar debugger (DebuggerPanel component)
- Look for "fallback:unhandled" or "unknown" messages
- Check browser console for Cedar errors
-
Verify outputSchema includes Cedar response:
outputSchema: z.object({ ... }) .or(z.object({ error: z.string() })) .or(z.object({ type: z.literal('setState'), stateKey: z.string(), setterKey: z.string(), args: z.any(), _raw: z.any(), }))
Message Rendering Issues
-
Check message type:
- Open Cedar debugger → Response Handlers
- Look for "fallback:unhandled" or "unknown:*" tags
-
Verify message renderer:
// Must use CustomMastraMessage for Mastra events type ToolCallMessage = CustomMastraMessage<'tool-call'>; // Must set namespace for overrides createMessageRenderer<ToolCallMessage>({ type: 'tool-call', namespace: 'mastra', // Required for override ... }) -
Verify registration:
// layout.tsx import { messageRenderers } from '@/cedar/messageRenderers'; <CedarCopilot messageRenderers={messageRenderers} ... />
File Organization
src/
├── app/
│ ├── layout.tsx # Register messageRenderers here
│ └── page.tsx # Define states and tools here
├── cedar/
│ ├── messageRenderers.tsx # Custom message renderers
│ └── components/
│ └── text/
│ └── ShimmerText.tsx # Reusable UI components
├── backend/
│ └── src/
│ └── mastra/
│ ├── agents/
│ │ └── coding-agent.ts # Agent configuration
│ └── tools/
│ └── e2b.ts # Tool definitions with Cedar responses
└── types/
└── *.ts # Shared TypeScript types
Best Practices
Frontend
- Use
React.useState+useRegisterStatefor states with custom setters - Use
useCedarStateONLY for simple states without setters - Never duplicate state management approaches
- Keep state keys unique and descriptive
- Add console logs during development for debugging
Backend
- Return Cedar-structured responses for UI-impacting tools
- Always use
type: 'setState' as const - Match
stateKeyandsetterKeyexactly to frontend - Include
_rawfield for backward compatibility - Add Cedar response type to outputSchema union
- Return
{ error: string }on failures
Message Rendering
- Use
CustomMastraMessage<T>for Mastra events - Use
CustomMessage<T, P>for custom messages - Set
namespace: 'mastra'when overriding defaults - Always include
validateMessagefunction - Register all renderers in
layout.tsx
Debugging
- Use console logs with prefixes:
[STATE SETTER],[FRONTEND TOOL] - Use Cedar debugger panel for response inspection
- Check browser console for Cedar errors
- Verify state keys match exactly between frontend/backend
- Test Cedar responses in isolation before full integration
Quick Reference
Frontend State Declaration
const [state, setState] = React.useState<Type>(initialValue); useRegisterState({ key: 'stateKey', description: 'Description', value: state, setValue: setState, stateSetters: { setterName: { name: 'setterName', description: 'Setter description', argsSchema: z.object({ ... }), execute: (currentState, setValue, args) => { console.log('[STATE SETTER] setterName called with:', args); setValue(args.newValue); }, }, }, });
Backend Cedar Response
return { type: 'setState' as const, stateKey: 'stateKey', // Match frontend setterKey: 'setterName', // Match frontend args: { // Arguments matching setter's argsSchema }, _raw: { // Original response data }, };
Message Renderer
import { createMessageRenderer, CustomMastraMessage } from 'cedar-os'; type MyMessage = CustomMastraMessage<'my-type'>; export const MyRenderer = createMessageRenderer<MyMessage>({ type: 'my-type', namespace: 'mastra', render: (message) => <div>{message.payload?.content}</div>, validateMessage: (msg): msg is MyMessage => { return msg.type === 'my-type' && 'payload' in msg; }, });
Additional Resources
- Cedar OS Docs: docs.cedarcopilot.com
- Mastra Docs: docs.mastra.ai
- Example Implementation: Agentic Web Coding Agent
This integration pattern eliminates boilerplate state synchronization between AI backends and frontend UIs. Backend tools declare their UI impact through structured responses, and Cedar handles the rest—no manual event buses, no scattered state logic, no agent orchestration overhead.