diff --git a/Cargo.toml b/Cargo.toml
index b106d02f..f9ecce06 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,7 @@ async-trait = "0.1.22"
 futures = "0.3.1"
 pin-project = "0.4.6"
 serde_with_macros = "1.0.1"
+frunk = "0.3.1"
 
 teloxide-macros = "0.2.1"
 
diff --git a/README.md b/README.md
index e9ee31ec..819c28e3 100644
--- a/README.md
+++ b/README.md
@@ -23,12 +23,11 @@
 
 ## Table of contents
  - [Features](https://github.com/teloxide/teloxide#features)
- - [Getting started](https://github.com/teloxide/teloxide#getting-started)
- - [Examples](https://github.com/teloxide/teloxide#examples)
+ - [Setting up your environment](https://github.com/teloxide/teloxide#setting-up-your-environment)
+ - [API overview](https://github.com/teloxide/teloxide#api-overview)
    - [The ping-pong bot](https://github.com/teloxide/teloxide#the-ping-pong-bot)
    - [Commands](https://github.com/teloxide/teloxide#commands)
-   - [Guess a number](https://github.com/teloxide/teloxide#guess-a-number)
- - [More examples!](https://github.com/teloxide/teloxide#more-examples)
+   - [Dialogues](https://github.com/teloxide/teloxide#dialogues)
  - [Recommendations](https://github.com/teloxide/teloxide#recommendations)
  - [FAQ](https://github.com/teloxide/teloxide#faq)
    - [Where I can ask questions?](https://github.com/teloxide/teloxide#where-i-can-ask-questions)
@@ -57,9 +56,10 @@ All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">
 Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>!
 </p>
 
-## Getting started
- 1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
- 2. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
+## Setting up your environment
+ 1. [Download Rust](http://rustup.rs/).
+ 2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
+ 3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
 ```bash
 # Unix-like
 $ export TELOXIDE_TOKEN=<Your token here>
@@ -67,7 +67,7 @@ $ export TELOXIDE_TOKEN=<Your token here>
 # Windows
 $ set TELOXIDE_TOKEN=<Your token here>
 ```
- 3. Be sure that you are up to date:
+ 4. Be sure that you are up to date:
 ```bash
 # If you're using stable
 $ rustup update stable
@@ -78,7 +78,7 @@ $ rustup update nightly
 $ rustup override set nightly
 ```
 
- 4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
+ 5. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
 ```toml
 [dependencies]
 teloxide = "0.2.0"
@@ -87,7 +87,9 @@ tokio = "0.2.11"
 pretty_env_logger = "0.4.0"
 ```
 
-## The ping-pong bot
+## API overview
+
+### The ping-pong bot
 This bot has a single message handler, which answers "pong" to each incoming message:
 
 ([Full](https://github.com/teloxide/teloxide/blob/master/examples/ping_pong_bot/src/main.rs))
@@ -119,7 +121,7 @@ async fn main() {
   </kbd>
 </div>
 
-## Commands
+### Commands
 Commands are defined similar to how we define CLI using [structopt](https://docs.rs/structopt/0.3.9/structopt/). This bot says "I am a cat! Meow!" on `/meow`, generates a random number within [0; 1) on `/generate`, and shows the usage guide on `/help`:
 
 ([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs))
@@ -185,84 +187,182 @@ See? The dispatcher gives us a stream of messages, so we can handle it as we wan
  
  - ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html) and [others](https://docs.rs/teloxide/latest/teloxide/dispatching/trait.DispatcherHandlerRxExt.html) and [others](https://docs.rs/tokio/0.2.13/tokio/sync/index.html)!
 
-## Guess a number
-Wanna see more? This is a bot, which starts a game on each incoming message. You must guess a number from 1 to 10 (inclusively):
+### Dialogues
+Wanna see more? This is how dialogues management is made in teloxide.
 
-([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs))
+([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs))
 ```rust
 // Imports are omitted...
 
-#[derive(SmartDefault)]
-enum Dialogue {
-    #[default]
-    Start,
-    ReceiveAttempt(u8),
+pub struct StartState;
+
+pub struct ReceiveFullNameState {
+    rest: StartState,
 }
 
-type Cx<State> = DialogueDispatcherHandlerCx<Message, State>;
-type Res = ResponseResult<DialogueStage<Dialogue>>;
-
-async fn start(cx: Cx<()>) -> Res {
-    cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).")
-        .send()
-        .await?;
-    next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
+pub struct ReceiveAgeState {
+    rest: ReceiveFullNameState,
+    full_name: String,
 }
 
-async fn receive_attempt(cx: Cx<u8>) -> Res {
-    let secret = cx.dialogue;
+pub struct ReceiveFavouriteMusicState {
+    rest: ReceiveAgeState,
+    age: u8,
+}
 
-    match cx.update.text() {
-        None => {
-            cx.answer("Oh, please, send me a text message!").send().await?;
-            next(Dialogue::ReceiveAttempt(secret))
+#[derive(Display)]
+#[display(
+    "Your full name: {rest.rest.full_name}, your age: {rest.age}, your \
+     favourite music: {favourite_music}"
+)]
+pub struct ExitState {
+    rest: ReceiveFavouriteMusicState,
+    favourite_music: FavouriteMusic,
+}
+
+up!(
+    StartState -> ReceiveFullNameState,
+    ReceiveFullNameState + [full_name: String] -> ReceiveAgeState,
+    ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState,
+    ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState,
+);
+
+pub type Dialogue = Coprod!(
+    StartState,
+    ReceiveFullNameState,
+    ReceiveAgeState,
+    ReceiveFavouriteMusicState,
+);
+
+wrap_dialogue!(
+    Wrapper(Dialogue),
+    default Self(Dialogue::inject(StartState)),
+);
+```
+
+The [`wrap_dialogue!`](https://docs.rs/teloxide/latest/teloxide/macro.wrap_dialogue.html) macro generates a new-type of `Dialogue` with a default implementation.
+
+([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs))
+```rust
+// Imports are omitted...
+
+pub type In<State> = TransitionIn<State, std::convert::Infallible>;
+pub type Out = TransitionOut<Wrapper>;
+
+pub async fn start(cx: In<StartState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    cx.answer_str("Let's start! First, what's your full name?").await?;
+    next(dialogue.up())
+}
+
+pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text_owned() {
+        Some(full_name) => {
+            cx.answer_str("What a wonderful name! Your age?").await?;
+            next(dialogue.up(full_name))
+        }
+        _ => {
+            cx.answer_str("Please, enter a text message!").await?;
+            next(dialogue)
         }
-        Some(text) => match text.parse::<u8>() {
-            Ok(attempt) => {
-                if attempt == secret {
-                    cx.answer("Congratulations! You won!").send().await?;
-                    exit()
-                } else {
-                    cx.answer("No.").send().await?;
-                    next(Dialogue::ReceiveAttempt(secret))
-                }
-            }
-            Err(_) => {
-                cx.answer("Oh, please, send me a number in the range [1; 10]!")
-                    .send()
-                    .await?;
-                next(Dialogue::ReceiveAttempt(secret))
-            }
-        },
     }
 }
 
-async fn handle_message(
-    cx: DialogueDispatcherHandlerCx<Message, Dialogue>,
-) -> Res {
-    // Match is omitted...
+pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text().map(str::parse) {
+        Some(Ok(age)) => {
+            cx.answer("Good. Now choose your favourite music:")
+                .reply_markup(FavouriteMusic::markup())
+                .send()
+                .await?;
+            next(dialogue.up(age))
+        }
+        _ => {
+            cx.answer_str("Please, enter a number!").await?;
+            next(dialogue)
+        }
+    }
 }
 
-#[tokio::main]
-async fn main() {
-    // Setup is omitted...
+pub async fn receive_favourite_music(
+    cx: In<ReceiveFavouriteMusicState>,
+) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text().map(str::parse) {
+        Some(Ok(favourite_music)) => {
+            cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music)))
+                .await?;
+            exit()
+        }
+        _ => {
+            cx.answer_str("Please, enter from the keyboard!").await?;
+            next(dialogue)
+        }
+    }
 }
 ```
 
-<div align="center">
-  <kbd>
-    <img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="600" />
-  </kbd>
-  <br/><br/>
-</div>
+([dialogue_bot/src/favourite_music.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/favourite_music.rs))
+```rust
+// Imports are omitted...
 
-Our [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine), designating a user dialogue, cannot be in an invalid state, and this is why it is called "type-safe". We could use `enum` + `Option`s instead, but it would lead us to lots of unpleasant `.unwrap()`s.
+#[derive(Copy, Clone, Display, FromStr)]
+pub enum FavouriteMusic {
+    Rock,
+    Metal,
+    Pop,
+    Other,
+}
 
-Remember that a classical [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine) is defined by its initial state, a list of its possible states and a transition function? We can think that `Dialogue` is a finite automaton with a context type at each state (`Dialogue::Start` has `()`, `Dialogue::ReceiveAttempt` has `u8`).
+impl FavouriteMusic {
+    pub fn markup() -> ReplyKeyboardMarkup {
+        ReplyKeyboardMarkup::default().append_row(vec![
+            KeyboardButton::new("Rock"),
+            KeyboardButton::new("Metal"),
+            KeyboardButton::new("Pop"),
+            KeyboardButton::new("Other"),
+        ])
+    }
+}
+```
 
-See [examples/dialogue_bot](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs) to see a bit more complicated bot with dialogues.
 
-## [More examples!](https://github.com/teloxide/teloxide/tree/master/examples)
+([dialogue_bot/src/main.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs))
+```rust
+// Imports are omitted...
+
+#[tokio::main]
+async fn main() {
+    teloxide::enable_logging!();
+    log::info!("Starting dialogue_bot!");
+
+    let bot = Bot::from_env();
+
+    Dispatcher::new(bot)
+        .messages_handler(DialogueDispatcher::new(|cx| async move {
+            let DialogueWithCx { cx, dialogue } = cx;
+
+            // Unwrap without panic because of std::convert::Infallible.
+            let Wrapper(dialogue) = dialogue.unwrap();
+
+            dispatch!(
+                [cx, dialogue] ->
+                [start, receive_full_name, receive_age, receive_favourite_music]
+            )
+            .expect("Something wrong with the bot!")
+        }))
+        .dispatch()
+        .await;
+}
+```
+
+[More examples!](https://github.com/teloxide/teloxide/tree/master/examples)
 
 ## Recommendations
  - Use this pattern:
diff --git a/examples/dialogue_bot/Cargo.toml b/examples/dialogue_bot/Cargo.toml
index 947470f0..eac16046 100644
--- a/examples/dialogue_bot/Cargo.toml
+++ b/examples/dialogue_bot/Cargo.toml
@@ -10,9 +10,9 @@ edition = "2018"
 log = "0.4.8"
 tokio = "0.2.9"
 pretty_env_logger = "0.4.0"
-smart-default = "0.6.0"
 parse-display = "0.1.1"
 teloxide = { path = "../../" }
+frunk = "0.3.1"
 
 [profile.release]
 lto = true
\ No newline at end of file
diff --git a/examples/dialogue_bot/src/favourite_music.rs b/examples/dialogue_bot/src/favourite_music.rs
new file mode 100644
index 00000000..5b102332
--- /dev/null
+++ b/examples/dialogue_bot/src/favourite_music.rs
@@ -0,0 +1,22 @@
+use parse_display::{Display, FromStr};
+
+use teloxide::types::{KeyboardButton, ReplyKeyboardMarkup};
+
+#[derive(Copy, Clone, Display, FromStr)]
+pub enum FavouriteMusic {
+    Rock,
+    Metal,
+    Pop,
+    Other,
+}
+
+impl FavouriteMusic {
+    pub fn markup() -> ReplyKeyboardMarkup {
+        ReplyKeyboardMarkup::default().append_row(vec![
+            KeyboardButton::new("Rock"),
+            KeyboardButton::new("Metal"),
+            KeyboardButton::new("Pop"),
+            KeyboardButton::new("Other"),
+        ])
+    }
+}
diff --git a/examples/dialogue_bot/src/main.rs b/examples/dialogue_bot/src/main.rs
index 9d610c2f..fac687fb 100644
--- a/examples/dialogue_bot/src/main.rs
+++ b/examples/dialogue_bot/src/main.rs
@@ -15,167 +15,16 @@
 // ```
 
 #![allow(clippy::trivial_regex)]
+#![allow(dead_code)]
 
-#[macro_use]
-extern crate smart_default;
+mod favourite_music;
+mod states;
+mod transitions;
 
-use std::convert::Infallible;
-use teloxide::{
-    prelude::*,
-    types::{KeyboardButton, ReplyKeyboardMarkup},
-};
+use states::*;
+use transitions::*;
 
-use parse_display::{Display, FromStr};
-
-// ============================================================================
-// [Favourite music kinds]
-// ============================================================================
-
-#[derive(Copy, Clone, Display, FromStr)]
-enum FavouriteMusic {
-    Rock,
-    Metal,
-    Pop,
-    Other,
-}
-
-impl FavouriteMusic {
-    fn markup() -> ReplyKeyboardMarkup {
-        ReplyKeyboardMarkup::default().append_row(vec![
-            KeyboardButton::new("Rock"),
-            KeyboardButton::new("Metal"),
-            KeyboardButton::new("Pop"),
-            KeyboardButton::new("Other"),
-        ])
-    }
-}
-
-// ============================================================================
-// [A type-safe finite automaton]
-// ============================================================================
-
-#[derive(Clone)]
-struct ReceiveAgeState {
-    full_name: String,
-}
-
-#[derive(Clone)]
-struct ReceiveFavouriteMusicState {
-    data: ReceiveAgeState,
-    age: u8,
-}
-
-#[derive(Display)]
-#[display(
-    "Your full name: {data.data.full_name}, your age: {data.age}, your \
-     favourite music: {favourite_music}"
-)]
-struct ExitState {
-    data: ReceiveFavouriteMusicState,
-    favourite_music: FavouriteMusic,
-}
-
-#[derive(SmartDefault)]
-enum Dialogue {
-    #[default]
-    Start,
-    ReceiveFullName,
-    ReceiveAge(ReceiveAgeState),
-    ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
-}
-
-// ============================================================================
-// [Control a dialogue]
-// ============================================================================
-
-type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
-type Res = ResponseResult<DialogueStage<Dialogue>>;
-
-async fn start(cx: Cx<()>) -> Res {
-    cx.answer("Let's start! First, what's your full name?").send().await?;
-    next(Dialogue::ReceiveFullName)
-}
-
-async fn full_name(cx: Cx<()>) -> Res {
-    match cx.update.text() {
-        None => {
-            cx.answer("Please, send me a text message!").send().await?;
-            next(Dialogue::ReceiveFullName)
-        }
-        Some(full_name) => {
-            cx.answer("What a wonderful name! Your age?").send().await?;
-            next(Dialogue::ReceiveAge(ReceiveAgeState {
-                full_name: full_name.to_owned(),
-            }))
-        }
-    }
-}
-
-async fn age(cx: Cx<ReceiveAgeState>) -> Res {
-    match cx.update.text().unwrap().parse() {
-        Ok(age) => {
-            cx.answer("Good. Now choose your favourite music:")
-                .reply_markup(FavouriteMusic::markup())
-                .send()
-                .await?;
-            next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState {
-                data: cx.dialogue.unwrap(),
-                age,
-            }))
-        }
-        Err(_) => {
-            cx.answer("Oh, please, enter a number!").send().await?;
-            next(Dialogue::ReceiveAge(cx.dialogue.unwrap()))
-        }
-    }
-}
-
-async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res {
-    match cx.update.text().unwrap().parse() {
-        Ok(favourite_music) => {
-            cx.answer(format!(
-                "Fine. {}",
-                ExitState {
-                    data: cx.dialogue.clone().unwrap(),
-                    favourite_music
-                }
-            ))
-            .send()
-            .await?;
-            exit()
-        }
-        Err(_) => {
-            cx.answer("Oh, please, enter from the keyboard!").send().await?;
-            next(Dialogue::ReceiveFavouriteMusic(cx.dialogue.unwrap()))
-        }
-    }
-}
-
-async fn handle_message(cx: Cx<Dialogue>) -> Res {
-    let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
-
-    // You need handle the error instead of panicking in real-world code, maybe
-    // send diagnostics to a development chat.
-    match dialogue.expect("Failed to get dialogue info from storage") {
-        Dialogue::Start => {
-            start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
-        }
-        Dialogue::ReceiveFullName => {
-            full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await
-        }
-        Dialogue::ReceiveAge(s) => {
-            age(DialogueDispatcherHandlerCx::new(bot, update, s)).await
-        }
-        Dialogue::ReceiveFavouriteMusic(s) => {
-            favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s))
-                .await
-        }
-    }
-}
-
-// ============================================================================
-// [Run!]
-// ============================================================================
+use teloxide::prelude::*;
 
 #[tokio::main]
 async fn main() {
@@ -190,7 +39,16 @@ async fn run() {
 
     Dispatcher::new(bot)
         .messages_handler(DialogueDispatcher::new(|cx| async move {
-            handle_message(cx).await.expect("Something wrong with the bot!")
+            let DialogueWithCx { cx, dialogue } = cx;
+
+            // Unwrap without panic because of std::convert::Infallible.
+            let Wrapper(dialogue) = dialogue.unwrap();
+
+            dispatch!(
+                [cx, dialogue] ->
+                [start, receive_full_name, receive_age, receive_favourite_music]
+            )
+            .expect("Something wrong with the bot!")
         }))
         .dispatch()
         .await;
diff --git a/examples/dialogue_bot/src/states.rs b/examples/dialogue_bot/src/states.rs
new file mode 100644
index 00000000..c920392a
--- /dev/null
+++ b/examples/dialogue_bot/src/states.rs
@@ -0,0 +1,49 @@
+use teloxide::prelude::*;
+
+use super::favourite_music::FavouriteMusic;
+use parse_display::Display;
+
+pub struct StartState;
+
+pub struct ReceiveFullNameState {
+    rest: StartState,
+}
+
+pub struct ReceiveAgeState {
+    rest: ReceiveFullNameState,
+    full_name: String,
+}
+
+pub struct ReceiveFavouriteMusicState {
+    rest: ReceiveAgeState,
+    age: u8,
+}
+
+#[derive(Display)]
+#[display(
+    "Your full name: {rest.rest.full_name}, your age: {rest.age}, your \
+     favourite music: {favourite_music}"
+)]
+pub struct ExitState {
+    rest: ReceiveFavouriteMusicState,
+    favourite_music: FavouriteMusic,
+}
+
+up!(
+    StartState -> ReceiveFullNameState,
+    ReceiveFullNameState + [full_name: String] -> ReceiveAgeState,
+    ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState,
+    ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState,
+);
+
+pub type Dialogue = Coprod!(
+    StartState,
+    ReceiveFullNameState,
+    ReceiveAgeState,
+    ReceiveFavouriteMusicState,
+);
+
+wrap_dialogue!(
+    Wrapper(Dialogue),
+    default Self(Dialogue::inject(StartState)),
+);
diff --git a/examples/dialogue_bot/src/transitions.rs b/examples/dialogue_bot/src/transitions.rs
new file mode 100644
index 00000000..4a20fafb
--- /dev/null
+++ b/examples/dialogue_bot/src/transitions.rs
@@ -0,0 +1,64 @@
+use teloxide::prelude::*;
+
+use super::{favourite_music::FavouriteMusic, states::*};
+
+pub type In<State> = TransitionIn<State, std::convert::Infallible>;
+pub type Out = TransitionOut<Wrapper>;
+
+pub async fn start(cx: In<StartState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    cx.answer_str("Let's start! First, what's your full name?").await?;
+    next(dialogue.up())
+}
+
+pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text_owned() {
+        Some(full_name) => {
+            cx.answer_str("What a wonderful name! Your age?").await?;
+            next(dialogue.up(full_name))
+        }
+        _ => {
+            cx.answer_str("Please, enter a text message!").await?;
+            next(dialogue)
+        }
+    }
+}
+
+pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text().map(str::parse) {
+        Some(Ok(age)) => {
+            cx.answer("Good. Now choose your favourite music:")
+                .reply_markup(FavouriteMusic::markup())
+                .send()
+                .await?;
+            next(dialogue.up(age))
+        }
+        _ => {
+            cx.answer_str("Please, enter a number!").await?;
+            next(dialogue)
+        }
+    }
+}
+
+pub async fn receive_favourite_music(
+    cx: In<ReceiveFavouriteMusicState>,
+) -> Out {
+    let (cx, dialogue) = cx.unpack();
+
+    match cx.update.text().map(str::parse) {
+        Some(Ok(favourite_music)) => {
+            cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music)))
+                .await?;
+            exit()
+        }
+        _ => {
+            cx.answer_str("Please, enter from the keyboard!").await?;
+            next(dialogue)
+        }
+    }
+}
diff --git a/examples/guess_a_number_bot/Cargo.toml b/examples/guess_a_number_bot/Cargo.toml
deleted file mode 100644
index c81ef20f..00000000
--- a/examples/guess_a_number_bot/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "guess_a_number_bot"
-version = "0.1.0"
-authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
-edition = "2018"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-log = "0.4.8"
-tokio = "0.2.9"
-smart-default = "0.6.0"
-rand = "0.7.3"
-pretty_env_logger = "0.4.0"
-teloxide = { path = "../../" }
\ No newline at end of file
diff --git a/examples/guess_a_number_bot/src/main.rs b/examples/guess_a_number_bot/src/main.rs
deleted file mode 100644
index 019f593a..00000000
--- a/examples/guess_a_number_bot/src/main.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-// This is a guess-a-number game!
-//
-// # Example
-// ```
-//  - Hello
-//  - Let's play a game! Guess a number from 1 to 10 (inclusively).
-//  - 4
-//  - No.
-//  - 3
-//  - No.
-//  - Blablabla
-//  - Oh, please, send me a text message!
-//  - 111
-//  - Oh, please, send me a number in the range [1; 10]!
-//  - 5
-//  - Congratulations! You won!
-// ```
-
-#[macro_use]
-extern crate smart_default;
-
-use teloxide::prelude::*;
-
-use rand::{thread_rng, Rng};
-use std::convert::Infallible;
-
-// ============================================================================
-// [A type-safe finite automaton]
-// ============================================================================
-
-#[derive(SmartDefault)]
-enum Dialogue {
-    #[default]
-    Start,
-    ReceiveAttempt(u8),
-}
-
-// ============================================================================
-// [Control a dialogue]
-// ============================================================================
-
-type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
-type Res = ResponseResult<DialogueStage<Dialogue>>;
-
-async fn start(cx: Cx<()>) -> Res {
-    cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).")
-        .send()
-        .await?;
-    next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
-}
-
-async fn receive_attempt(cx: Cx<u8>) -> Res {
-    let secret = cx.dialogue.unwrap();
-
-    match cx.update.text() {
-        None => {
-            cx.answer("Oh, please, send me a text message!").send().await?;
-            next(Dialogue::ReceiveAttempt(secret))
-        }
-        Some(text) => match text.parse::<u8>() {
-            Ok(attempt) => {
-                if attempt == secret {
-                    cx.answer("Congratulations! You won!").send().await?;
-                    exit()
-                } else {
-                    cx.answer("No.").send().await?;
-                    next(Dialogue::ReceiveAttempt(secret))
-                }
-            }
-            Err(_) => {
-                cx.answer("Oh, please, send me a number in the range [1; 10]!")
-                    .send()
-                    .await?;
-                next(Dialogue::ReceiveAttempt(secret))
-            }
-        },
-    }
-}
-
-async fn handle_message(
-    cx: DialogueDispatcherHandlerCx<Message, Dialogue, Infallible>,
-) -> Res {
-    let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
-
-    // You need handle the error instead of panicking in real-world code, maybe
-    // send diagnostics to a development chat.
-    match dialogue.expect("Failed to get dialogue info from storage") {
-        Dialogue::Start => {
-            start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
-        }
-        Dialogue::ReceiveAttempt(secret) => {
-            receive_attempt(DialogueDispatcherHandlerCx::new(
-                bot, update, secret,
-            ))
-            .await
-        }
-    }
-}
-
-// ============================================================================
-// [Run!]
-// ============================================================================
-
-#[tokio::main]
-async fn main() {
-    run().await;
-}
-
-async fn run() {
-    teloxide::enable_logging!();
-    log::info!("Starting guess_a_number_bot!");
-
-    let bot = Bot::from_env();
-
-    Dispatcher::new(bot)
-        .messages_handler(DialogueDispatcher::new(|cx| async move {
-            handle_message(cx).await.expect("Something wrong with the bot!")
-        }))
-        .dispatch()
-        .await;
-}
diff --git a/examples/heroku_ping_pong_bot/Cargo.toml b/examples/heroku_ping_pong_bot/Cargo.toml
index 984abfb1..d5d8d1f1 100644
--- a/examples/heroku_ping_pong_bot/Cargo.toml
+++ b/examples/heroku_ping_pong_bot/Cargo.toml
@@ -11,7 +11,7 @@ log = "0.4.8"
 futures = "0.3.4"
 tokio = "0.2.9"
 pretty_env_logger = "0.4.0"
-teloxide = "0.2.0"
+teloxide =  { path = "../../" }
 
 # Used to setup a webhook
 warp = "0.2.2"
diff --git a/examples/heroku_ping_pong_bot/src/main.rs b/examples/heroku_ping_pong_bot/src/main.rs
index 06d2491c..57d3d105 100644
--- a/examples/heroku_ping_pong_bot/src/main.rs
+++ b/examples/heroku_ping_pong_bot/src/main.rs
@@ -14,14 +14,19 @@ async fn main() {
     run().await;
 }
 
-async fn handle_rejection(error: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
+async fn handle_rejection(
+    error: warp::Rejection,
+) -> Result<impl warp::Reply, Infallible> {
     log::error!("Cannot process the request due to: {:?}", error);
     Ok(StatusCode::INTERNAL_SERVER_ERROR)
 }
 
-pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener<Infallible> {
+pub async fn webhook<'a>(
+    bot: Arc<Bot>,
+) -> impl update_listeners::UpdateListener<Infallible> {
     // Heroku defines auto defines a port value
-    let teloxide_token = env::var("TELOXIDE_TOKEN").expect("TELOXIDE_TOKEN env variable missing");
+    let teloxide_token = env::var("TELOXIDE_TOKEN")
+        .expect("TELOXIDE_TOKEN env variable missing");
     let port: u16 = env::var("PORT")
         .expect("PORT env variable missing")
         .parse()
@@ -31,10 +36,7 @@ pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener
     let path = format!("bot{}", teloxide_token);
     let url = format!("https://{}/{}", host, path);
 
-    bot.set_webhook(url)
-        .send()
-        .await
-        .expect("Cannot setup a webhook");
+    bot.set_webhook(url).send().await.expect("Cannot setup a webhook");
 
     let (tx, rx) = mpsc::unbounded_channel();
 
@@ -80,12 +82,14 @@ async fn run() {
     Dispatcher::new(Arc::clone(&bot))
         .messages_handler(|rx: DispatcherHandlerRx<Message>| {
             rx.for_each(|message| async move {
-                message.answer("pong").send().await.log_on_error().await;
+                message.answer_str("pong").await.log_on_error().await;
             })
         })
         .dispatch_with_listener(
             webhook(bot).await,
-            LoggingErrorHandler::with_custom_text("An error from the update listener"),
+            LoggingErrorHandler::with_custom_text(
+                "An error from the update listener",
+            ),
         )
         .await;
 }
diff --git a/examples/ping_pong_bot/src/main.rs b/examples/ping_pong_bot/src/main.rs
index b59e8c0b..1b52e4cc 100644
--- a/examples/ping_pong_bot/src/main.rs
+++ b/examples/ping_pong_bot/src/main.rs
@@ -16,7 +16,7 @@ async fn run() {
     Dispatcher::new(bot)
         .messages_handler(|rx: DispatcherHandlerRx<Message>| {
             rx.for_each(|message| async move {
-                message.answer("pong").send().await.log_on_error().await;
+                message.answer_str("pong").await.log_on_error().await;
             })
         })
         .dispatch()
diff --git a/examples/shared_state_bot/src/main.rs b/examples/shared_state_bot/src/main.rs
index a066849b..edfc1a6c 100644
--- a/examples/shared_state_bot/src/main.rs
+++ b/examples/shared_state_bot/src/main.rs
@@ -26,11 +26,10 @@ async fn run() {
                 let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
 
                 message
-                    .answer(format!(
+                    .answer_str(format!(
                         "I received {} messages in total.",
                         previous
                     ))
-                    .send()
                     .await
                     .log_on_error()
                     .await;
diff --git a/examples/simple_commands_bot/src/main.rs b/examples/simple_commands_bot/src/main.rs
index 25bdb39a..8c6d71be 100644
--- a/examples/simple_commands_bot/src/main.rs
+++ b/examples/simple_commands_bot/src/main.rs
@@ -18,7 +18,7 @@ fn generate() -> String {
 }
 
 async fn answer(
-    cx: DispatcherHandlerCx<Message>,
+    cx: UpdateWithCx<Message>,
     command: Command,
 ) -> ResponseResult<()> {
     match command {
diff --git a/examples/webhook_ping_pong_bot/src/main.rs b/examples/webhook_ping_pong_bot/src/main.rs
index 8f41c0e2..2c33d03e 100644
--- a/examples/webhook_ping_pong_bot/src/main.rs
+++ b/examples/webhook_ping_pong_bot/src/main.rs
@@ -63,7 +63,7 @@ async fn run() {
     Dispatcher::new(Arc::clone(&bot))
         .messages_handler(|rx: DispatcherHandlerRx<Message>| {
             rx.for_each(|message| async move {
-                message.answer("pong").send().await.log_on_error().await;
+                message.answer_str("pong").await.log_on_error().await;
             })
         })
         .dispatch_with_listener(
diff --git a/src/dispatching/dialogue/dialogue_dispatcher.rs b/src/dispatching/dialogue/dialogue_dispatcher.rs
index 0181a564..385545c2 100644
--- a/src/dispatching/dialogue/dialogue_dispatcher.rs
+++ b/src/dispatching/dialogue/dialogue_dispatcher.rs
@@ -1,9 +1,9 @@
 use crate::dispatching::{
     dialogue::{
-        DialogueDispatcherHandler, DialogueDispatcherHandlerCx, DialogueStage,
-        GetChatId, InMemStorage, Storage,
+        DialogueDispatcherHandler, DialogueStage, DialogueWithCx, GetChatId,
+        InMemStorage, Storage,
     },
-    DispatcherHandler, DispatcherHandlerCx,
+    DispatcherHandler, UpdateWithCx,
 };
 use std::{convert::Infallible, marker::PhantomData};
 
@@ -34,7 +34,7 @@ pub struct DialogueDispatcher<D, S, H, Upd> {
     /// A value is the TX part of an unbounded asynchronous MPSC channel. A
     /// handler that executes updates from the same chat ID sequentially
     /// handles the RX part.
-    senders: Arc<Map<i64, mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>,
+    senders: Arc<Map<i64, mpsc::UnboundedSender<UpdateWithCx<Upd>>>>,
 }
 
 impl<D, H, Upd> DialogueDispatcher<D, InMemStorage<D>, H, Upd>
@@ -78,14 +78,14 @@ where
     }
 
     #[must_use]
-    fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCx<Upd>> {
+    fn new_tx(&self) -> mpsc::UnboundedSender<UpdateWithCx<Upd>> {
         let (tx, rx) = mpsc::unbounded_channel();
 
         let storage = Arc::clone(&self.storage);
         let handler = Arc::clone(&self.handler);
         let senders = Arc::clone(&self.senders);
 
-        tokio::spawn(rx.for_each(move |cx: DispatcherHandlerCx<Upd>| {
+        tokio::spawn(rx.for_each(move |cx: UpdateWithCx<Upd>| {
             let storage = Arc::clone(&storage);
             let handler = Arc::clone(&handler);
             let senders = Arc::clone(&senders);
@@ -98,14 +98,7 @@ where
                     .await
                     .map(Option::unwrap_or_default);
 
-                match handler
-                    .handle(DialogueDispatcherHandlerCx {
-                        bot: cx.bot,
-                        update: cx.update,
-                        dialogue,
-                    })
-                    .await
-                {
+                match handler.handle(DialogueWithCx { cx, dialogue }).await {
                     DialogueStage::Next(new_dialogue) => {
                         if let Ok(Some(_)) =
                             storage.update_dialogue(chat_id, new_dialogue).await
@@ -144,10 +137,10 @@ where
 {
     fn handle(
         self,
-        updates: mpsc::UnboundedReceiver<DispatcherHandlerCx<Upd>>,
+        updates: mpsc::UnboundedReceiver<UpdateWithCx<Upd>>,
     ) -> BoxFuture<'static, ()>
     where
-        DispatcherHandlerCx<Upd>: 'static,
+        UpdateWithCx<Upd>: 'static,
     {
         let this = Arc::new(self);
 
@@ -221,10 +214,10 @@ mod tests {
         }
 
         let dispatcher = DialogueDispatcher::new(
-            |cx: DialogueDispatcherHandlerCx<MyUpdate, (), Infallible>| async move {
+            |cx: DialogueWithCx<MyUpdate, (), Infallible>| async move {
                 delay_for(Duration::from_millis(300)).await;
 
-                match cx.update {
+                match cx.cx.update {
                     MyUpdate { chat_id: 1, unique_number } => {
                         SEQ1.lock().await.push(unique_number);
                     }
@@ -266,11 +259,11 @@ mod tests {
                 MyUpdate::new(3, 1611),
             ]
             .into_iter()
-            .map(|update| DispatcherHandlerCx {
+            .map(|update| UpdateWithCx {
                 update,
                 bot: Bot::new("Doesn't matter here"),
             })
-            .collect::<Vec<DispatcherHandlerCx<MyUpdate>>>(),
+            .collect::<Vec<UpdateWithCx<MyUpdate>>>(),
         );
 
         let (tx, rx) = mpsc::unbounded_channel();
diff --git a/src/dispatching/dialogue/dialogue_dispatcher_handler.rs b/src/dispatching/dialogue/dialogue_dispatcher_handler.rs
index 69743621..111febcd 100644
--- a/src/dispatching/dialogue/dialogue_dispatcher_handler.rs
+++ b/src/dispatching/dialogue/dialogue_dispatcher_handler.rs
@@ -1,4 +1,4 @@
-use crate::prelude::{DialogueDispatcherHandlerCx, DialogueStage};
+use crate::prelude::{DialogueStage, DialogueWithCx};
 use futures::future::BoxFuture;
 use std::{future::Future, sync::Arc};
 
@@ -12,26 +12,23 @@ pub trait DialogueDispatcherHandler<Upd, D, E> {
     #[must_use]
     fn handle(
         self: Arc<Self>,
-        cx: DialogueDispatcherHandlerCx<Upd, D, E>,
+        cx: DialogueWithCx<Upd, D, E>,
     ) -> BoxFuture<'static, DialogueStage<D>>
     where
-        DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static;
+        DialogueWithCx<Upd, D, E>: Send + 'static;
 }
 
 impl<Upd, D, E, F, Fut> DialogueDispatcherHandler<Upd, D, E> for F
 where
-    F: Fn(DialogueDispatcherHandlerCx<Upd, D, E>) -> Fut
-        + Send
-        + Sync
-        + 'static,
+    F: Fn(DialogueWithCx<Upd, D, E>) -> Fut + Send + Sync + 'static,
     Fut: Future<Output = DialogueStage<D>> + Send + 'static,
 {
     fn handle(
         self: Arc<Self>,
-        cx: DialogueDispatcherHandlerCx<Upd, D, E>,
+        cx: DialogueWithCx<Upd, D, E>,
     ) -> BoxFuture<'static, Fut::Output>
     where
-        DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static,
+        DialogueWithCx<Upd, D, E>: Send + 'static,
     {
         Box::pin(async move { self(cx).await })
     }
diff --git a/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs b/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs
deleted file mode 100644
index 7c76db74..00000000
--- a/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs
+++ /dev/null
@@ -1,186 +0,0 @@
-use crate::{
-    dispatching::dialogue::GetChatId,
-    requests::{
-        DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
-        PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
-        SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
-        SendVenue, SendVideo, SendVideoNote, SendVoice,
-    },
-    types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
-    Bot,
-};
-use std::sync::Arc;
-
-/// A context of a [`DialogueDispatcher`]'s message handler.
-///
-/// See [the module-level documentation for the design
-/// overview](crate::dispatching::dialogue).
-///
-/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
-#[derive(Debug)]
-pub struct DialogueDispatcherHandlerCx<Upd, D, E> {
-    pub bot: Arc<Bot>,
-    pub update: Upd,
-    pub dialogue: Result<D, E>,
-}
-
-impl<Upd, D, E> DialogueDispatcherHandlerCx<Upd, D, E> {
-    /// Creates a new instance with the provided fields.
-    pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
-        Self { bot, update, dialogue: Ok(dialogue) }
-    }
-
-    /// Creates a new instance by substituting a dialogue and preserving
-    /// `self.bot` and `self.update`.
-    pub fn with_new_dialogue<Nd, Ne>(
-        self,
-        new_dialogue: Result<Nd, Ne>,
-    ) -> DialogueDispatcherHandlerCx<Upd, Nd, Ne> {
-        DialogueDispatcherHandlerCx {
-            bot: self.bot,
-            update: self.update,
-            dialogue: new_dialogue,
-        }
-    }
-}
-
-impl<Upd, D, E> GetChatId for DialogueDispatcherHandlerCx<Upd, D, E>
-where
-    Upd: GetChatId,
-{
-    fn chat_id(&self) -> i64 {
-        self.update.chat_id()
-    }
-}
-
-impl<D, E> DialogueDispatcherHandlerCx<Message, D, E> {
-    pub fn answer<T>(&self, text: T) -> SendMessage
-    where
-        T: Into<String>,
-    {
-        self.bot.send_message(self.chat_id(), text)
-    }
-
-    pub fn reply_to<T>(&self, text: T) -> SendMessage
-    where
-        T: Into<String>,
-    {
-        self.bot
-            .send_message(self.chat_id(), text)
-            .reply_to_message_id(self.update.id)
-    }
-
-    pub fn answer_photo(&self, photo: InputFile) -> SendPhoto {
-        self.bot.send_photo(self.update.chat.id, photo)
-    }
-
-    pub fn answer_audio(&self, audio: InputFile) -> SendAudio {
-        self.bot.send_audio(self.update.chat.id, audio)
-    }
-
-    pub fn answer_animation(&self, animation: InputFile) -> SendAnimation {
-        self.bot.send_animation(self.update.chat.id, animation)
-    }
-
-    pub fn answer_document(&self, document: InputFile) -> SendDocument {
-        self.bot.send_document(self.update.chat.id, document)
-    }
-
-    pub fn answer_video(&self, video: InputFile) -> SendVideo {
-        self.bot.send_video(self.update.chat.id, video)
-    }
-
-    pub fn answer_voice(&self, voice: InputFile) -> SendVoice {
-        self.bot.send_voice(self.update.chat.id, voice)
-    }
-
-    pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup
-    where
-        T: Into<Vec<InputMedia>>,
-    {
-        self.bot.send_media_group(self.update.chat.id, media_group)
-    }
-
-    pub fn answer_location(
-        &self,
-        latitude: f32,
-        longitude: f32,
-    ) -> SendLocation {
-        self.bot.send_location(self.update.chat.id, latitude, longitude)
-    }
-
-    pub fn answer_venue<T, U>(
-        &self,
-        latitude: f32,
-        longitude: f32,
-        title: T,
-        address: U,
-    ) -> SendVenue
-    where
-        T: Into<String>,
-        U: Into<String>,
-    {
-        self.bot.send_venue(
-            self.update.chat.id,
-            latitude,
-            longitude,
-            title,
-            address,
-        )
-    }
-
-    pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote {
-        self.bot.send_video_note(self.update.chat.id, video_note)
-    }
-
-    pub fn answer_contact<T, U>(
-        &self,
-        phone_number: T,
-        first_name: U,
-    ) -> SendContact
-    where
-        T: Into<String>,
-        U: Into<String>,
-    {
-        self.bot.send_contact(self.chat_id(), phone_number, first_name)
-    }
-
-    pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
-        self.bot.send_sticker(self.update.chat.id, sticker)
-    }
-
-    pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage
-    where
-        T: Into<ChatId>,
-    {
-        self.bot.forward_message(chat_id, self.update.chat.id, self.update.id)
-    }
-
-    pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
-    where
-        T: Into<String>,
-    {
-        self.bot.edit_message_text(
-            ChatOrInlineMessage::Chat {
-                chat_id: self.update.chat.id.into(),
-                message_id: self.update.id,
-            },
-            text,
-        )
-    }
-
-    pub fn edit_message_caption(&self) -> EditMessageCaption {
-        self.bot.edit_message_caption(ChatOrInlineMessage::Chat {
-            chat_id: self.update.chat.id.into(),
-            message_id: self.update.id,
-        })
-    }
-
-    pub fn delete_message(&self) -> DeleteMessage {
-        self.bot.delete_message(self.update.chat.id, self.update.id)
-    }
-
-    pub fn pin_message(&self) -> PinChatMessage {
-        self.bot.pin_chat_message(self.update.chat.id, self.update.id)
-    }
-}
diff --git a/src/dispatching/dialogue/dialogue_stage.rs b/src/dispatching/dialogue/dialogue_stage.rs
index 70989bc9..2fd3c8e7 100644
--- a/src/dispatching/dialogue/dialogue_stage.rs
+++ b/src/dispatching/dialogue/dialogue_stage.rs
@@ -1,3 +1,6 @@
+use crate::dispatching::dialogue::TransitionOut;
+use frunk::coproduct::CoprodInjector;
+
 /// Continue or terminate a dialogue.
 ///
 /// See [the module-level documentation for the design
@@ -8,18 +11,29 @@ pub enum DialogueStage<D> {
     Exit,
 }
 
-/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
-///
-/// See [the module-level documentation for the design
-/// overview](crate::dispatching::dialogue).
-pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
-    Ok(DialogueStage::Next(dialogue))
+/// A dialogue wrapper to bypass orphan rules.
+pub trait DialogueWrapper<D> {
+    fn new(dialogue: D) -> Self;
 }
 
-/// A shortcut for `Ok(DialogueStage::Exit)`.
+/// Returns a new dialogue state.
 ///
 /// See [the module-level documentation for the design
 /// overview](crate::dispatching::dialogue).
-pub fn exit<E, D>() -> Result<DialogueStage<D>, E> {
+pub fn next<Dialogue, State, Index, DWrapper>(
+    new_state: State,
+) -> TransitionOut<DWrapper>
+where
+    Dialogue: CoprodInjector<State, Index>,
+    DWrapper: DialogueWrapper<Dialogue>,
+{
+    Ok(DialogueStage::Next(DWrapper::new(Dialogue::inject(new_state))))
+}
+
+/// Exits a dialogue.
+///
+/// See [the module-level documentation for the design
+/// overview](crate::dispatching::dialogue).
+pub fn exit<DWrapper>() -> TransitionOut<DWrapper> {
     Ok(DialogueStage::Exit)
 }
diff --git a/src/dispatching/dialogue/dialogue_with_cx.rs b/src/dispatching/dialogue/dialogue_with_cx.rs
new file mode 100644
index 00000000..5ff09e88
--- /dev/null
+++ b/src/dispatching/dialogue/dialogue_with_cx.rs
@@ -0,0 +1,49 @@
+use crate::dispatching::{dialogue::GetChatId, UpdateWithCx};
+use std::fmt::Debug;
+
+/// A context of a [`DialogueDispatcher`]'s message handler.
+///
+/// See [the module-level documentation for the design
+/// overview](crate::dispatching::dialogue).
+///
+/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
+#[derive(Debug)]
+pub struct DialogueWithCx<Upd, D, E> {
+    pub cx: UpdateWithCx<Upd>,
+    pub dialogue: Result<D, E>,
+}
+
+impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
+    /// Returns the inner `UpdateWithCx<Upd>` and an unwrapped dialogue.
+    pub fn unpack(self) -> (UpdateWithCx<Upd>, D)
+    where
+        E: Debug,
+    {
+        (self.cx, self.dialogue.unwrap())
+    }
+}
+
+impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
+    /// Creates a new instance with the provided fields.
+    pub fn new(cx: UpdateWithCx<Upd>, dialogue: D) -> Self {
+        Self { cx, dialogue: Ok(dialogue) }
+    }
+
+    /// Creates a new instance by substituting a dialogue and preserving
+    /// `self.bot` and `self.update`.
+    pub fn with_new_dialogue<Nd, Ne>(
+        self,
+        new_dialogue: Result<Nd, Ne>,
+    ) -> DialogueWithCx<Upd, Nd, Ne> {
+        DialogueWithCx { cx: self.cx, dialogue: new_dialogue }
+    }
+}
+
+impl<Upd, D, E> GetChatId for DialogueWithCx<Upd, D, E>
+where
+    Upd: GetChatId,
+{
+    fn chat_id(&self) -> i64 {
+        self.cx.update.chat_id()
+    }
+}
diff --git a/src/dispatching/dialogue/mod.rs b/src/dispatching/dialogue/mod.rs
index 092b8d6a..fd770d5e 100644
--- a/src/dispatching/dialogue/mod.rs
+++ b/src/dispatching/dialogue/mod.rs
@@ -36,22 +36,195 @@
 //! [`Dispatcher::messages_handler`]:
 //! crate::dispatching::Dispatcher::messages_handler
 //! [`UpdateKind::Message(message)`]: crate::types::UpdateKind::Message
-//! [`DialogueDispatcherHandlerCx<YourUpdate, D>`]:
-//! crate::dispatching::dialogue::DialogueDispatcherHandlerCx
+//! [`DialogueWithCx<YourUpdate, D>`]:
+//! crate::dispatching::dialogue::DialogueWithCx
 //! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot
 
 #![allow(clippy::type_complexity)]
 
 mod dialogue_dispatcher;
 mod dialogue_dispatcher_handler;
-mod dialogue_dispatcher_handler_cx;
 mod dialogue_stage;
+mod dialogue_with_cx;
 mod get_chat_id;
 mod storage;
 
+use crate::{requests::ResponseResult, types::Message};
 pub use dialogue_dispatcher::DialogueDispatcher;
 pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
-pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
-pub use dialogue_stage::{exit, next, DialogueStage};
+pub use dialogue_stage::{exit, next, DialogueStage, DialogueWrapper};
+pub use dialogue_with_cx::DialogueWithCx;
 pub use get_chat_id::GetChatId;
 pub use storage::{InMemStorage, Storage};
+
+/// Dispatches a dialogue state into transition functions.
+///
+/// # Example
+/// ```no_run
+/// use teloxide::prelude::*;
+///
+/// pub struct StartState;
+/// pub struct ReceiveWordState;
+/// pub struct ReceiveNumberState;
+/// pub struct ExitState;
+///
+/// pub type Dialogue = Coprod!(
+///     StartState,
+///     ReceiveWordState,
+///     ReceiveNumberState,
+/// );
+///
+/// wrap_dialogue!(
+///     Wrapper(Dialogue),
+///     default Self(Dialogue::inject(StartState)),
+/// );
+///
+/// pub type In<State> = TransitionIn<State, std::convert::Infallible>;
+/// pub type Out = TransitionOut<Wrapper>;
+///
+/// pub async fn start(cx: In<StartState>) -> Out { todo!() }
+/// pub async fn receive_word(cx: In<ReceiveWordState>) -> Out { todo!() }
+/// pub async fn receive_number(cx: In<ReceiveNumberState>) -> Out { todo!() }
+///
+/// # #[tokio::main]
+/// # async fn main() {
+/// let cx: In<Dialogue> = todo!();
+/// let (cx, dialogue) = cx.unpack();
+///
+/// // StartState -> start
+/// // ReceiveWordState -> receive_word
+/// // ReceiveNumberState -> receive_number
+/// let stage = dispatch!(
+///     [cx, dialogue] ->
+///     [start, receive_word, receive_number]
+/// );
+/// # }
+/// ```
+#[macro_export]
+macro_rules! dispatch {
+    ([$cx:ident, $dialogue:ident] -> [$transition:ident, $($transitions:ident),+]) => {
+        match $dialogue {
+            Coproduct::Inl(state) => {
+                $transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
+            }
+            Coproduct::Inr(another) => { dispatch!([$cx, another] -> [$($transitions),+]) }
+        }
+    };
+
+    ([$cx:ident, $dialogue:ident] -> [$transition:ident]) => {
+        match $dialogue {
+            Coproduct::Inl(state) => {
+                $transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
+            }
+            Coproduct::Inr(_absurd) => unreachable!(),
+        }
+    };
+}
+
+/// Generates a dialogue wrapper and implements `Default` for it.
+///
+/// The reason is to bypass orphan rules to be able to pass a user-defined
+/// dialogue into [`DialogueDispatcher`]. Since a dialogue is
+/// [`frunk::Coproduct`], we cannot directly satisfy the `D: Default`
+/// constraint.
+///
+/// # Examples
+/// ```
+/// use teloxide::prelude::*;
+///
+/// struct StartState;
+/// struct ReceiveWordState;
+/// struct ReceiveNumberState;
+/// struct ExitState;
+///
+/// type Dialogue = Coprod!(
+///     StartState,
+///     ReceiveWordState,
+///     ReceiveNumberState,
+/// );
+///
+/// wrap_dialogue!(
+///     Wrapper(Dialogue),
+///     default Self(Dialogue::inject(StartState)),
+/// );
+///
+/// let start_state = Wrapper::default();
+/// ```
+///
+/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
+/// [`frunk::Coproduct`]: https://docs.rs/frunk/0.3.1/frunk/coproduct/enum.Coproduct.html
+#[macro_export]
+macro_rules! wrap_dialogue {
+    ($name:ident($dialogue:ident), default $default_block:expr, ) => {
+        pub struct $name(pub $dialogue);
+
+        impl teloxide::dispatching::dialogue::DialogueWrapper<$dialogue>
+            for $name
+        {
+            fn new(d: $dialogue) -> Wrapper {
+                $name(d)
+            }
+        }
+
+        impl Default for $name {
+            fn default() -> $name {
+                $default_block
+            }
+        }
+    };
+}
+
+/// Generates `.up(field)` methods for dialogue states.
+///
+/// Given inductively defined states, this macro generates `.up(field)` methods
+/// from `Sn` to `Sn+1`.
+///
+/// # Examples
+/// ```
+/// use teloxide::prelude::*;
+///
+/// struct StartState;
+///
+/// struct ReceiveWordState {
+///     rest: StartState,
+/// }
+///
+/// struct ReceiveNumberState {
+///     rest: ReceiveWordState,
+///     word: String,
+/// }
+///
+/// struct ExitState {
+///     rest: ReceiveNumberState,
+///     number: i32,
+/// }
+///
+/// up!(
+///     StartState -> ReceiveWordState,
+///     ReceiveWordState + [word: String] -> ReceiveNumberState,
+///     ReceiveNumberState + [number: i32] -> ExitState,
+/// );
+///
+/// let start_state = StartState;
+/// let receive_word_state = start_state.up();
+/// let receive_number_state = receive_word_state.up("Hello".to_owned());
+/// let exit_state = receive_number_state.up(123);
+/// ```
+#[macro_export]
+macro_rules! up {
+    ( $( $from:ident $(+ [$field_name:ident : $field_type:ty])? -> $to:ident ),+, ) => {
+        $(
+            impl $from {
+                pub fn up(self, $( $field_name: $field_type )?) -> $to {
+                    $to { rest: self, $($field_name)? }
+                }
+            }
+        )+
+    };
+}
+
+/// A type passed into a FSM transition function.
+pub type TransitionIn<State, E> = DialogueWithCx<Message, State, E>;
+
+// A type returned from a FSM transition function.
+pub type TransitionOut<DWrapper> = ResponseResult<DialogueStage<DWrapper>>;
diff --git a/src/dispatching/dispatcher.rs b/src/dispatching/dispatcher.rs
index 8390a623..1a7fdad4 100644
--- a/src/dispatching/dispatcher.rs
+++ b/src/dispatching/dispatcher.rs
@@ -1,7 +1,7 @@
 use crate::{
     dispatching::{
         update_listeners, update_listeners::UpdateListener, DispatcherHandler,
-        DispatcherHandlerCx,
+        UpdateWithCx,
     },
     error_handlers::{ErrorHandler, LoggingErrorHandler},
     types::{
@@ -14,7 +14,7 @@ use futures::StreamExt;
 use std::{fmt::Debug, sync::Arc};
 use tokio::sync::mpsc;
 
-type Tx<Upd> = Option<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>;
+type Tx<Upd> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd>>>;
 
 #[macro_use]
 mod macros {
@@ -36,7 +36,7 @@ fn send<'a, Upd>(
 {
     if let Some(tx) = tx {
         if let Err(error) =
-            tx.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
+            tx.send(UpdateWithCx { bot: Arc::clone(&bot), update })
         {
             log::error!(
                 "The RX part of the {} channel is closed, but an update is \
diff --git a/src/dispatching/dispatcher_handler.rs b/src/dispatching/dispatcher_handler.rs
index a1ae2c55..a00f2479 100644
--- a/src/dispatching/dispatcher_handler.rs
+++ b/src/dispatching/dispatcher_handler.rs
@@ -1,6 +1,6 @@
 use std::future::Future;
 
-use crate::dispatching::{DispatcherHandlerCx, DispatcherHandlerRx};
+use crate::dispatching::{DispatcherHandlerRx, UpdateWithCx};
 use futures::future::BoxFuture;
 
 /// An asynchronous handler of a stream of updates used in [`Dispatcher`].
@@ -16,7 +16,7 @@ pub trait DispatcherHandler<Upd> {
         updates: DispatcherHandlerRx<Upd>,
     ) -> BoxFuture<'static, ()>
     where
-        DispatcherHandlerCx<Upd>: Send + 'static;
+        UpdateWithCx<Upd>: Send + 'static;
 }
 
 impl<Upd, F, Fut> DispatcherHandler<Upd> for F
@@ -26,7 +26,7 @@ where
 {
     fn handle(self, updates: DispatcherHandlerRx<Upd>) -> BoxFuture<'static, ()>
     where
-        DispatcherHandlerCx<Upd>: Send + 'static,
+        UpdateWithCx<Upd>: Send + 'static,
     {
         Box::pin(async move { self(updates).await })
     }
diff --git a/src/dispatching/dispatcher_handler_rx_ext.rs b/src/dispatching/dispatcher_handler_rx_ext.rs
index e0aa607d..99d567ae 100644
--- a/src/dispatching/dispatcher_handler_rx_ext.rs
+++ b/src/dispatching/dispatcher_handler_rx_ext.rs
@@ -1,5 +1,5 @@
 use crate::{
-    prelude::DispatcherHandlerCx, types::Message, utils::command::BotCommand,
+    prelude::UpdateWithCx, types::Message, utils::command::BotCommand,
 };
 use futures::{stream::BoxStream, Stream, StreamExt};
 
@@ -10,18 +10,18 @@ pub trait DispatcherHandlerRxExt {
     /// Extracts only text messages from this stream of arbitrary messages.
     fn text_messages(
         self,
-    ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
+    ) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
     where
-        Self: Stream<Item = DispatcherHandlerCx<Message>>;
+        Self: Stream<Item = UpdateWithCx<Message>>;
 
     /// Extracts only commands with their arguments from this stream of
     /// arbitrary messages.
     fn commands<C, N>(
         self,
         bot_name: N,
-    ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
+    ) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
     where
-        Self: Stream<Item = DispatcherHandlerCx<Message>>,
+        Self: Stream<Item = UpdateWithCx<Message>>,
         C: BotCommand,
         N: Into<String> + Send;
 }
@@ -32,9 +32,9 @@ where
 {
     fn text_messages(
         self,
-    ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
+    ) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
     where
-        Self: Stream<Item = DispatcherHandlerCx<Message>>,
+        Self: Stream<Item = UpdateWithCx<Message>>,
     {
         Box::pin(self.filter_map(|cx| async move {
             cx.update.text_owned().map(|text| (cx, text))
@@ -44,9 +44,9 @@ where
     fn commands<C, N>(
         self,
         bot_name: N,
-    ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
+    ) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
     where
-        Self: Stream<Item = DispatcherHandlerCx<Message>>,
+        Self: Stream<Item = UpdateWithCx<Message>>,
         C: BotCommand,
         N: Into<String> + Send,
     {
diff --git a/src/dispatching/mod.rs b/src/dispatching/mod.rs
index 20bd9485..cb257908 100644
--- a/src/dispatching/mod.rs
+++ b/src/dispatching/mod.rs
@@ -80,17 +80,17 @@
 pub mod dialogue;
 mod dispatcher;
 mod dispatcher_handler;
-mod dispatcher_handler_cx;
 mod dispatcher_handler_rx_ext;
 pub mod update_listeners;
+mod update_with_cx;
 
 pub use dispatcher::Dispatcher;
 pub use dispatcher_handler::DispatcherHandler;
-pub use dispatcher_handler_cx::DispatcherHandlerCx;
 pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
 use tokio::sync::mpsc::UnboundedReceiver;
+pub use update_with_cx::UpdateWithCx;
 
 /// A type of a stream, consumed by [`Dispatcher`]'s handlers.
 ///
 /// [`Dispatcher`]: crate::dispatching::Dispatcher
-pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<DispatcherHandlerCx<Upd>>;
+pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<UpdateWithCx<Upd>>;
diff --git a/src/dispatching/dispatcher_handler_cx.rs b/src/dispatching/update_with_cx.rs
similarity index 89%
rename from src/dispatching/dispatcher_handler_cx.rs
rename to src/dispatching/update_with_cx.rs
index 3431892f..2292d4d9 100644
--- a/src/dispatching/dispatcher_handler_cx.rs
+++ b/src/dispatching/update_with_cx.rs
@@ -2,9 +2,9 @@ use crate::{
     dispatching::dialogue::GetChatId,
     requests::{
         DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
-        PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
-        SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
-        SendVenue, SendVideo, SendVideoNote, SendVoice,
+        PinChatMessage, Request, ResponseResult, SendAnimation, SendAudio,
+        SendContact, SendDocument, SendLocation, SendMediaGroup, SendMessage,
+        SendPhoto, SendSticker, SendVenue, SendVideo, SendVideoNote, SendVoice,
     },
     types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
     Bot,
@@ -18,12 +18,12 @@ use std::sync::Arc;
 ///
 /// [`Dispatcher`]: crate::dispatching::Dispatcher
 #[derive(Debug)]
-pub struct DispatcherHandlerCx<Upd> {
+pub struct UpdateWithCx<Upd> {
     pub bot: Arc<Bot>,
     pub update: Upd,
 }
 
-impl<Upd> GetChatId for DispatcherHandlerCx<Upd>
+impl<Upd> GetChatId for UpdateWithCx<Upd>
 where
     Upd: GetChatId,
 {
@@ -32,7 +32,14 @@ where
     }
 }
 
-impl DispatcherHandlerCx<Message> {
+impl UpdateWithCx<Message> {
+    pub async fn answer_str<T>(&self, text: T) -> ResponseResult<Message>
+    where
+        T: Into<String>,
+    {
+        self.answer(text).send().await
+    }
+
     pub fn answer<T>(&self, text: T) -> SendMessage
     where
         T: Into<String>,
diff --git a/src/prelude.rs b/src/prelude.rs
index 318234dc..43e29238 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -1,20 +1,21 @@
 //! Commonly used items.
 
 pub use crate::{
+    dispatch,
     dispatching::{
         dialogue::{
-            exit, next, DialogueDispatcher, DialogueDispatcherHandlerCx,
-            DialogueStage, GetChatId,
+            exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
+            DialogueWrapper, GetChatId, TransitionIn, TransitionOut,
         },
-        Dispatcher, DispatcherHandlerCx, DispatcherHandlerRx,
-        DispatcherHandlerRxExt,
+        Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
     },
     error_handlers::{LoggingErrorHandler, OnError},
     requests::{Request, ResponseResult},
     types::{Message, Update},
-    Bot, RequestError,
+    up, wrap_dialogue, Bot, RequestError,
 };
 
+pub use frunk::{Coprod, Coproduct};
 pub use tokio::sync::mpsc::UnboundedReceiver;
 
 pub use futures::StreamExt;