[{"data":1,"prerenderedAt":922},["ShallowReactive",2],{"blog-ru-aiogram-fsm-3-dialog-state":3,"blog-counterpart-ru-aiogram-fsm-3-dialog-state":920},{"id":4,"title":5,"body":6,"cluster":896,"ctaType":897,"date":898,"description":899,"extension":900,"intent":901,"meta":902,"navigation":118,"ogImage":903,"path":904,"primaryKeyword":905,"readTime":906,"relatedCases":907,"relatedCourse":909,"relatedService":910,"seo":911,"slug":912,"stem":913,"tags":914,"updated":920,"__hash__":921},"blogRu\u002Fru\u002Fblog\u002Faiogram-fsm-3-dialog-state.md","FSM в Aiogram 3: пошаговая форма, отмена и хранение",{"type":7,"value":8,"toc":888},"minimark",[9,22,25,30,40,49,79,91,95,102,149,152,156,625,640,734,739,746,750,768,782,786,836,840,868,884],[10,11,12,16,17,21],"p",{},[13,14,15],"strong",{},"FSM в Aiogram 3 нужен, когда боту надо провести человека через несколько связанных шагов и помнить, какой ответ ждать дальше."," Ниже соберём форму заявки: тема → описание → подтверждение. В примере есть проверка текста, команда ",[18,19,20],"code",{},"\u002Fcancel"," и очистка состояния после завершения.",[10,23,24],{},"FSM не заменяет базу данных. Он хранит временное состояние диалога; созданную заявку нужно отдельно сохранять через сервис и repository.",[26,27,29],"h2",{"id":28},"что-будет-в-итоге","Что будет в итоге",[31,32,38],"pre",{"className":33,"code":35,"language":36,"meta":37},[34],"language-text","\u002Frequest → тема → описание → подтверждение\n             ↘ \u002Fcancel на любом шаге\n","text","",[18,39,35],{"__ignoreMap":37},[10,41,42,43,48],{},"Создайте бота через BotFather, подготовьте Python 3.10+ и установите ту же версию, что используется в ",[44,45,47],"a",{"href":46},"\u002Fru\u002Fblog\u002Faiogram-3-tutorial-2025\u002F","туториале по Aiogram 3",":",[31,50,54],{"className":51,"code":52,"language":53,"meta":37,"style":37},"language-bash shiki shiki-themes github-light github-dark","python -m pip install \"aiogram==3.31.0\"\n","bash",[18,55,56],{"__ignoreMap":37},[57,58,61,65,69,73,76],"span",{"class":59,"line":60},"line",1,[57,62,64],{"class":63},"sScJk","python",[57,66,68],{"class":67},"sj4cs"," -m",[57,70,72],{"class":71},"sZZnC"," pip",[57,74,75],{"class":71}," install",[57,77,78],{"class":71}," \"aiogram==3.31.0\"\n",[10,80,81,82,85,86,90],{},"Перед запуском передайте токен через ",[18,83,84],{},"BOT_TOKEN"," в окружении, не вставляйте его в исходник. Для краткого вводного маршрута подойдёт ",[44,87,89],{"href":88},"\u002Fru\u002Fcourses\u002Faiogram-start\u002F","бесплатный практикум Aiogram","; здесь сосредоточимся именно на переходах между состояниями.",[26,92,94],{"id":93},"опишите-состояния-и-переходы","Опишите состояния и переходы",[10,96,97,98,101],{},"Состояние — не текст предыдущего сообщения, а явный этап сценария. Handler с фильтром ",[18,99,100],{},"RequestForm.topic"," сработает только тогда, когда FSM ждёт тему.",[31,103,106],{"className":104,"code":105,"language":64,"meta":37,"style":37},"language-python shiki shiki-themes github-light github-dark","from aiogram.fsm.state import State, StatesGroup\n\n\nclass RequestForm(StatesGroup):\n    topic = State()\n    details = State()\n    confirmation = State()\n",[18,107,108,113,120,125,131,137,143],{"__ignoreMap":37},[57,109,110],{"class":59,"line":60},[57,111,112],{},"from aiogram.fsm.state import State, StatesGroup\n",[57,114,116],{"class":59,"line":115},2,[57,117,119],{"emptyLinePlaceholder":118},true,"\n",[57,121,123],{"class":59,"line":122},3,[57,124,119],{"emptyLinePlaceholder":118},[57,126,128],{"class":59,"line":127},4,[57,129,130],{},"class RequestForm(StatesGroup):\n",[57,132,134],{"class":59,"line":133},5,[57,135,136],{},"    topic = State()\n",[57,138,140],{"class":59,"line":139},6,[57,141,142],{},"    details = State()\n",[57,144,146],{"class":59,"line":145},7,[57,147,148],{},"    confirmation = State()\n",[10,150,151],{},"У FSM нет автоматического перехода к следующему шагу: обработчик сам сохраняет ответ, устанавливает новое состояние и отправляет следующий вопрос.",[26,153,155],{"id":154},"реализуйте-форму-и-отмену","Реализуйте форму и отмену",[31,157,159],{"className":104,"code":158,"language":64,"meta":37,"style":37},"from aiogram import F, Router\nfrom aiogram.filters import Command\nfrom aiogram.fsm.context import FSMContext\nfrom aiogram.fsm.state import State, StatesGroup\nfrom aiogram.types import Message\n\n\nrouter = Router()\n\n\nclass RequestForm(StatesGroup):\n    topic = State()\n    details = State()\n    confirmation = State()\n\n\n@router.message(Command(\"cancel\"))\nasync def cancel_request(message: Message, state: FSMContext) -> None:\n    if await state.get_state() is None:\n        await message.answer(\"Сейчас нет активной формы.\")\n        return\n\n    await state.clear()\n    await message.answer(\"Форма отменена. Чтобы начать заново, отправьте \u002Frequest.\")\n\n\n@router.message(Command(\"request\"))\nasync def start_request(message: Message, state: FSMContext) -> None:\n    # Повторная команда начинает новую форму, а не продолжает старый черновик.\n    await state.clear()\n    await state.set_state(RequestForm.topic)\n    await message.answer(\"Коротко назовите тему обращения. Для отмены — \u002Fcancel.\")\n\n\n@router.message(RequestForm.topic, F.text)\nasync def receive_topic(message: Message, state: FSMContext) -> None:\n    topic = message.text.strip()\n    if not topic or len(topic) > 120:\n        await message.answer(\"Тема должна содержать от 1 до 120 символов. Попробуйте ещё раз.\")\n        return\n\n    await state.update_data(topic=topic)\n    await state.set_state(RequestForm.details)\n    await message.answer(\"Опишите задачу в нескольких предложениях. Для отмены — \u002Fcancel.\")\n\n\n@router.message(RequestForm.topic)\nasync def reject_non_text_topic(message: Message) -> None:\n    await message.answer(\"На этом шаге отправьте тему обычным текстом.\")\n\n\n@router.message(RequestForm.details, F.text)\nasync def receive_details(message: Message, state: FSMContext) -> None:\n    details = message.text.strip()\n    if not details or len(details) > 2000:\n        await message.answer(\"Описание должно содержать от 1 до 2000 символов. Попробуйте ещё раз.\")\n        return\n\n    data = await state.update_data(details=details)\n    await state.set_state(RequestForm.confirmation)\n    await message.answer(\n        f\"Проверьте заявку:\\nТема: {data['topic']}\\nОписание: {data['details']}\\n\\n\"\n        \"Отправьте \u002Fconfirm для подтверждения или \u002Fcancel для отмены.\"\n    )\n\n\n@router.message(RequestForm.confirmation, Command(\"confirm\"))\nasync def confirm_request(message: Message, state: FSMContext) -> None:\n    data = await state.get_data()\n\n    # В рабочем проекте передайте data сервису и сохраните заявку в БД здесь.\n    # Очищайте FSM только после успешного сохранения.\n    await message.answer(f\"Заявка «{data['topic']}» подтверждена.\")\n    await state.clear()\n\n\n@router.message(RequestForm.confirmation)\nasync def repeat_confirmation(message: Message) -> None:\n    await message.answer(\"Отправьте \u002Fconfirm для подтверждения или \u002Fcancel для отмены.\")\n\n\n@router.message(RequestForm.details)\nasync def reject_non_text_details(message: Message) -> None:\n    await message.answer(\"На этом шаге отправьте описание обычным текстом.\")\n",[18,160,161,166,171,176,180,185,189,193,199,204,209,214,219,224,229,234,239,245,251,257,263,269,274,280,286,291,296,302,308,314,319,325,331,336,341,347,353,359,365,371,376,381,387,393,399,404,409,415,421,427,432,437,443,449,455,461,467,472,477,483,489,495,501,507,513,518,523,529,535,541,546,552,558,564,569,574,579,585,591,597,602,607,613,619],{"__ignoreMap":37},[57,162,163],{"class":59,"line":60},[57,164,165],{},"from aiogram import F, Router\n",[57,167,168],{"class":59,"line":115},[57,169,170],{},"from aiogram.filters import Command\n",[57,172,173],{"class":59,"line":122},[57,174,175],{},"from aiogram.fsm.context import FSMContext\n",[57,177,178],{"class":59,"line":127},[57,179,112],{},[57,181,182],{"class":59,"line":133},[57,183,184],{},"from aiogram.types import Message\n",[57,186,187],{"class":59,"line":139},[57,188,119],{"emptyLinePlaceholder":118},[57,190,191],{"class":59,"line":145},[57,192,119],{"emptyLinePlaceholder":118},[57,194,196],{"class":59,"line":195},8,[57,197,198],{},"router = Router()\n",[57,200,202],{"class":59,"line":201},9,[57,203,119],{"emptyLinePlaceholder":118},[57,205,207],{"class":59,"line":206},10,[57,208,119],{"emptyLinePlaceholder":118},[57,210,212],{"class":59,"line":211},11,[57,213,130],{},[57,215,217],{"class":59,"line":216},12,[57,218,136],{},[57,220,222],{"class":59,"line":221},13,[57,223,142],{},[57,225,227],{"class":59,"line":226},14,[57,228,148],{},[57,230,232],{"class":59,"line":231},15,[57,233,119],{"emptyLinePlaceholder":118},[57,235,237],{"class":59,"line":236},16,[57,238,119],{"emptyLinePlaceholder":118},[57,240,242],{"class":59,"line":241},17,[57,243,244],{},"@router.message(Command(\"cancel\"))\n",[57,246,248],{"class":59,"line":247},18,[57,249,250],{},"async def cancel_request(message: Message, state: FSMContext) -> None:\n",[57,252,254],{"class":59,"line":253},19,[57,255,256],{},"    if await state.get_state() is None:\n",[57,258,260],{"class":59,"line":259},20,[57,261,262],{},"        await message.answer(\"Сейчас нет активной формы.\")\n",[57,264,266],{"class":59,"line":265},21,[57,267,268],{},"        return\n",[57,270,272],{"class":59,"line":271},22,[57,273,119],{"emptyLinePlaceholder":118},[57,275,277],{"class":59,"line":276},23,[57,278,279],{},"    await state.clear()\n",[57,281,283],{"class":59,"line":282},24,[57,284,285],{},"    await message.answer(\"Форма отменена. Чтобы начать заново, отправьте \u002Frequest.\")\n",[57,287,289],{"class":59,"line":288},25,[57,290,119],{"emptyLinePlaceholder":118},[57,292,294],{"class":59,"line":293},26,[57,295,119],{"emptyLinePlaceholder":118},[57,297,299],{"class":59,"line":298},27,[57,300,301],{},"@router.message(Command(\"request\"))\n",[57,303,305],{"class":59,"line":304},28,[57,306,307],{},"async def start_request(message: Message, state: FSMContext) -> None:\n",[57,309,311],{"class":59,"line":310},29,[57,312,313],{},"    # Повторная команда начинает новую форму, а не продолжает старый черновик.\n",[57,315,317],{"class":59,"line":316},30,[57,318,279],{},[57,320,322],{"class":59,"line":321},31,[57,323,324],{},"    await state.set_state(RequestForm.topic)\n",[57,326,328],{"class":59,"line":327},32,[57,329,330],{},"    await message.answer(\"Коротко назовите тему обращения. Для отмены — \u002Fcancel.\")\n",[57,332,334],{"class":59,"line":333},33,[57,335,119],{"emptyLinePlaceholder":118},[57,337,339],{"class":59,"line":338},34,[57,340,119],{"emptyLinePlaceholder":118},[57,342,344],{"class":59,"line":343},35,[57,345,346],{},"@router.message(RequestForm.topic, F.text)\n",[57,348,350],{"class":59,"line":349},36,[57,351,352],{},"async def receive_topic(message: Message, state: FSMContext) -> None:\n",[57,354,356],{"class":59,"line":355},37,[57,357,358],{},"    topic = message.text.strip()\n",[57,360,362],{"class":59,"line":361},38,[57,363,364],{},"    if not topic or len(topic) > 120:\n",[57,366,368],{"class":59,"line":367},39,[57,369,370],{},"        await message.answer(\"Тема должна содержать от 1 до 120 символов. Попробуйте ещё раз.\")\n",[57,372,374],{"class":59,"line":373},40,[57,375,268],{},[57,377,379],{"class":59,"line":378},41,[57,380,119],{"emptyLinePlaceholder":118},[57,382,384],{"class":59,"line":383},42,[57,385,386],{},"    await state.update_data(topic=topic)\n",[57,388,390],{"class":59,"line":389},43,[57,391,392],{},"    await state.set_state(RequestForm.details)\n",[57,394,396],{"class":59,"line":395},44,[57,397,398],{},"    await message.answer(\"Опишите задачу в нескольких предложениях. Для отмены — \u002Fcancel.\")\n",[57,400,402],{"class":59,"line":401},45,[57,403,119],{"emptyLinePlaceholder":118},[57,405,407],{"class":59,"line":406},46,[57,408,119],{"emptyLinePlaceholder":118},[57,410,412],{"class":59,"line":411},47,[57,413,414],{},"@router.message(RequestForm.topic)\n",[57,416,418],{"class":59,"line":417},48,[57,419,420],{},"async def reject_non_text_topic(message: Message) -> None:\n",[57,422,424],{"class":59,"line":423},49,[57,425,426],{},"    await message.answer(\"На этом шаге отправьте тему обычным текстом.\")\n",[57,428,430],{"class":59,"line":429},50,[57,431,119],{"emptyLinePlaceholder":118},[57,433,435],{"class":59,"line":434},51,[57,436,119],{"emptyLinePlaceholder":118},[57,438,440],{"class":59,"line":439},52,[57,441,442],{},"@router.message(RequestForm.details, F.text)\n",[57,444,446],{"class":59,"line":445},53,[57,447,448],{},"async def receive_details(message: Message, state: FSMContext) -> None:\n",[57,450,452],{"class":59,"line":451},54,[57,453,454],{},"    details = message.text.strip()\n",[57,456,458],{"class":59,"line":457},55,[57,459,460],{},"    if not details or len(details) > 2000:\n",[57,462,464],{"class":59,"line":463},56,[57,465,466],{},"        await message.answer(\"Описание должно содержать от 1 до 2000 символов. Попробуйте ещё раз.\")\n",[57,468,470],{"class":59,"line":469},57,[57,471,268],{},[57,473,475],{"class":59,"line":474},58,[57,476,119],{"emptyLinePlaceholder":118},[57,478,480],{"class":59,"line":479},59,[57,481,482],{},"    data = await state.update_data(details=details)\n",[57,484,486],{"class":59,"line":485},60,[57,487,488],{},"    await state.set_state(RequestForm.confirmation)\n",[57,490,492],{"class":59,"line":491},61,[57,493,494],{},"    await message.answer(\n",[57,496,498],{"class":59,"line":497},62,[57,499,500],{},"        f\"Проверьте заявку:\\nТема: {data['topic']}\\nОписание: {data['details']}\\n\\n\"\n",[57,502,504],{"class":59,"line":503},63,[57,505,506],{},"        \"Отправьте \u002Fconfirm для подтверждения или \u002Fcancel для отмены.\"\n",[57,508,510],{"class":59,"line":509},64,[57,511,512],{},"    )\n",[57,514,516],{"class":59,"line":515},65,[57,517,119],{"emptyLinePlaceholder":118},[57,519,521],{"class":59,"line":520},66,[57,522,119],{"emptyLinePlaceholder":118},[57,524,526],{"class":59,"line":525},67,[57,527,528],{},"@router.message(RequestForm.confirmation, Command(\"confirm\"))\n",[57,530,532],{"class":59,"line":531},68,[57,533,534],{},"async def confirm_request(message: Message, state: FSMContext) -> None:\n",[57,536,538],{"class":59,"line":537},69,[57,539,540],{},"    data = await state.get_data()\n",[57,542,544],{"class":59,"line":543},70,[57,545,119],{"emptyLinePlaceholder":118},[57,547,549],{"class":59,"line":548},71,[57,550,551],{},"    # В рабочем проекте передайте data сервису и сохраните заявку в БД здесь.\n",[57,553,555],{"class":59,"line":554},72,[57,556,557],{},"    # Очищайте FSM только после успешного сохранения.\n",[57,559,561],{"class":59,"line":560},73,[57,562,563],{},"    await message.answer(f\"Заявка «{data['topic']}» подтверждена.\")\n",[57,565,567],{"class":59,"line":566},74,[57,568,279],{},[57,570,572],{"class":59,"line":571},75,[57,573,119],{"emptyLinePlaceholder":118},[57,575,577],{"class":59,"line":576},76,[57,578,119],{"emptyLinePlaceholder":118},[57,580,582],{"class":59,"line":581},77,[57,583,584],{},"@router.message(RequestForm.confirmation)\n",[57,586,588],{"class":59,"line":587},78,[57,589,590],{},"async def repeat_confirmation(message: Message) -> None:\n",[57,592,594],{"class":59,"line":593},79,[57,595,596],{},"    await message.answer(\"Отправьте \u002Fconfirm для подтверждения или \u002Fcancel для отмены.\")\n",[57,598,600],{"class":59,"line":599},80,[57,601,119],{"emptyLinePlaceholder":118},[57,603,605],{"class":59,"line":604},81,[57,606,119],{"emptyLinePlaceholder":118},[57,608,610],{"class":59,"line":609},82,[57,611,612],{},"@router.message(RequestForm.details)\n",[57,614,616],{"class":59,"line":615},83,[57,617,618],{},"async def reject_non_text_details(message: Message) -> None:\n",[57,620,622],{"class":59,"line":621},84,[57,623,624],{},"    await message.answer(\"На этом шаге отправьте описание обычным текстом.\")\n",[10,626,627,628,631,632,635,636,639],{},"Подключите ",[18,629,630],{},"router"," к ",[18,633,634],{},"Dispatcher"," через ",[18,637,638],{},"dispatcher.include_router(router)",". Для локального запуска достаточно точки входа:",[31,641,643],{"className":104,"code":642,"language":64,"meta":37,"style":37},"import asyncio\nimport os\n\nfrom aiogram import Bot, Dispatcher\n\n\nasync def main() -> None:\n    token = os.getenv(\"BOT_TOKEN\")\n    if not token:\n        raise RuntimeError(\"Set BOT_TOKEN in the environment\")\n\n    bot = Bot(token=token)\n    dispatcher = Dispatcher()\n    dispatcher.include_router(router)\n    await dispatcher.start_polling(bot)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n",[18,644,645,650,655,659,664,668,672,677,682,687,692,696,701,706,711,716,720,724,729],{"__ignoreMap":37},[57,646,647],{"class":59,"line":60},[57,648,649],{},"import asyncio\n",[57,651,652],{"class":59,"line":115},[57,653,654],{},"import os\n",[57,656,657],{"class":59,"line":122},[57,658,119],{"emptyLinePlaceholder":118},[57,660,661],{"class":59,"line":127},[57,662,663],{},"from aiogram import Bot, Dispatcher\n",[57,665,666],{"class":59,"line":133},[57,667,119],{"emptyLinePlaceholder":118},[57,669,670],{"class":59,"line":139},[57,671,119],{"emptyLinePlaceholder":118},[57,673,674],{"class":59,"line":145},[57,675,676],{},"async def main() -> None:\n",[57,678,679],{"class":59,"line":195},[57,680,681],{},"    token = os.getenv(\"BOT_TOKEN\")\n",[57,683,684],{"class":59,"line":201},[57,685,686],{},"    if not token:\n",[57,688,689],{"class":59,"line":206},[57,690,691],{},"        raise RuntimeError(\"Set BOT_TOKEN in the environment\")\n",[57,693,694],{"class":59,"line":211},[57,695,119],{"emptyLinePlaceholder":118},[57,697,698],{"class":59,"line":216},[57,699,700],{},"    bot = Bot(token=token)\n",[57,702,703],{"class":59,"line":221},[57,704,705],{},"    dispatcher = Dispatcher()\n",[57,707,708],{"class":59,"line":226},[57,709,710],{},"    dispatcher.include_router(router)\n",[57,712,713],{"class":59,"line":231},[57,714,715],{},"    await dispatcher.start_polling(bot)\n",[57,717,718],{"class":59,"line":236},[57,719,119],{"emptyLinePlaceholder":118},[57,721,722],{"class":59,"line":241},[57,723,119],{"emptyLinePlaceholder":118},[57,725,726],{"class":59,"line":247},[57,727,728],{},"if __name__ == \"__main__\":\n",[57,730,731],{"class":59,"line":253},[57,732,733],{},"    asyncio.run(main())\n",[10,735,736,738],{},[18,737,20],{}," зарегистрирован раньше обработчиков состояний, поэтому команда не попадёт в обычный ввод описания. Общий обработчик «ответить на любое сообщение» лучше подключать после сценарных роутеров: иначе он может перехватить шаг формы.",[10,740,741],{},[742,743],"img",{"alt":744,"src":745},"Диаграмма переходов между состояниями FSM в Aiogram","\u002Fimages\u002Fcourses\u002Faiogram-start\u002Ffsm-flow.svg",[26,747,749],{"id":748},"где-хранится-ответ-между-шагами","Где хранится ответ между шагами",[10,751,752,755,756,759,760,763,764,767],{},[18,753,754],{},"state.update_data()"," сохраняет промежуточные поля, а ",[18,757,758],{},"state.get_data()"," читает их. В примере финальный ",[18,761,762],{},"update_data()"," возвращает объединённые данные, поэтому отдельное чтение не требуется. На предыдущем шаге можно вызывать ",[18,765,766],{},"get_data()",", если надо показать сводку или дать возможность исправить поле.",[10,769,770,771,774,775,781],{},"По умолчанию Aiogram использует ",[18,772,773],{},"MemoryStorage",": он удобен для локальной проверки, но хранит данные в памяти процесса. После перезапуска они исчезнут. Для сценария, который должен переживать рестарт, настройте внешнее хранилище, например Redis. TTL можно назначить отдельно для состояния и данных. FSM-хранилище всё равно не является надёжным хранилищем заказов или заявок. ",[44,776,780],{"href":777,"rel":778},"https:\u002F\u002Fdocs.aiogram.dev\u002Fen\u002Flatest\u002Fdispatcher\u002Ffinite_state_machine\u002Fstorages.html",[779],"nofollow","Документация Aiogram: FSM storage",".",[26,783,785],{"id":784},"частые-ошибки","Частые ошибки",[787,788,789,796,805,811,820,830],"ul",{},[790,791,792,795],"li",{},[13,793,794],{},"Забыли установить следующее состояние."," После ответа пользователь снова попадает в тот же этап или теряется в общем обработчике.",[790,797,798,801,802,781],{},[13,799,800],{},"Не очистили старый сценарий при повторном старте."," Начните новый процесс с ",[18,803,804],{},"await state.clear()",[790,806,807,810],{},[13,808,809],{},"Команда отмены обрабатывается как ответ формы."," Зарегистрируйте command handler раньше широких state handlers.",[790,812,813,819],{},[13,814,815,816,781],{},"В state data положили весь объект ",[18,817,818],{},"Message"," Храните только короткие сериализуемые значения, которые нужны следующему шагу.",[790,821,822,825,826,829],{},[13,823,824],{},"Заявка очищена до записи в БД."," Сначала сохраните запись; вызывайте ",[18,827,828],{},"state.clear()"," после успешного ответа repository.",[790,831,832,835],{},[13,833,834],{},"Использовали MemoryStorage в production и ждёте сохранения после рестарта."," Подключите внешнее хранилище, а бизнес-данные сохраняйте отдельно.",[26,837,839],{"id":838},"проверьте-сценарий","Проверьте сценарий",[841,842,843,850,853,856,859,865],"ol",{},[790,844,845,846,849],{},"Отправьте ",[18,847,848],{},"\u002Frequest"," и убедитесь, что бот ждёт тему.",[790,851,852],{},"Отправьте фото вместо текста: бот должен объяснить ожидаемый формат, не меняя шаг.",[790,854,855],{},"Отправьте тему длиннее 120 символов: бот должен попросить исправить её.",[790,857,858],{},"Отправьте корректную тему и описание; проверьте, что оба значения доступны финальному обработчику.",[790,860,861,862,864],{},"Начните новую форму и отправьте ",[18,863,20],{}," на каждом шаге.",[790,866,867],{},"Перезапустите polling: проверьте, что помнит выбранное вами хранилище, и не рассчитывайте на сохранность MemoryStorage.",[10,869,870,871,875,876,879,880,781],{},"Дальше можно разобрать ",[44,872,874],{"href":873},"\u002Fru\u002Fblog\u002Fproduction-struktura-aiogram\u002F","структуру production-проекта на Aiogram 3"," и полный путь от первого бота до FSM в ",[44,877,878],{"href":88},"практикуме с упражнениями",". Для продуктового бота на реальных заявках FSM должен вызывать сервис, который валидирует бизнес-правила и сохраняет результат в БД; пример такого подхода есть в ",[44,881,883],{"href":882},"\u002Fru\u002Fportfolio\u002Ffazer-cards-bot\u002F","кейсе Telegram-проекта",[885,886,887],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":37,"searchDepth":115,"depth":115,"links":889},[890,891,892,893,894,895],{"id":28,"depth":115,"text":29},{"id":93,"depth":115,"text":94},{"id":154,"depth":115,"text":155},{"id":748,"depth":115,"text":749},{"id":784,"depth":115,"text":785},{"id":838,"depth":115,"text":839},"telegram-bots","course","2026-10-12","Как собрать FSM-сценарий в Aiogram 3: StatesGroup, проверка ввода, \u002Fcancel, данные между шагами и выбор MemoryStorage или Redis.","md","educational",{},"\u002Fimages\u002Fog\u002Fblog\u002Fru\u002Faiogram-fsm-3-dialog-state.png","\u002Fru\u002Fblog\u002Faiogram-fsm-3-dialog-state","FSM Aiogram 3","9 мин",[908],"fazer-cards-bot","aiogram-start","bots",{"title":5,"description":899},"aiogram-fsm-3-dialog-state","ru\u002Fblog\u002Faiogram-fsm-3-dialog-state",[915,916,917,918,919],"Aiogram 3","FSM","Python","Telegram-бот","Практика",null,"HSpfnPNnj_95AC_d1QdnXYLojqfeqUrsQ-SwiF48kQ0",1791764624386]