Telegram conversations often need memory: the bot asks a question, waits for an answer, then asks the next question. TelegramEx stores per-chat state and data to support these flows.
State Storage
Each chat stores a {state, data} tuple:
stateis usually an atom such as:waiting_namedatacan be any Elixir term, commonly a map
FSM.set_state(:my_bot, chat_id, :waiting_name, %{step: 1})
{state, data} = FSM.get_state(:my_bot, chat_id)
FSM.reset_state(:my_bot, chat_id)The bot name identifies the ETS storage table. Use the same atom you pass to
use TelegramEx, name: ....
Starting a Flow
A normal handler can start a stateful flow by returning a transition:
def handle_message(%{text: "/survey", chat: chat}, ctx) do
ctx
|> Message.text("What is your name?")
|> Message.send(chat["id"])
{:transition, :survey_name, %{}}
endAfter this handler returns, TelegramEx stores {:survey_name, %{}} for that
chat.
State Handlers
defstate/2 makes handlers match only when ctx.state has the declared value:
defstate :survey_name do
def handle_message(%{text: name, chat: chat}, ctx) do
ctx
|> Message.text("How old are you?")
|> Message.send(chat["id"])
{:transition, :survey_age, Map.put(ctx.data, :name, name)}
end
endConceptually, the handler is rewritten to match the state in the context:
def handle_message(message, %{state: :survey_name} = ctx) do
...
endThis means state handlers are still normal function clauses. Ordering still matters when multiple clauses could match.
Complete Flow
defmodule MyBot do
use TelegramEx, name: :my_bot
def handle_message(%{text: "/survey", chat: chat}, ctx) do
ctx
|> Message.text("What is your name?")
|> Message.send(chat["id"])
{:transition, :survey_name, %{}}
end
defstate :survey_name do
def handle_message(%{text: name, chat: chat}, ctx) do
ctx
|> Message.text("How old are you?")
|> Message.send(chat["id"])
{:transition, :survey_age, Map.put(ctx.data, :name, name)}
end
end
defstate :survey_age do
def handle_message(%{text: age_text, chat: chat}, ctx) do
case Integer.parse(age_text) do
{age, ""} when age > 0 ->
ctx
|> Message.text("Saved #{ctx.data.name}, age #{age}.")
|> Message.send(chat["id"])
FSM.reset_state(:my_bot, chat["id"])
_ ->
ctx
|> Message.text("Please enter a valid age.")
|> Message.send(chat["id"])
:ok
end
end
end
endReturn Values
The server persists common FSM return values automatically:
{:transition, state}- move tostate, keep current data{:transition, state, data}- move tostate, storedata{:stay, data}- keep current state, replace data
Use FSM.reset_state/2 when the flow is complete or cancelled.
Cancellation
Long flows should usually support cancellation in every state:
defstate :survey_age do
def handle_message(%{text: "/cancel", chat: chat}, ctx) do
ctx
|> Message.text("Survey cancelled.")
|> Message.send(chat["id"])
FSM.reset_state(:my_bot, chat["id"])
end
endPut cancellation clauses before broad text clauses inside the same state.
Routers and FSM
FSM works in routers as well as the main bot module. This is often the cleanest shape for multi-step features: the main bot starts the flow, and a router owns the state-specific handlers.