0
# State Management
1
2
Built-in Finite State Machine (FSM) support for complex conversational flows with multiple storage backends, state groups, and context management. Enables creating multi-step interactions, forms, and complex bot workflows.
3
4
## Capabilities
5
6
### Core FSM Classes
7
8
Foundation classes for state management and context handling.
9
10
```python { .api }
11
class State:
12
"""Represents a single state in the FSM"""
13
14
def __init__(self, state: str, group_name: str | None = None):
15
"""
16
Initialize a state.
17
18
Parameters:
19
- state: Unique state identifier
20
- group_name: Optional group name (auto-detected from StatesGroup)
21
"""
22
23
@property
24
def state(self) -> str:
25
"""Get the state identifier"""
26
27
@property
28
def group(self) -> str:
29
"""Get the state group name"""
30
31
class StatesGroup:
32
"""Base class for grouping related states"""
33
34
def __class_getitem__(cls, item: str) -> State:
35
"""Get state by name"""
36
37
class FSMContext:
38
"""Context for managing FSM state and data"""
39
40
async def set_state(self, state: State | str | None = None) -> None:
41
"""
42
Set the current state.
43
44
Parameters:
45
- state: State to set (None clears state)
46
"""
47
48
async def get_state(self) -> str | None:
49
"""Get the current state"""
50
51
async def clear(self) -> None:
52
"""Clear state and all associated data"""
53
54
async def set_data(self, data: dict[str, Any]) -> None:
55
"""
56
Set context data.
57
58
Parameters:
59
- data: Data dictionary to store
60
"""
61
62
async def get_data(self) -> dict[str, Any]:
63
"""Get all context data"""
64
65
async def update_data(self, **kwargs: Any) -> None:
66
"""
67
Update context data with new values.
68
69
Parameters:
70
- kwargs: Key-value pairs to update
71
"""
72
73
@property
74
def key(self) -> StorageKey:
75
"""Get the storage key for this context"""
76
```
77
78
### Storage Backends
79
80
Different storage implementations for state persistence.
81
82
```python { .api }
83
class BaseStorage:
84
"""Abstract base class for FSM storage"""
85
86
async def set_state(self, key: StorageKey, state: str | None = None) -> None:
87
"""Set state for the given key"""
88
89
async def get_state(self, key: StorageKey) -> str | None:
90
"""Get state for the given key"""
91
92
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
93
"""Set data for the given key"""
94
95
async def get_data(self, key: StorageKey) -> dict[str, Any]:
96
"""Get data for the given key"""
97
98
async def close(self) -> None:
99
"""Close the storage connection"""
100
101
class MemoryStorage(BaseStorage):
102
"""In-memory storage (default, not persistent)"""
103
104
def __init__(self):
105
"""Initialize memory storage"""
106
107
class RedisStorage(BaseStorage):
108
"""Redis-based storage for persistence across restarts"""
109
110
def __init__(
111
self,
112
redis: Redis,
113
key_builder: KeyBuilder | None = None,
114
state_ttl: int | None = None,
115
data_ttl: int | None = None
116
):
117
"""
118
Initialize Redis storage.
119
120
Parameters:
121
- redis: Redis connection instance
122
- key_builder: Custom key builder for Redis keys
123
- state_ttl: TTL for state keys (seconds)
124
- data_ttl: TTL for data keys (seconds)
125
"""
126
127
class StorageKey:
128
"""Key for identifying user/chat in storage"""
129
130
def __init__(
131
self,
132
bot_id: int,
133
chat_id: int,
134
user_id: int
135
):
136
"""
137
Initialize storage key.
138
139
Parameters:
140
- bot_id: Bot identifier
141
- chat_id: Chat identifier
142
- user_id: User identifier
143
"""
144
145
@property
146
def bot_id(self) -> int:
147
"""Bot ID"""
148
149
@property
150
def chat_id(self) -> int:
151
"""Chat ID"""
152
153
@property
154
def user_id(self) -> int:
155
"""User ID"""
156
```
157
158
### FSM Strategies
159
160
Different strategies for FSM context isolation.
161
162
```python { .api }
163
class FSMStrategy(str, Enum):
164
"""FSM isolation strategies"""
165
CHAT = "CHAT" # One context per chat
166
USER_IN_CHAT = "USER_IN_CHAT" # One context per user in each chat
167
GLOBAL_USER = "GLOBAL_USER" # One context per user globally
168
```
169
170
## Usage Examples
171
172
### Basic State Machine
173
174
```python
175
from aiogram import Router, F
176
from aiogram.types import Message
177
from aiogram.filters import Command, StateFilter
178
from aiogram.fsm.context import FSMContext
179
from aiogram.fsm.state import State, StatesGroup
180
181
router = Router()
182
183
# Define states
184
class Form(StatesGroup):
185
name = State()
186
age = State()
187
email = State()
188
189
# Start the form
190
@router.message(Command("form"))
191
async def start_form(message: Message, state: FSMContext):
192
await state.set_state(Form.name)
193
await message.answer("What's your name?")
194
195
# Handle name input
196
@router.message(StateFilter(Form.name), F.text)
197
async def process_name(message: Message, state: FSMContext):
198
# Save the name
199
await state.update_data(name=message.text)
200
201
# Move to next state
202
await state.set_state(Form.age)
203
await message.answer("What's your age?")
204
205
# Handle age input with validation
206
@router.message(StateFilter(Form.age), F.text.regexp(r"^\d+$"))
207
async def process_age(message: Message, state: FSMContext):
208
age = int(message.text)
209
210
if 13 <= age <= 120:
211
await state.update_data(age=age)
212
await state.set_state(Form.email)
213
await message.answer("What's your email?")
214
else:
215
await message.answer("Please enter a valid age (13-120)")
216
217
# Handle invalid age
218
@router.message(StateFilter(Form.age))
219
async def invalid_age(message: Message):
220
await message.answer("Please enter your age as a number")
221
222
# Handle email input
223
@router.message(StateFilter(Form.email), F.text.regexp(r".+@.+\..+"))
224
async def process_email(message: Message, state: FSMContext):
225
await state.update_data(email=message.text)
226
227
# Get all collected data
228
data = await state.get_data()
229
230
# Clear the state
231
await state.clear()
232
233
# Show summary
234
await message.answer(
235
f"Form completed!\n\n"
236
f"Name: {data['name']}\n"
237
f"Age: {data['age']}\n"
238
f"Email: {data['email']}"
239
)
240
241
# Handle invalid email
242
@router.message(StateFilter(Form.email))
243
async def invalid_email(message: Message):
244
await message.answer("Please enter a valid email address")
245
246
# Cancel command (works in any state)
247
@router.message(Command("cancel"), StateFilter("*"))
248
async def cancel_form(message: Message, state: FSMContext):
249
current_state = await state.get_state()
250
if current_state is None:
251
await message.answer("Nothing to cancel")
252
return
253
254
await state.clear()
255
await message.answer("Form cancelled")
256
```
257
258
### Advanced State Machine with Branching
259
260
```python
261
class Survey(StatesGroup):
262
# Initial questions
263
name = State()
264
age = State()
265
266
# Branching based on age
267
minor_guardian = State() # For users under 18
268
adult_occupation = State() # For adults
269
270
# Common final states
271
feedback = State()
272
confirmation = State()
273
274
@router.message(Command("survey"))
275
async def start_survey(message: Message, state: FSMContext):
276
await state.set_state(Survey.name)
277
await message.answer("Welcome to our survey! What's your name?")
278
279
@router.message(StateFilter(Survey.name), F.text)
280
async def process_survey_name(message: Message, state: FSMContext):
281
await state.update_data(name=message.text)
282
await state.set_state(Survey.age)
283
await message.answer("What's your age?")
284
285
@router.message(StateFilter(Survey.age), F.text.regexp(r"^\d+$"))
286
async def process_survey_age(message: Message, state: FSMContext):
287
age = int(message.text)
288
await state.update_data(age=age)
289
290
# Branch based on age
291
if age < 18:
292
await state.set_state(Survey.minor_guardian)
293
await message.answer("Since you're under 18, we need your guardian's name:")
294
else:
295
await state.set_state(Survey.adult_occupation)
296
await message.answer("What's your occupation?")
297
298
@router.message(StateFilter(Survey.minor_guardian), F.text)
299
async def process_guardian(message: Message, state: FSMContext):
300
await state.update_data(guardian=message.text)
301
await state.set_state(Survey.feedback)
302
await message.answer("Any feedback about our service?")
303
304
@router.message(StateFilter(Survey.adult_occupation), F.text)
305
async def process_occupation(message: Message, state: FSMContext):
306
await state.update_data(occupation=message.text)
307
await state.set_state(Survey.feedback)
308
await message.answer("Any feedback about our service?")
309
310
@router.message(StateFilter(Survey.feedback), F.text)
311
async def process_feedback(message: Message, state: FSMContext):
312
await state.update_data(feedback=message.text)
313
314
# Show summary based on collected data
315
data = await state.get_data()
316
summary = f"Survey Summary:\nName: {data['name']}\nAge: {data['age']}\n"
317
318
if 'guardian' in data:
319
summary += f"Guardian: {data['guardian']}\n"
320
if 'occupation' in data:
321
summary += f"Occupation: {data['occupation']}\n"
322
323
summary += f"Feedback: {data['feedback']}\n\nIs this correct? (yes/no)"
324
325
await state.set_state(Survey.confirmation)
326
await message.answer(summary)
327
328
@router.message(StateFilter(Survey.confirmation), F.text.lower().in_(["yes", "y"]))
329
async def confirm_survey(message: Message, state: FSMContext):
330
data = await state.get_data()
331
await state.clear()
332
333
# Here you would typically save the data
334
await message.answer("Thank you! Your survey has been submitted.")
335
336
@router.message(StateFilter(Survey.confirmation), F.text.lower().in_(["no", "n"]))
337
async def reject_survey(message: Message, state: FSMContext):
338
await state.clear()
339
await message.answer("Survey cancelled. You can start over with /survey")
340
341
@router.message(StateFilter(Survey.confirmation))
342
async def invalid_confirmation(message: Message):
343
await message.answer("Please answer 'yes' or 'no'")
344
```
345
346
### State Machine with Inline Keyboards
347
348
```python
349
from aiogram.types import CallbackQuery
350
from aiogram.utils.keyboard import InlineKeyboardBuilder
351
352
class Order(StatesGroup):
353
category = State()
354
item = State()
355
quantity = State()
356
confirmation = State()
357
358
# Product data
359
CATEGORIES = {
360
"food": ["Pizza", "Burger", "Salad"],
361
"drinks": ["Coffee", "Tea", "Juice"],
362
"desserts": ["Cake", "Ice Cream", "Cookies"]
363
}
364
365
@router.message(Command("order"))
366
async def start_order(message: Message, state: FSMContext):
367
await state.set_state(Order.category)
368
369
builder = InlineKeyboardBuilder()
370
for category in CATEGORIES.keys():
371
builder.button(text=category.title(), callback_data=f"cat_{category}")
372
builder.adjust(2)
373
374
await message.answer("Choose a category:", reply_markup=builder.as_markup())
375
376
@router.callback_query(StateFilter(Order.category), F.data.startswith("cat_"))
377
async def process_category(callback: CallbackQuery, state: FSMContext):
378
category = callback.data.split("_")[1]
379
await state.update_data(category=category)
380
await state.set_state(Order.item)
381
382
builder = InlineKeyboardBuilder()
383
for item in CATEGORIES[category]:
384
builder.button(text=item, callback_data=f"item_{item.lower().replace(' ', '_')}")
385
builder.adjust(1)
386
387
await callback.message.edit_text(
388
f"Choose an item from {category}:",
389
reply_markup=builder.as_markup()
390
)
391
392
@router.callback_query(StateFilter(Order.item), F.data.startswith("item_"))
393
async def process_item(callback: CallbackQuery, state: FSMContext):
394
item = callback.data.split("_")[1].replace("_", " ").title()
395
await state.update_data(item=item)
396
await state.set_state(Order.quantity)
397
398
builder = InlineKeyboardBuilder()
399
for i in range(1, 6):
400
builder.button(text=str(i), callback_data=f"qty_{i}")
401
builder.adjust(5)
402
403
await callback.message.edit_text(
404
f"How many {item} would you like?",
405
reply_markup=builder.as_markup()
406
)
407
408
@router.callback_query(StateFilter(Order.quantity), F.data.startswith("qty_"))
409
async def process_quantity(callback: CallbackQuery, state: FSMContext):
410
quantity = int(callback.data.split("_")[1])
411
await state.update_data(quantity=quantity)
412
413
data = await state.get_data()
414
415
builder = InlineKeyboardBuilder()
416
builder.button(text="✅ Confirm", callback_data="confirm_order")
417
builder.button(text="❌ Cancel", callback_data="cancel_order")
418
builder.adjust(1)
419
420
await callback.message.edit_text(
421
f"Order Summary:\n"
422
f"Category: {data['category'].title()}\n"
423
f"Item: {data['item']}\n"
424
f"Quantity: {quantity}\n\n"
425
f"Confirm your order?",
426
reply_markup=builder.as_markup()
427
)
428
await state.set_state(Order.confirmation)
429
430
@router.callback_query(StateFilter(Order.confirmation), F.data == "confirm_order")
431
async def confirm_order(callback: CallbackQuery, state: FSMContext):
432
data = await state.get_data()
433
await state.clear()
434
435
await callback.message.edit_text(
436
f"✅ Order confirmed!\n\n"
437
f"You ordered {data['quantity']} {data['item']} from {data['category']}.\n"
438
f"Your order is being prepared."
439
)
440
441
@router.callback_query(StateFilter(Order.confirmation), F.data == "cancel_order")
442
async def cancel_order(callback: CallbackQuery, state: FSMContext):
443
await state.clear()
444
await callback.message.edit_text("❌ Order cancelled.")
445
```
446
447
### Custom Storage Configuration
448
449
```python
450
import redis.asyncio as redis
451
from aiogram.fsm.storage.redis import RedisStorage
452
453
# Configure Redis storage
454
redis_client = redis.Redis(host='localhost', port=6379, db=0)
455
storage = RedisStorage(redis_client, state_ttl=3600, data_ttl=3600)
456
457
# Create dispatcher with custom storage
458
dp = Dispatcher(storage=storage)
459
460
# Custom storage with different FSM strategy
461
dp = Dispatcher(
462
storage=storage,
463
fsm_strategy=FSMStrategy.GLOBAL_USER # One context per user globally
464
)
465
```
466
467
### State Machine with File Upload
468
469
```python
470
class FileUpload(StatesGroup):
471
waiting_file = State()
472
waiting_description = State()
473
474
@router.message(Command("upload"))
475
async def start_upload(message: Message, state: FSMContext):
476
await state.set_state(FileUpload.waiting_file)
477
await message.answer("Please send a file (photo, document, or video)")
478
479
@router.message(StateFilter(FileUpload.waiting_file), F.content_type.in_({"photo", "document", "video"}))
480
async def process_file(message: Message, state: FSMContext):
481
# Store file information
482
if message.photo:
483
file_id = message.photo[-1].file_id
484
file_type = "photo"
485
elif message.document:
486
file_id = message.document.file_id
487
file_type = "document"
488
elif message.video:
489
file_id = message.video.file_id
490
file_type = "video"
491
492
await state.update_data(file_id=file_id, file_type=file_type)
493
await state.set_state(FileUpload.waiting_description)
494
await message.answer("File received! Please provide a description:")
495
496
@router.message(StateFilter(FileUpload.waiting_description), F.text)
497
async def process_description(message: Message, state: FSMContext):
498
await state.update_data(description=message.text)
499
data = await state.get_data()
500
await state.clear()
501
502
# Here you would save the file and description
503
await message.answer(
504
f"Upload complete!\n"
505
f"File type: {data['file_type']}\n"
506
f"Description: {data['description']}\n"
507
f"File ID: {data['file_id']}"
508
)
509
510
@router.message(StateFilter(FileUpload.waiting_file))
511
async def invalid_file(message: Message):
512
await message.answer("Please send a photo, document, or video file")
513
```
514
515
## Types
516
517
### Storage Types
518
519
```python { .api }
520
class KeyBuilder:
521
"""Builder for Redis storage keys"""
522
523
def build(self, key: StorageKey, part: str) -> str:
524
"""Build a Redis key for the given storage key and part"""
525
526
class BaseEventIsolation:
527
"""Base class for event isolation mechanisms"""
528
pass
529
```