aiogram-dialog
Overview
Concept
Aiogram-dialog is a GUI framework for telegram bot. It is inspired by ideas of Android SDK and React.js
Main ideas are:
Split data retrieving and message rendering
Unite rendering buttons and processing clicks
Better states routing
Widgets
The main building block of your UI is Window. Each window represents a message sent to user and processing of a user reaction on it.
Each window consists of Widgets and callback functions. Widgets can represent message text and keyboard. Callbacks are used to retrieve required data or process user input.
You combine windows into Dialog. This allows you to switch between windows creating different scenarios of communication with user.
In more complex cases you can create more than one dialog. Then you can start new dialogs without closing previous one and automatically return back when it is closed. You can pass data between dialogs keeping they state isolated at the same time.
Quickstart
Install library:
pip install aiogram_dialog
Create states group for your dialog:
from aiogram.filters.state import StatesGroup, State
class MySG(StatesGroup):
main = State()
Create at least one window with buttons or text:
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Window
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const
class MySG(StatesGroup):
main = State()
main_window = Window(
Const("Hello, unknown person"), # just a constant text
Button(Const("Useless button"), id="nothing"), # button with text and id
state=MySG.main, # state is used to identify window between dialogs
)
Create dialog with your windows:
from aiogram_dialog import Dialog
dialog = Dialog(main_window)
Let’s assume that you have created your aiogram bot with dispatcher and states storage as you normally do.
It is important you have a storage because aiogram_dialog uses FSMContext
internally to store it state:
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
storage = MemoryStorage()
bot = Bot(token='BOT TOKEN HERE')
dp = Dispatcher(storage=storage)
To start using your dialog you need to register it. Also library needs some additional registrations for its internals. To do it we will create DialogRegistry and use it to register our dialog
from aiogram_dialog import DialogRegistry
registry = DialogRegistry(dp) # this is required to use `aiogram_dialog`
registry.register(dialog) # register a dialog
At this point we have configured everything. But dialog won’t start itself. We will create simple command handler to deal with it.
To start dialog we need DialogManager which is automatically injected by library. Also mind the reset_stack
argument. The library can start multiple dialogs stacking one above other. Currently we do not want this feature, so we will reset stack on each start:
from aiogram.types import Message
from aiogram_dialog import DialogManager, StartMode
@dp.message(commands=["start"])
async def start(message: Message, dialog_manager: DialogManager):
# Important: always set `mode=StartMode.RESET_STACK` you don't want to stack dialogs
await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)
Last step, you need to start your bot as usual:
if __name__ == '__main__':
dp.run_polling(bot, skip_updates=True)
Summary:
from aiogram import Bot, Dispatcher
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message
from aiogram_dialog import (
Dialog, DialogManager, DialogRegistry, StartMode, Window,
)
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const
storage = MemoryStorage()
bot = Bot(token='BOT TOKEN HERE')
dp = Dispatcher(storage=storage)
registry = DialogRegistry(dp)
class MySG(StatesGroup):
main = State()
main_window = Window(
Const("Hello, unknown person"),
Button(Const("Useless button"), id="nothing"),
state=MySG.main,
)
dialog = Dialog(main_window)
registry.register(dialog)
@dp.message(commands=["start"])
async def start(message: Message, dialog_manager: DialogManager):
await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)
if __name__ == '__main__':
dp.run_polling(bot, skip_updates=True)
The result will look like:

Widgets and rendering
Passing data
Some widgets contain fixed text, others can show dynamic contents For example:
Const("Hello, {name}!")
will be rendered asHello, {name}!
Format("Hello, {name}!")
will interpolate with window data and transformed to something likeHello, Tishka17!
So, widgets can use data. But data must be loaded from somewhere. To do it Windows and Dialogs have getter
attribute.
Getter can be either a function returning data or static dict or list of such objects.
So let’s create a function and use it to enrich our window with data.
Note
In this and later examples we will skip common bot creation and dialog registration code unless it has notable differences with quickstart
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Window, Dialog
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Format
class MySG(StatesGroup):
main = State()
async def get_data(**kwargs):
return {
"name": "Tishka17",
}
dialog = Dialog(
Window(
Format("Hello, {name}!"),
Button(Const("Useless button"), id="nothing"),
state=MySG.main,
getter=get_data, # here we set our data getter
)
)
It will look like:

