Building a Booking Bot with aiogram 3 FSM
Most useful Telegram bots aren’t a single /start command — they’re multi-step flows. A client picks a service, selects a date, enters their details, confirms the booking. This is exactly what FSM (Finite State Machine) is for in aiogram 3.
This guide walks through a complete appointment booking bot: states, inline keyboards, Redis storage, and admin notifications — production-ready.
What Is FSM and Why Use It
FSM tracks which step of a conversation the user is currently on. Without it, the bot has no memory of prior messages within a session.
In aiogram 3, FSM consists of:
State— a single conversation stepStatesGroup— groups related statesStateFilter— routes handlers based on current stateFSMContext— stores data across steps (service name, date, phone number)
Need a similar bot built for your business? Write on Telegram — I’ll review the requirements and suggest a solution.
Project Structure
booking_bot/
├── bot.py # dispatcher and startup
├── handlers/
│ ├── booking.py # FSM conversation handlers
│ └── admin.py # notifications and booking list
├── states.py # StatesGroup definitions
├── keyboards.py # inline and reply keyboards
└── storage.py # Redis or MemoryStorage
Defining States
# states.py
from aiogram.fsm.state import State, StatesGroup
class BookingForm(StatesGroup):
choose_service = State()
choose_date = State()
choose_time = State()
enter_name = State()
enter_phone = State()
confirm = State()
Each State() represents one step. aiogram handles the transitions.
Conversation Handlers
# handlers/booking.py
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from states import BookingForm
router = Router()
@router.message(F.text == "📅 Book Appointment")
async def start_booking(message: Message, state: FSMContext):
await state.set_state(BookingForm.choose_service)
await message.answer("Select a service:", reply_markup=services_kb())
@router.callback_query(StateFilter(BookingForm.choose_service))
async def process_service(callback: CallbackQuery, state: FSMContext):
await state.update_data(service=callback.data)
await state.set_state(BookingForm.choose_date)
await callback.message.edit_text("Pick a date:", reply_markup=dates_kb())
@router.message(StateFilter(BookingForm.enter_name))
async def process_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(BookingForm.enter_phone)
await message.answer("Enter your phone number:")
@router.message(StateFilter(BookingForm.enter_phone))
async def process_phone(message: Message, state: FSMContext):
await state.update_data(phone=message.text)
data = await state.get_data()
await state.set_state(BookingForm.confirm)
summary = (
f"✅ Please confirm:\n\n"
f"Service: {data['service']}\n"
f"Date: {data['date']}\n"
f"Time: {data['time']}\n"
f"Name: {data['name']}\n"
f"Phone: {data['phone']}"
)
await message.answer(summary, reply_markup=confirm_kb())
State Storage: Redis for Production
Always use Redis in production — it persists state across restarts:
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379")
dp = Dispatcher(storage=storage)
For local development, MemoryStorage() is fine. Just remember it clears on restart.
Admin Notifications
@router.callback_query(F.data == "confirm", StateFilter(BookingForm.confirm))
async def finish_booking(callback: CallbackQuery, state: FSMContext, bot: Bot):
data = await state.get_data()
await state.clear()
await callback.message.edit_text("🎉 Booking confirmed! We'll contact you shortly.")
await bot.send_message(
ADMIN_CHAT_ID,
f"📋 New booking!\n\n"
f"Service: {data['service']}\n"
f"Date: {data['date']} at {data['time']}\n"
f"Client: {data['name']} — {data['phone']}"
)
Cancellation Handling
Always give users a way out:
@router.message(F.text == "❌ Cancel")
async def cancel_booking(message: Message, state: FSMContext):
if await state.get_state():
await state.clear()
await message.answer("Booking cancelled.", reply_markup=main_menu_kb())
Need help implementing this? I build these professionally. Write on Telegram → or vic.kell@ya.ru
FAQ
Can I use MemoryStorage in production?
Not recommended — data is lost on any restart. Redis is the standard choice; even a free Redis Cloud instance works fine.
How many states can a StatesGroup have?
Unlimited in practice. Aim for 3–8 steps per flow for good UX.
How do I save FSM data to a database?
After state.get_data() you get a plain dict. Pass it to your ORM (SQLAlchemy, Tortoise ORM) or directly to asyncpg.
Can multiple users run the bot simultaneously?
Yes — FSM is scoped per user_id + chat_id, so every user has independent state.
How do I test FSM handlers?
Use pytest-asyncio with mock Message, CallbackQuery, and FSMContext objects. aiogram provides test utilities in aiogram.utils.test.