State Management
Built-in state management is a Teams SDK 2.1 feature.
The Teams SDK provides built-in, per-turn state for storing conversation and user data across activities. State is loaded before each activity handler runs, saved automatically after the turn, and exposed through ctx.state.
Setup
State is disabled by default. Enable it with the state=True app option:
from microsoft_teams.apps import App
app = App(state=True)
Without a dedicated provider, state uses the Teams SDK's process-local, in-memory LocalStorage implementation. This is useful for local development, but values are lost when the process restarts and aren't shared across instances.
Registering an OAuth flow with add_oauth_flow() automatically enables state so pending sign-ins can be associated with the correct flow. Set state=False explicitly to fall back to process-local in-memory maps.
Reading and writing state
Use ctx.state.conversation and ctx.state.user in an activity handler. Values must be JSON-serializable.
from microsoft_teams.api import MessageActivity
from microsoft_teams.apps import ActivityContext
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]) -> None:
assert ctx.state is not None
count = ctx.state.conversation.get("message_count", 0) + 1
ctx.state.conversation["message_count"] = count
if ctx.state.user is not None and ctx.activity.text.startswith("my name is "):
ctx.state.user["name"] = ctx.activity.text[len("my name is "):].strip()
name = ctx.state.user.get("name", "there") if ctx.state.user is not None else "there"
await ctx.send(f"Hello, {name}. Message #{count}.")
Use key in scope to check for a value, del scope[key] to remove one, and clear() to remove all values. Changes inside stored lists and dictionaries are detected automatically when the turn is saved.
Conversation state is shared by everyone in the current conversation. User state is scoped to the current sender within that conversation and is unavailable when an activity has no usable sender ID.
The SDK writes a scope to storage only when it changes during the current activity. After all handlers for that activity finish, the SDK saves those changes and closes the turn state. Read or update state only while handling the activity. Don't capture it for timers or background tasks because accessing it after the turn ends throws TurnStateSealedError.
Clearing state
Remove a value with del scope[key], or clear a scope with clear(). To remove both scopes from the backing store:
await ctx.state.delete()
Values written after delete() are saved normally at the end of the current turn.
Scaling to distributed state
For production or multi-instance deployments, implement the Teams SDK's Storage[str, Any] contract with a shared, durable backend and pass it through StateOptions. The state API used by handlers doesn't change:
from typing import Any
from microsoft_teams.apps import App, StateOptions
from microsoft_teams.common import Storage
def create_app(durable_storage: Storage[str, Any]) -> App:
return App(
state=StateOptions(
storage=durable_storage,
key_prefix="my-app",
)
)
The SDK serializes each scope as a JSON string and replaces the complete scope on save, so concurrent turns use last-writer-wins semantics. Configure expiry, retries, and other storage-specific behavior on your storage implementation.