Since version 1.6 you do not need getter to access some common objects:
dialog_data
-contents of corresponding field from current context. Normally it is used to store data between multiple calls and windows withing single dialogstart_data
- data passed during current dialog start. It is also accessible usingcurrent_context
middleware_data
- data passed from middlewares to handler. Same asdialog_manager.data
event
- current processing event which triggered window update. Be careful using it, because different types of events can cause refreshing same window.
Widget types
Base information
Currently there are 4 kinds of widgets: texts, keyboards and media.
Texts used to render text anywhere in dialog. It can be message text, button title and so on.
Keyboards represent parts of
InlineKeyboard
Media represent media attachment to message
Input allows to process incoming messages from user. Is has no representation.
Also there are 2 general types:
Whenable
can be hidden or shown depending on data or some conditions. Currently all widgets are whenable. See: Hiding widgetsActionable
is any widget with action (currently only any type of keyboard). It hasid
and can be found by that id. It recommended for all stateful widgets (e.g Checkboxes) to have unique id within dialog. Buttons with different behavior also must have different ids.
Note
Widget id can contain only ascii letters, numbers, underscore and dot symbol.
123
,com.mysite.id
,my_item
- valid idshello world
,my:item
,птичка
- invalid ids
Text widget types
Every time you need to render text use any of text widgets:
Const
- returns text with no midificationsFormat
- formats text usingformat
function. If used in window the data is retrived viagetter
funcion.Multi - multiple texts, joined with a separator
Case - shows one of texts based on condition
Progress
- shows a progress barList
- shows a dynamic group of texts (similar to Select keyboard widget)Jinja - represents a HTML rendered using jinja2 template
Keyboard widget types
Each keyboard provides one or multiple inline buttons. Text on button is rendered using text widget
Button - single inline button. User provided
on_click
method is called when it is clicked.Url - single inline button with url
SwitchInlineQuery
- single inline button to switch inline modeGroup - any group of keyboards one above another or rearranging buttons.
Row - simplified version of group. All buttons placed in single row.
Column - another simplified version of group. All buttons placed in single column one per row.
ScrollingGroup - the same as the
Group
, but with the ability to scroll through pages with buttons.ListGroup
- group of widgets applied repeated multiple times for each item in listCheckbox - button with two states
Select - dynamic group of buttons intended for selection use.
Radio - switch between multiple items. Like select but stores chosen item and renders it differently.
Multiselect - selection of multiple items. Like select/radio but stores all chosen items and renders them differently.
Calendar - simulates a calendar in the form of a keyboard.
Counter
- couple of buttons +/- to input a numberSwitchTo
- switches window within a dialog using provided stateNext
/Back
- switches state forward or backwardStart
- starts a new dialog with no paramsCancel
- closes the current dialog with no result. An underlying dialog is shown
Media widget types
StaticMedia - simple way to share media by url or file path
DynamicMedia - some media attachment constructed dynamically
Combining texts
To combine multiple texts you can use Multi
widget. You can use any texts inside it. Also you can provide a string separator. In simple cases you can just concatenate widgets using +
operator.
from aiogram_dialog.widgets.text import Multi, Const, Format
# let's assume this is our window data getter
async def get_data(**kwargs):
return {"name": "Tishka17"}
# This will produce text `Hello! And goodbye!`
text = Multi(
Const("Hello!"),
Const("And goodbye!"),
sep=" ",
)
# This one will produce text `Hello, Tishka17, and goodbye {name}!`
text2 = Multi(
Format("Hello, {name}"),
Const("and goodbye {name}!"),
sep=", ",
)
# This one will produce `01.02.2003T04:05:06`
text3 = Multi(
Multi(Const("01"), Const("02"), Const("2003"), sep="."),
Multi(Const("04"), Const("05"), Const("06"), sep=":"),
sep="T"
)
To select one of the texts depending on some condition you should use Case
.
The condition can be either a data key or a function:
from typing import Dict
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.text import Case, Const, Format
# let's assume this is our window data getter
async def get_data(**kwargs):
return {"color": "red", "number": 42}
# This will produce text `Square`
text = Case(
{
"red": Const("Square"),
"green": Const("Unicorn"),
"blue": Const("Moon"),
},
selector="color",
)
# This one will produce text `42 is even!`
def parity_selector(data: Dict, case: Case, manager: DialogManager):
return data["number"] % 2
text2 = Case(
{
0: Format("{number} is even!"),
1: Const("It is Odd"),
},
selector=parity_selector,
)
Jinja HTML rendering
It is very easy to create safe HTML messages using Jinja2 templates. Documentation for template language is available at official jinja web page
To use it you need to create text using Jinja
class instead of Format
and set proper parse_mode
.
If you do not want to set default parse mode for whole bot you can set it per-window.
For example you can use environment substitution, cycles and filters:
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Window
from aiogram_dialog.widgets.text import Jinja
class DialogSG(StatesGroup):
ANIMALS = State()
# let's assume this is our window data getter
async def get_data(**kwargs):
return {
"title": "Animals list",
"animals": ["cat", "dog", "my brother's tortoise"]
}
html_text = Jinja("""
<b>{{title}}</b>
{% for animal in animals %}
* <a href="https://yandex.ru/search/?text={{ animal }}">{{ animal|capitalize }}</a>
{% endfor %}
""")
window = Window(
html_text,
parse_mode="HTML", # do not forget to set parse mode
state=DialogSG.ANIMALS,
getter=get_data
)
It will be rendered to this HTML:
<b>Animals list</b>
* <a href="https://yandex.ru/search/?text=cat">Cat</a>
* <a href="https://yandex.ru/search/?text=dog">Dog</a>
* <a href="https://yandex.ru/search/?text=my brother's tortoise">My brother's tortoise</a>
If you want to add custom filters
or do some configuration of jinja Environment you can setup it using aiogram_dialog.widgets.text.setup_jinja
function
Keyboards
Url
Url represents a button with an url. It has no callbacks because telegram does not provide any notifications on click.
Url itself can be any text (including Const
or Format
)
from aiogram_dialog.widgets.kbd import Url
from aiogram_dialog.widgets.text import Const
go_btn = Url(
Const("Github"),
Const('https://github.com/Tishka17/aiogram_dialog/'),
)

