TelegramEx applications are built around handler modules. A handler module is a regular Elixir module that receives parsed Telegram updates and decides what to send back.
Bot Modules
A bot module starts with use TelegramEx:
defmodule MyBot do
use TelegramEx, name: :my_bot
endThe :name value is important. TelegramEx uses it to:
- read the bot token from application config
- create FSM storage for that bot
- name the polling server process
For this bot, the token is read from:
config :telegram_ex,
my_bot: "123456:token"Messages
Telegram sends ordinary user messages as message updates. TelegramEx parses
them into TelegramEx.Types.Message structs and calls handle_message/2.
def handle_message(%{text: "/start", chat: chat}, ctx) do
ctx
|> Message.text("Welcome!")
|> Message.send(chat["id"])
endHandlers are just Elixir function clauses. Pattern matching is the main routing tool:
def handle_message(%{text: "/help", chat: chat}, ctx) do
ctx
|> Message.text("Available commands: /start, /help")
|> Message.send(chat["id"])
end
def handle_message(%{text: text, chat: chat}, ctx) do
ctx
|> Message.text("You said: #{text}")
|> Message.send(chat["id"])
endClause order matters. Put specific handlers before broad handlers.
Callback Queries
Inline keyboard buttons do not arrive as messages. They arrive as callback
queries and are handled by handle_callback/2.
def handle_message(%{text: "/confirm", chat: chat}, ctx) do
keyboard = [[%{text: "Confirm", callback_data: "confirm"}]]
ctx
|> Message.text("Please confirm.")
|> Message.inline_keyboard(keyboard)
|> Message.send(chat["id"])
end
def handle_callback(%{data: "confirm"} = callback, ctx) do
ctx
|> Message.text("Confirmed.")
|> Message.answer_callback_query(callback)
|> Message.send(callback.message.chat["id"])
endMessage.answer_callback_query/2 acknowledges the button press so Telegram
clients stop showing the loading state.
Context
The second handler argument is a context map. It is created for each update and is also used by builder pipelines.
The server puts these keys into the context:
:token- bot token used by API requests:state- current FSM state for the chat, ornil:data- current FSM data for the chat, ornil:message_thread_id- present for forum topic messages
Builders add request data to the same map:
:payload- Telegram API payload under construction:chat_id- target chat:method- Telegram API method, such as"sendMessage":format-:jsonor:multipart
This is why builders compose naturally:
ctx
|> Message.text("Hello")
|> Message.silent()
|> Message.send(chat_id)Builder steps do not mutate the original map directly. They wrap the context in
TelegramEx.Effect, store payload data inside that effect, and keep passing the
effect through the pipeline. The final send/2 step turns the accumulated
context into a Telegram API request and stores any error in the effect.
Most handlers do not need to manage effects manually. Returning the builder
pipeline is enough; the server converts successful effects to :ok and failed
effects to {:error, reason}. See Effects for the detailed model.
Return Values
Most simple handlers return the effect produced by a builder pipeline:
def handle_message(%{text: "/start", chat: chat}, ctx) do
ctx
|> Message.text("Welcome!")
|> Message.send(chat["id"])
endThe server also accepts ordinary handler results.
FSM-aware handlers can return transition values:
{:transition, state}- move tostate, keep current data{:transition, state, data}- move tostate, storedata{:stay, data}- keep current state, replace data
Routers can return :pass to let the next router or the main bot module try the
update.
Dispatch Flow
For each update, TelegramEx:
- parses the raw Telegram payload into a struct
- loads FSM state and data for the chat
- builds the handler context
- tries routers in order
- tries the main bot module
- converts effects to handler results
- applies FSM transition return values
This keeps application code small: handlers describe behavior, while the server handles polling, parsing, context setup, and state persistence.