state.storage-patterns-ts
purpose
State management with IStorage interface, LocalStorage, and per-user/per-conversation state patterns in Teams bots.
rules
- Use the
IStorageinterface (get/set/delete) for all state management. It supports both synchronous and async implementations, so custom backends (Redis, Cosmos DB) can return Promises. github.com/microsoft/teams.ts LocalStoragefrom@microsoft/teams.commonis an in-memory store with optional LRU eviction. Pass{ max: N }to cap entries and prevent unbounded memory growth in long-running bots. github.com/microsoft/teams.ts- Pass storage to the
Appconstructor via thestorageoption. The storage instance is then available asctx.storagein all route handlers. github.com/microsoft/teams.ts - For per-user state, key on
activity.from.id(oractivity.from.aadObjectIdfor AAD-stable IDs). For per-conversation state, key onactivity.conversation.id. Choose the right scope for your data. learn.microsoft.com -- Bot state - When combining state with
ChatPrompt, pass the user's storedmessagesarray to the prompt'smessagesoption. This restores conversation history across handler invocations. github.com/microsoft/teams.ts - Initialize state lazily: check if state exists for the key, and if not, create a default state object and store it. This avoids null reference errors on first interaction. github.com/microsoft/teams.ts
LocalStorageis volatile -- data is lost on process restart. For production bots, implementIStoragebacked by a persistent store (Azure Cosmos DB, Redis, SQL). learn.microsoft.com -- Bot state management- Keep state objects small. Store only what is needed (message history, preferences, session flags). Large state objects increase memory pressure and serialization cost for persistent backends. github.com/microsoft/teams.ts
- The
IStoragegeneric signature isIStorage<TKey, TValue>. Type both the key and value for compile-time safety. For example,new LocalStorage<IUserState>()types the value while using string keys. github.com/microsoft/teams.ts - Do not rely on in-memory state in multi-instance deployments (e.g., Azure App Service with multiple instances or containers). Each instance has its own
LocalStorage. Use a shared persistent store instead. learn.microsoft.com -- Scale out
patterns
Per-user state with ChatPrompt message history
import { App } from '@microsoft/teams.apps';
import { ChatPrompt, LocalMemory, Message } from '@microsoft/teams.ai';
import { OpenAIChatModel } from '@microsoft/teams.openai';
import { LocalStorage, ConsoleLogger } from '@microsoft/teams.common';
import { DevtoolsPlugin } from '@microsoft/teams.dev';
const model = new OpenAIChatModel({
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4o',
});
interface IUserState {
messages: Message[];
preferences: Record<string, any>;
}
const userStore = new LocalStorage<IUserState>({}, {
max: 1000, // LRU eviction after 1000 users
});
const app = new App({
logger: new ConsoleLogger('state-bot', { level: 'debug' }),
plugins: [new DevtoolsPlugin()],
});
app.on('message', async ({ activity, send }) => {
const userId = activity.from.id;
// Lazy initialization: create state if it does not exist
let state = userStore.get(userId);
if (!state) {
state = { messages: [], preferences: {} };
userStore.set(userId, state);
}
const prompt = new ChatPrompt({
model,
instructions: 'You are a helpful assistant.',
messages: state.messages, // Restore conversation history
});
const result = await prompt.send(activity.text);
if (result.content) {
await send(result.content);
}
});
app.start(3978);Per-conversation state with App storage
import { App } from '@microsoft/teams.apps';
import { ChatPrompt, Message } from '@microsoft/teams.ai';
import { OpenAIChatModel } from '@microsoft/teams.openai';
import { LocalStorage, ConsoleLogger } from '@microsoft/teams.common';
import { DevtoolsPlugin } from '@microsoft/teams.dev';
const model = new OpenAIChatModel({
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4o',
});
interface IConversationState {
messages: Message[];
topicCount: number;
}
// Pass storage to App -- available as ctx.storage in handlers
const storage = new LocalStorage<IConversationState>({}, { max: 500 });
const app = new App({
storage,
logger: new ConsoleLogger('conv-bot'),
plugins: [new DevtoolsPlugin()],
});
app.on('message', async ({ activity, send, storage }) => {
const convId = activity.conversation.id;
let state = storage.get(convId) as IConversationState | undefined;
if (!state) {
state = { messages: [], topicCount: 0 };
storage.set(convId, state);
}
const prompt = new ChatPrompt({
model,
instructions: 'You are a helpful assistant. Be concise.',
messages: state.messages,
});
const result = await prompt.send(activity.text);
if (result.content) {
state.topicCount++;
await send(result.content);
}
});
app.start(3978);IStorage interface for custom backends
import { IStorage } from '@microsoft/teams.common';
// Example: custom Redis-backed storage implementing IStorage
class RedisStorage<T> implements IStorage<string, T> {
private client: any; // Your Redis client
constructor(redisClient: any) {
this.client = redisClient;
}
async get(key: string): Promise<T | undefined> {
const raw = await this.client.get(`bot:state:${key}`);
return raw ? JSON.parse(raw) : undefined;
}
async set(key: string, value: T): Promise<void> {
await this.client.set(`bot:state:${key}`, JSON.stringify(value));
}
async delete(key: string): Promise<void> {
await this.client.del(`bot:state:${key}`);
}
}
// Usage with App
// const storage = new RedisStorage<IConversationState>(redisClient);
// const app = new App({ storage });pitfalls
- Unbounded
LocalStorage: Not settingmaxonLocalStorageallows the store to grow without limit, eventually exhausting process memory. Always specify a max entry count. - Data loss on restart:
LocalStorageis in-memory only. Restarting the process loses all state. Use a persistent backend (Cosmos DB, Redis) for production bots. - Wrong state key scope: Using
activity.from.idwhen you want conversation-scoped state (or vice versa) causes data to bleed across contexts. Useactivity.conversation.idfor conversation state andactivity.from.idfor user state. - Mutating state without re-setting: If your
IStoragebackend uses serialization (e.g., Redis), mutating the returned object does not persist changes. Callstorage.set(key, state)after modifications. - Large message history: Passing the entire message history to
ChatPromptwithout amaxlimit onLocalMemorycan exceed token limits. UseLocalMemorywithmaxandcollapsefor automatic summarization. - Multi-instance deployments with
LocalStorage: Each process instance has its own in-memory store. In scaled deployments, a user may hit different instances, seeing inconsistent state. - Missing null check on
get():storage.get(key)returnsundefinedif the key does not exist. Always check forundefinedand initialize before accessing properties.
references
- Teams AI Library v2 -- GitHub
- Bot state management concepts
- Azure Cosmos DB for state storage
- Teams bot conversation context
instructions
This expert covers state management and storage patterns for Teams bots built with the Teams AI Library v2 (@microsoft/teams.ts) in TypeScript. Use it when you need to:
- Understand the
IStorageinterface (get/set/delete) and implement custom backends - Configure
LocalStoragewith LRU eviction limits - Pass storage to the
Appconstructor and access it in handlers viactx.storage - Implement per-user state keyed on
activity.from.id - Implement per-conversation state keyed on
activity.conversation.id - Combine stored message history with
ChatPromptfor multi-turn conversations
Pair with ai.memory-localmemory-ts.md for LocalMemory (AI message memory with summarization) and auth.oauth-sso-ts.md for authenticated state patterns. Pair with ai.memory-localmemory-ts.md for combining state with AI conversation history, and runtime.app-init-ts.md for passing storage to the App constructor.
research
Deep Research prompt:
"Write a micro expert on state and storage patterns for Teams SDK v2 bots (TypeScript). Cover IStorage interface, LocalStorage with LRU eviction, per-user and per-conversation state, combining state with ChatPrompt messages, implementing custom persistent backends (Redis, Cosmos DB), and warnings about multi-instance deployments. Include 2-3 TypeScript code examples."