Checkbox
Some of the widgets are stateful. They have some state which is affected by on user clicks.
One of such widgets is Checkbox. It can be in checked and unchecked state represented by two texts. On each click it inverses its state.
If a dialog with checkbox is visible, you can check its state by calling is_checked
method and change it calling set_checked
As button has on_click
callback, checkbox has on_state_changed
which is called each time state switched regardless the reason
from aiogram_dialog import DialogManager, ChatEvent
from aiogram_dialog.widgets.kbd import Checkbox, ManagedCheckboxAdapter
from aiogram_dialog.widgets.text import Const
async def check_changed(event: ChatEvent, checkbox: ManagedCheckboxAdapter,
manager: DialogManager):
print("Check status changed:", checkbox.is_checked())
check = Checkbox(
Const("✓ Checked"),
Const("Unchecked"),
id="check",
default=True, # so it will be checked by default,
on_state_changed=check_changed,
)


Note
State of widget is stored separately for each separate opened dialog. But all windows in dialog share same storage. So, multiple widgets with same id will share state. But at the same time if you open several copies of same dialogs they will not mix their states
Select
Select acts like a group of buttons but data is provided dynamically. It is mainly intended to use for selection a item from a list.
Normally text of selection buttons is dynamic (e.g. Format
).
During rendering an item text, it is passed a dictionary with:
item
- current item itselfdata
- original window datapos
- position of item in current items list starting from 1pos0
- position starting from 0
So the main required thing is items. Normally it is a string with key in your window data. The value by this key must be a collection of any objects. If you have a static list of items you can pass it directly to a select widget instead of providing data key.
Next important thing is ids. Besides a widget id you need a function which can return id (string or integer type) for any item.
import operator
from typing import Any
from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Select
from aiogram_dialog.widgets.text import Format
# let's assume this is our window data getter
async def get_data(**kwargs):
fruits = [
("Apple", '1'),
("Pear", '2'),
("Orange", '3'),
("Banana", '4'),
]
return {
"fruits": fruits,
"count": len(fruits),
}
async def on_fruit_selected(callback: CallbackQuery, widget: Any,
manager: DialogManager, item_id: str):
print("Fruit selected: ", item_id)
fruits_kbd = Select(
Format("{item[0]} ({pos}/{data[count]})"), # E.g `✓ Apple (1/4)`
id="s_fruits",
item_id_getter=operator.itemgetter(1),
# each item is a tuple with id on a first position
items="fruits", # we will use items from window data at a key `fruits`
on_click=on_fruit_selected,
)

