Building a Booking Bot with aiogram 3 FSM — KEL IT
Telegram Bots 4 min read

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 step
  • StatesGroup — groups related states
  • StateFilter — routes handlers based on current state
  • FSMContext — 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.

KEL IT

Need a custom solution?

I build these types of projects professionally. Telegram bots, Mini Apps, websites, mobile and desktop applications. Tell me about your project and I'll get back to you with a plan.