or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

api-methods.mdbot-and-dispatcher.mdfilters-and-handlers.mdindex.mdstate-management.mdtypes-and-objects.mdutilities-and-enums.md

state-management.mddocs/

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

```