Radio
Radio is staeful version of select widget. It marks each clicked item as checked deselecting others. It stores which item is selected so it can be accessed later
Unlike for the Select
you need two texts. First one is used to render checked item, second one is for unchecked. Passed data is the same as for Select
Unlike in normal buttons and window they are used to render an item, but not the window data itself.
Also you can provide on_state_changed
callback function. It will be called when selected item is changed.
import operator
from aiogram_dialog.widgets.kbd import Radio
from aiogram_dialog.widgets.text import Format
# let's assume this is our window data getter
async def get_data(**kwargs):
fruits = [
("Apple", '1'),
("Pear", '2'),
("Orange", '3'),
("Banana", '4'),
]
return {
"fruits": fruits,
"count": len(fruits),
}
fruits_kbd = Radio(
Format("🔘 {item[0]}"), # E.g `🔘 Apple`
Format("⚪️ {item[0]}"),
id="r_fruits",
item_id_getter=operator.itemgetter(1),
items="fruits",
)

Useful methods:
get_checked
- returns an id of selected itemsis_checked
- returns if certain id is currently selectedset_checked
- sets the selected item by id
Multiselect
Multiselect is another kind of stateful selection widget.
It very similar to Radio
but remembers multiple selected items
Same as for Radio
you should pass two texts (for checked and unchecked items). Passed data is the same as for Select
import operator
from aiogram_dialog.widgets.kbd import Multiselect
from aiogram_dialog.widgets.text import Format
# let's assume this is our window data getter
async def get_data(**kwargs):
fruits = [
("Apple", '1'),
("Pear", '2'),
("Orange", '3'),
("Banana", '4'),
]
return {
"fruits": fruits,
"count": len(fruits),
}
fruits_kbd = Multiselect(
Format("✓ {item[0]}"), # E.g `✓ Apple`
Format("{item[0]}"),
id="m_fruits",
item_id_getter=operator.itemgetter(1),
items="fruits",
)
After few clicks it will look like:

Other useful options are:
min_selected
- limits minimal number of selected items ignoring clicks if this restriction is violated. It does not affect initial state.max_selected
- limits maximal number of selected itemson_state_changed
- callback function. Called when item changes selected state
To work with selection you can use this methods:
get_checked
- returns a list of ids of all selected itemsis_checked
- returns if certain id is currently selectedset_checked
- changes selection state of provided idreset_checked
- resets all checked items to unchecked state
Warning
Multiselect
widgets stores state of all checked items even if they disappear from window data.
It is very useful when you have pagination, but might be unexpected when data is really removed.
Calendar
Calendar widget allows you to display the keyboard in the form of a calendar, flip through the months and select the date. The initial state looks like the days of the current month. It is possible to switch to the state for choosing the month of the current year or in the state of choosing years.
from datetime import date
from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Calendar
async def on_date_selected(callback: CallbackQuery, widget,
manager: DialogManager, selected_date: date):
await callback.answer(str(selected_date))
calendar = Calendar(id='calendar', on_click=on_date_selected)



Media
StaticMedia
StaticMedia
allows you to share media files by their path os URLs. Though address supports string interpolation as it can be Text
widget, other parameters remain static.
You can use it providing path
or url
to the file, it’s ContentType and additional parameters if required.
Also you might need to change media type (type=ContentType.Photo
) or provide any additional params supported by aiogram using media_params
Be careful using relative paths. Mind the working directory.
from aiogram_dialog.widgets.media import StaticMedia
windows = Window(
StaticMedia(
path="/home/tishka17/python_logo.png"),
type=ContentType.PHOTO,
),
state=DialogSG.greeting,
)
It will look like:

For more complex cases you can read source code of StaticMedia
and create your own widget with any logic you need.
Note
Telegram allows to send files using file_id
instead of uploading same file again.
This make media sending much faster. aiogram_dialog
uses this feature and caches sent file ids in memory
If you want to persistent file_id
cache, implement MediaIdStorageProtocol
and pass instance to your dialog registry
DynamicMedia
StaticMedia
allows you to share any supported media files. Just return a MediaAttachment
from data getter and set selector
for a field name.
Other option is to pass a callable returning MediaAttachment
as a selector
Other media sources
Sometimes you have some custom sources for media files: neither file in fulesystem, not URL in the interner, nor existing file in telegram. It could be some internal storage like database or private s3-compatible one or even runtime generated objects.
In this case recommended steps to solve a problem are:
Generate some custom URI identifying you media. It could be string like “bot://1234” or whatever you want
Inherit from
MessageManager
class and redefineget_media_source
method to load data identified by your URI from custom sourcePass you message manager instance when constructing
Registry
With such implementation you will be able to implement custom media retrieving and keep usage of existing media widgets and file id caching
Hiding widgets
Actually every widget can be hidden including texts, buttons, groups and so on.
It is managed by when
attribute. It can be either a data key, a predicate function or a F-filter (from magic-filter
)
from typing import Dict
from aiogram.filters.state import StatesGroup, State
from magic_filter import F
from aiogram_dialog import Window, DialogManager
from aiogram_dialog.widgets.common import Whenable
from aiogram_dialog.widgets.kbd import Button, Row, Group
from aiogram_dialog.widgets.text import Const, Format, Multi
class MySG(StatesGroup):
main = State()
async def get_data(**kwargs):
return {
"name": "Tishka17",
"extended": False,
}
def is_tishka17(data: Dict, widget: Whenable, manager: DialogManager):
return data.get("name") == "Tishka17"
window = Window(
Multi(
Const("Hello"),
Format("{name}", when="extended"),
sep=" "
),
Group(
Row(
Button(Const("Wait"), id="wait"),
Button(Const("Ignore"), id="ignore"),
when=F["extended"],
),
Button(Const("Admin mode"), id="nothing", when=is_tishka17),
),
state=MySG.main,
getter=get_data,
)

If you only change data setting "extended": True
the window will look differently

Transitions
Types of transitions
Talking to user you will need to switch between different chat states. It can be done using four types of transitions:
State switch inside dialog. Doing so you will just show another window.
Start a dialog in same stack. In this case dialog will be added to task stack with empty dialog context and corresponding window will be shown instead on previously visible one
Start a dialog in new stack. In this case dialog will be shown in a new message and behave independently from current one.
Close dialog. Dialog will be removed from stack, its data erased. underlying dialog will be shown

Task stack
To deal with multiple opened dialogs aiogram_dialog has such thing as dialog stack. It allows dialogs to be opened one over another (“stacked”) so only one of them is visible.
Each time you start a dialog new task is added on top of a stack and new dialog context is created.
Each time you close a dialog, task and dialog context are removed.
You can start same dialog multiple times, and multiple contexts (identified by intent_id
) will be added to stack preserving the order.
So you must be careful restarting you dialogs: do not forget to clear stack or it will eat all your memory
Starting with version 1.0 you can create new stacks but default one exists always.
State switch
Simplest thing you can do to change UI layout is to switch dialog state. It does not affect task stack and just draws another window. Dialog context is kept the same, so all your data is still available.
There are several ways to do it:
dialog_manager.switch_to
method. Pass another state and window will be switcheddialog_manager.next
method. It will switch to the next window in the same order they were passed during dialog creation. Cannot be called when the last window is activedialog_manager.back
method. Switch to the opposite direction (to the previous one). Cannot be called when the first window is active
Let’s create thee windows with buttons and these transitions:

The code may look like:
from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const
class DialogSG(StatesGroup):
first = State()
second = State()
third = State()
async def to_second(callback: CallbackQuery, button: Button,
manager: DialogManager):
await manager.switch_to(DialogSG.second)
async def go_back(callback: CallbackQuery, button: Button,
manager: DialogManager):
await manager.back()
async def go_next(callback: CallbackQuery, button: Button,
manager: DialogManager):
await manager.next()
dialog = Dialog(
Window(
Const("First"),
Button(Const("To second"), id="sec", on_click=to_second),
state=DialogSG.first,
),
Window(
Const("Second"),
Row(
Button(Const("Back"), id="back2", on_click=go_back),
Button(Const("Next"), id="next2", on_click=go_next),
),
state=DialogSG.second,
),
Window(
Const("Third"),
Button(Const("Back"), id="back3", on_click=go_back),
state=DialogSG.third,
)
)
It is ok to use these methods in message handler or if you have additional logic. But for simple cases it looks too complex. To simplify it we have special types of buttons. Each one can contain custom text if needed:
SwitchTo
- callsswitch_to
when clicked. State is provided via constructor attributeNext
- callsnext
when clickedBack
- callsback
when clicked
An example from above may be rewritten using these buttons:
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Back, Next, Row, SwitchTo
from aiogram_dialog.widgets.text import Const
class DialogSG(StatesGroup):
first = State()
second = State()
third = State()
dialog = Dialog(
Window(
Const("First"),
SwitchTo(Const("To second"), id="sec", state=DialogSG.second),
state=DialogSG.first,
),
Window(
Const("Second"),
Row(
Back(),
Next(),
),
state=DialogSG.second,
),
Window(
Const("Third"),
Back(),
state=DialogSG.third,
)
)
Note
You can wonder, why we do not set an id to Back/Next buttons. Though it is normally recommended, these buttons do usually the same action so they have default id
.
If you have multiple buttons of the same type in a window with on_click
callback, you should explicitly set different ids.
Starting a dialog
Each dialog is like a function in python. Is has some input (start_data
), output (result
) and is local data (dialog_data
and widget_data
).
When you start dialog in same stack (not passing StartMode.NEW_STACK
) it is show “above” current dialog. User stop interacting with current dialog windows and sees new one from this moment.
Dialog is identified by his starting state and you need to pass it when starting a dialog. It doesn’t have to be a first state in dialog, but be careful with it. There are several ways to do it:
call
dialog_manager.start
method.use
Start
keyboard widget, which calls same method by itself.
Started dialog will have his own empty context and has no access to parent one. You can specify its behavior passing some data to start
method. It will be stored inside context and is available as dialog_manager.start_data
.
You can store data in dialog_manager.dialog_data
and it will be kept until it is closed and accessed only within this dialog handlers. Stateful widgets store their data is similar way and it is also not shared across opened dialogs.
You have no limitations which dialog to start. You are limited only by a depth of a stack: no more than 100 dialogs can be placed in one stack simultaneously. You can even open the same dialog multiple times and each time it will placed above and have new context.
from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Start
from aiogram_dialog.widgets.text import Const
class DialogSG(StatesGroup):
first = State()
class SubDialogSG(StatesGroup):
first = State()
second = State()
async def start_subdialog(callback: CallbackQuery, button: Button,
manager: DialogManager):
await manager.start(SubDialogSG.second, data={"key": "value"})
dialog = Dialog(
Window(
Const("Main dialog"),
Start(Const("Start 1"), id="start", state=SubDialogSG.first),
Button(Const("Start 2"), id="sec", on_click=start_subdialog),
state=DialogSG.first,
),
)
subdialog = Dialog(
Window(
Const("Subdialog: first"),
state=SubDialogSG.first,
),
Window(
Const("Subdialog: second"),
state=SubDialogSG.second,
),
)
Closing a dialog
When dialog is closed it is removed from stack deleting context. From this moment user returns to a dialog which was underneath the current one.
To close a dialog you have to methods:
call
dialog_manager.done
.use
Cancel
button.
Parent dialog has no access to the context of child one. But you can pass some data as a result to done()
method and then process it in on_process_result
callback of parent dialog.
from typing import Any
from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery
from aiogram_dialog import Data, Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Cancel, Start
from aiogram_dialog.widgets.text import Const
class DialogSG(StatesGroup):
first = State()
class SubDialogSG(StatesGroup):
first = State()
async def main_process_result(start_data: Data, result: Any,
dialog_manager: DialogManager):
print("We have result:", result)
dialog = Dialog(
Window(
Const("Main dialog"),
Start(Const("Start 1"), id="start", state=SubDialogSG.first),
state=DialogSG.first,
),
on_process_result=main_process_result,
)
async def close_subdialog(callback: CallbackQuery, button: Button,
manager: DialogManager):
await manager.done(result={"name": "Tishka17"})
subdialog = Dialog(
Window(
Const("Subdialog"),
Button(Const("Close"), id="btn", on_click=close_subdialog),
Cancel(Const("Close")),
state=DialogSG.first,
),
)
Migration from previous versions
Miragtion 2.0b10 -> 2.0b17
Registry
is now created without dispatcher. After that you need to setup dispatchet usingsetup_dp
methodRegistry.register_start_handler
now requires router (or dispatcher)
Migration 1.x -> 2.0b10
Main objects like
Dialog
,LaunchMode
andDialogManager
should be imported directly fromaiogram_dialog
package.Whenable
is moved towidgets.common
subpackageWhen finding widget by
id
you will get managed version of widget. This objects no more expectDialogManager
orChatEvent
arguments in their methodsFor
ListGroup
items callbacks you will getSubManager
which behavior slightly changed.SubManager
moved toaiogram_dialog
packageManagedDialog
protocol renamed toDialogProtocol
Dialog
no more contains.next
,.back
and.switch_to
methods. They are available inDialogManager
DialogManagerFactory
protocol simplifiedno more
ManagedDialogAdapterProto
. You will getDialog
instance insteadno more
data
inContext
. Renamed tostart_data
many time agodialog_data
andstart_data
added toDialogManager
,data
is renamed tomiddleware_data
Migration 0.11 -> 1.0
reset_stack
was replaced withStartMode
. E.g.reset_stack=true
is nowmode=StartMode.RESET_STACK
dialog no more changes current aiogram state
- In manager
context
andcurrent_intent()
were replaced withcurrent_context()
call. dialog_data
is a dict to hold user datawidget_data
is a dict to hold data of widgetsstart_data
is a data provided whe dialog startstate
is current dialog state
- In manager
When subdialog finishes parent is restored with previous state, not which it was started
Changed signature of
on_process_result
callback. It now accepts start data used to start subdialogGroup.keep_rows
option removed. Setwidth=None
(default value) if you want to keep rows.
Helper tools (experimental)
State diagram
You can generate image with your states and transitions.
Firstly you need to install [graphviz](https://graphviz.org/download/) into your system. Check installation instructions on official site.
Install library with tools extras:
pip install aiogram_dialog[tools]
Import rendering method:
from aiogram_dialog.tools import render_transitions
Call it passing your registry or list of dialogs:
from aiogram.filters.state import StatesGroup, State
from aiogram.types import Message
from aiogram_dialog import Dialog, DialogManager, DialogProtocol, Window
from aiogram_dialog.tools import render_transitions
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Back, Next
from aiogram_dialog.widgets.text import Const
class RenderSG(StatesGroup):
first = State()
second = State()
last = State()
async def on_input(
message: Message, dialog: DialogProtocol, manager: DialogManager,
):
manager.dialog_data["name"] = message.text
await manager.next()
dialog = Dialog(
Window(
Const("1. First"),
Next(),
state=RenderSG.first,
),
Window(
Const("2. Second"),
Back(),
MessageInput(on_input),
state=RenderSG.second,
),
Window(
Const("3. Last"),
Back(),
state=RenderSG.last,
),
)
# this is diagram rendering
render_transitions([dialog])
Run your code and you will get aiogram_dialog.png
in working directory:

State transition hints
You may notice, that not all transitions are show on diagram. This is because library cannot analyze source code of you callbacks. Only transitions, done by special buttons are shown.
To fix this behavior you can set preview_add_transitions
parameter of window:
from aiogram.filters.state import StatesGroup, State
from aiogram.types import Message
from aiogram_dialog import Dialog, DialogManager, DialogProtocol, Window
from aiogram_dialog.tools import render_transitions
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Next, Back
from aiogram_dialog.widgets.text import Const
class RenderSG(StatesGroup):
first = State()
second = State()
last = State()
async def on_input(message: Message, dialog: DialogProtocol,
manager: DialogManager):
manager.dialog_data["name"] = message.text
await manager.next() # rendering tool cannot detect this call
dialog = Dialog(
Window(
Const("1. First"),
Next(),
state=RenderSG.first,
),
Window(
Const("2. Second"),
Back(),
MessageInput(on_input),
state=RenderSG.second,
preview_add_transitions=[Next()], # this is a hint for rendering tool
),
Window(
Const("3. Last"),
Back(),
state=RenderSG.last,
),
)
render_transitions([dialog])
Run the code and check updated rendering result:

Dialogs preview
Import rendering method:
from aiogram_dialog.tools import render_preview
Add some data to be shown on preview using preview_data
parameter of window:
class RenderSG(StatesGroup):
first = State()
dialog = Dialog(
Window(
Format("Hello, {name}"),
Cancel(),
state=RenderSG.first,
preview_data={"name": "Tishka17"},
),
)
Call it passing your registry and filename somewhere inside your asyncio code:
await render_transitions(registry, "preview.html")
Together it will be something like this:
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import Dialog, DialogRegistry, Window
from aiogram_dialog.tools import render_preview
from aiogram_dialog.widgets.kbd import Cancel
from aiogram_dialog.widgets.text import Format
class RenderSG(StatesGroup):
first = State()
dialog = Dialog(
Window(
Format("Hello, {name}"),
Cancel(),
state=RenderSG.first,
preview_data={"name": "Tishka17"},
),
)
storage = MemoryStorage()
bot = Bot(token='BOT TOKEN HERE')
dp = Dispatcher(bot, storage=storage)
registry = DialogRegistry(dp)
registry.register(dialog)
async def main():
await render_preview(registry, "preview.html")
if __name__ == '__main__':
asyncio.run(main())
As a result you will see a html file in working directory, that can be opened in browser to preview how all dialogs will look like.

Frequently asked questions (FAQ)
How can I retrieve data from stateful widget (Checkbox
, Multiselect
, etc)?
If you have a global variable with widget you can use it with dialog_manager:
widget.get_checked(manager)
Other option is to use widget id to get adapter and then call its methods:
widget = dialog_manager.dialog().find('some_widget_id')
widget.get_checked()
What is current_context().widget_data
for?
This dictionary contains data stored by widgets themselves. Data structure is a matter of widget implementation so it should not be accessed directly. If you need to retrieve widget state use its methods.
How can I set a default value for a Musltiselect
or a Radio
The better way is to state insed on_start
callback of dialog.
How can I show Select
widget in multiple rows? What about pagination?
Wrap it with some layout widget like Group
, Column
or ScrollingGroup
.
How can I show photo by its file_id
?
You need to create custom widget. Use StaticMedia
as a sample.
How can I request user location or contact?
You need to send somehow a message with reply keyboard. You can use MessageInput
to handle response within a window.
How can I make library not to send new message when user sends a message himself?
It is working this way because otherwise a dialog can be outside of user screen and he will loose it. If you still want to disable this feature you can add a MessageInput and then set dialog_manager.show_mode=ShowMode.EDIT
inside handler.
How can I access middleware data inside dialog handlers or widgets?
In getter you will get it as kwargs
In handlers it is available via
dialog_manager.data
During rendering (like in
Format
) it is passed as amiddleware_data
How can I find the current user?
Get it as a dialog_manager.event.from_user
.
Caution: in case of background updates (done via BgManager
) it can contain only id
. If it is not suitable for you case set load=True
while creating bg manager.
How can I pass data between dialogs?
Input - pass via dialog_manager.start(..., data="here")
, read using dialog_manager.start_data
.
Output - pass via dialog_manager.done(result="here")
, read as a parameter to on_process_result
of parent dialog
More details: Starting a dialog, Closing a dialog.