# Sending Messages
This guide covers the ExGram DSL for building and sending responses to your users.
## Understanding the DSL Philosophy
The ExGram DSL uses a **builder pattern**. DSL functions **build up** a list of actions on the context object. You **must return the context** from your handler, and ExGram will **execute all actions in order**.
### How It Works
```elixir
def handle({:command, "start", _msg}, context) do
context
|> answer("Welcome!") # Action 1: queued
|> answer("Here's a menu:") # Action 2: queued
|> answer_photo(photo_id) # Action 3: queued
end
# After handler returns, ExGram executes: action 1 → action 2 → action 3
```
**Key points:**
- DSL functions **build** actions, they don't execute immediately
- You **must return** the context for actions to execute
- Actions execute **in order** after your handler completes
- This is perfect for common/basic bot logic
### Wrong patterns
#### Not carrying context updates
This won't work as expected, Elixir it's immutable, so the updated context need to be passed to the next actions all the way to the end.
```elixir
def handle({:command, "start", _msg}, context) do
answer(context, "Welcome!") # ❌ This will never be sent!!
answer(context, "Here's a menu:") # ❌ This will never be sent!!
answer_photo(context, photo_id)
end
```
#### Doing actions, and then other things
There are two common got-chas.
The first one is, queueing actions, but not returning the context, this will make the actions to not be executed at all.
```elixir
def handle({:command, "start", msg}, context) do
answer(context, "Welcome!") # ❌ This will never be sent!!
MyBot.update_user_stats(extract_user(msg))
end
# Correct:
def handle({:command, "start", msg}, context) do
MyBot.update_user_stats(extract_user(msg))
answer(context, "Welcome!")
end
```
The second common mistake is the order if you mix DSL and non DSL:
```elixir
def handle({:command, "start", msg}, context) do
context = answer(context, "Welcome!")
# ❌ This will be sent BEFORE the "Welcome!" message, because the DSL actions are enqueued and executed AFTER the handle/2 method
ExGram.send_photo(extract_chat_id(msg), photo_id, bot: context.name)
context
end
# Correct:
def handle({:command, "start", msg}, context) do
chat_id = extract_id(msg)
# Using on_result allow you to do actions after the previous action
context
|> answer("Welcome!")
|> on_result(fn
{:ok, _}, name ->
ExGram.send_photo(chat_id, photo_id, bot: name)
error, _name ->
error
end)
end
```
### When NOT to Use the DSL
The DSL is really powerful and helps to make the bot's logic easier to follow, but there are cases where you will need to use the [Low-Level API](./low-level-api.md), for example:
- There are still no DSL action for the method you want. The DSL has been created as needed, so many methods still don't have a DSL created. Feel free to open an issue or a pull request 😄
- For complex bots with **background tasks**, **scheduled jobs**, or operations outside of handlers, in this cases you can't use the DSL at all.
```elixir
# In a background task or GenServer
def send_notification(user_id) do
# Use Low-Level API directly
ExGram.send_message(user_id, "Scheduled notification!", bot: :my_bot)
end
```
Read more about the Low-Level API in [this guide](./low-level-api.md)
## Sending Text Messages
### `answer/2-4`
Send a text message to the current chat.
```elixir
# Simple text
def handle({:command, "hello", _}, context) do
answer(context, "Hello there!")
end
# With options
def handle({:command, "secret", _}, context) do
answer(context, "🤫 Secret message", parse_mode: "Markdown", disable_notification: true)
end
# Multi-line
def handle({:command, "help", _}, context) do
answer(context, """
Available commands:
/start - Start the bot
/help - Show this help
/settings - Configure settings
""")
end
```
**Options:** All the `ExGram.send_message/3` options, you can see them in [the documentation](https://hexdocs.pm/ex_gram/ExGram.html#send_message/3)
### Multiple Messages
Chain multiple `answer` calls to send several messages:
```elixir
def handle({:command, "story", _}, context) do
context
|> answer("Once upon a time...")
|> answer("There was a bot...")
|> answer("The end!")
end
```
## Sending Media
All the fields that are files, will support three ways of sending that file:
- String: This is a file_id previously received in Telegram responses or messages.
- `{:file, "/path/to/file"}`: This will read the file and send it
- `{:file_content, "content", "filename.jpg"}`: Will send the "content" directly. It can be a `String.t`, `iodata()` or a `Enum.t()`, useful for streaming data directly without loading everything in memory.
For now only `answer_document` has a DSL method, we'll add more DSL for sending media files
```elixir
# Documents
answer_document(context, {:file, "/path/to/document.txt"}, opts \\ [])
```
## Keyboards
Create interactive buttons that users can press.
### Inline Keyboard
There is a neat DSL to create keyboards!
```elixir
import ExGram.Dsl.Keyboard # It is not added by default, you have to import it
def handle({:command, "choose", _}, context) do
markup =
keyboard :inline do
row do
inline_button "Option A", callback_data: "option_a"
inline_button "Option B", callback_data: "option_b"
end
row do
inline_button "Cancel", callback_data: "cancel"
end
end
answer(context, "Choose an option:", reply_markup: markup)
end
```
The `inline_button` accepts all the options that the `ExGram.Model.InlineKeyboardButton` accepts, for example:
```elixir
inline_button "Visit website", url: "https://example.com", style: "success"
```
### Reply keyboards
These are the keyboards that pop up at the bottom of the screen. You can also create them with the DSL.
```elixir
keyboard :reply do
row do
reply_button "Help", style: "success"
reply_button "Send my location", request_location: true, style: "danger"
end
end
```
This keyboards accept more options too, check [the documentation](https://hexdocs.pm/ex_gram/ExGram.Model.ReplyKeyboardMarkup.html) for available options:
```elixir
keyboard :reply, [is_persistent: true, one_time_keyboard: true, resize_keyboard: true] do
row do
reply_button "Help", style: "success"
end
end
```
### Dynamic building
This keyboards might look static, but you can actually do things like this to dynamically build your keyboards:
```elixir
keyboard :inline do
# Returning rows will make each element it's own row
Enum.map(1..3, fn index ->
row do
button to_string(index), callback_data: "index:#{index}"
end
end)
# Returning buttons will make all the elements be the same row
Enum.map(4..6, fn index ->
# Without a row block, buttons are placed in a single row
button to_string(index), callback_data: "index:#{index}"
end)
# Optional rows and buttons
if some_thing?() do
row do
button "Some thing happens", url: "https://something.com"
end
end
end
```
### Inspecting Keyboards
Keyboard models implement the `Inspect` protocol, so when you inspect them in IEx or logs, you see a visual layout instead of a wall of struct fields:
```elixir
iex> markup = keyboard :inline do
...> row do
...> button "1", callback_data: "one"
...> button "2", callback_data: "two"
...> button "3", url: "https://example.com"
...> end
...> row do
...> button "Back", callback_data: "back"
...> button "Next", callback_data: "next"
...> end
...> end
#InlineKeyboardMarkup<
[ 1 (cb) ][ 2 (cb) ][ 3 (url) ]
[ Back (cb) ][ Next (cb) ]
>
```
Each button shows its action type in parentheses: `cb` for `callback_data`, `url` for `url`, `web_app`, `pay`, etc.
To see the actual action values, pass `verbose: true` via `custom_options`:
```elixir
iex> inspect(markup, custom_options: [verbose: true])
#InlineKeyboardMarkup<
[ 1 (cb: "one") ][ 2 (cb: "two") ][ 3 (url: "https://example.com") ]
[ Back (cb: "back") ][ Next (cb: "next") ]
>
```
Reply keyboards show their options at the top:
```elixir
iex> keyboard :reply, [resize_keyboard: true, one_time_keyboard: true] do
...> row do
...> reply_button "Help"
...> reply_button "Settings"
...> end
...> end
#ReplyKeyboardMarkup<resize: true, one_time: true,
[ Help ][ Settings ]
>
```
Individual buttons also have compact inspect output, showing only non-nil fields:
```elixir
iex> %ExGram.Model.InlineKeyboardButton{text: "OK", callback_data: "ok"}
#InlineKeyboardButton<"OK" callback_data: "ok">
iex> %ExGram.Model.KeyboardButton{text: "Share Location", request_location: true}
#KeyboardButton<"Share Location" request_location: true>
```
## Callback Queries
### `answer_callback/2-3`
Always respond to callback queries to remove the loading indicator:
```elixir
# Simple acknowledgment
def handle({:callback_query, %{data: "click"}}, context) do
answer_callback(context, "Button clicked!")
end
# Show alert (popup)
def handle({:callback_query, %{data: "alert"}}, context) do
answer_callback(context, "This is an alert!", show_alert: true)
end
# Silent acknowledgment
def handle({:callback_query, _}, context) do
answer_callback(context)
end
```
## Inline Queries
### `answer_inline_query/2-3`
Respond to inline queries (`@yourbot search term`):
```elixir
def handle({:inline_query, %{query: query}}, context) do
results = search_results(query)
|> Enum.map(fn result ->
%{
type: "article",
id: result.id,
title: result.title,
description: result.description,
input_message_content: %{
message_text: result.content
}
}
end)
answer_inline_query(context, results, cache_time: 300)
end
```
See [Telegram InlineQueryResult docs](https://core.telegram.org/bots/api#inlinequeryresult) for result types.
## Editing Messages
### `edit/2-4`
Edit a previous message:
```elixir
# In callback query handler - edits the message with the button
def handle({:callback_query, %{data: "refresh"}}, context) do
context
|> answer_callback("Refreshing...")
|> edit("Updated content at #{DateTime.utc_now()}")
end
# Edit with new markup
def handle({:callback_query, %{data: "next_page"}}, context) do
new_markup = create_inline([[%{text: "Back", callback_data: "prev_page"}]])
context
|> answer_callback()
|> edit("Page 2", reply_markup: new_markup)
end
```
### `edit_inline/2-4`
Edit inline query result messages:
```elixir
edit_inline(context, "Updated inline result")
```
### `edit_markup/2`
Update only the inline keyboard:
```elixir
def handle({:callback_query, %{data: "toggle"}}, context) do
new_markup = keyboard do
row do
button "Toggled!", callback_data: "toggle"
end
end
context
|> answer_callback()
|> edit_markup(new_markup)
end
```
## Deleting Messages
### `delete/1-3`
Delete messages:
```elixir
# Delete the message that triggered the update
def handle({:callback_query, %{data: "delete"}}, context) do
context
|> answer_callback("Deleting...")
|> delete()
end
# Delete specific message
def handle({:command, "cleanup", _}, context) do
chat_id = extract_id(context)
message_id = "some_message_id"
msg = %{chat_id: chat_id, message_id: message_id}
delete(context, msg)
end
```
## Chaining Results with `on_result/2`
Tap into the execution chain and do something with the result of the previous action.
The callback receives two parameters:
- result: `{:ok, x} | {:error, error}`
- name: The bot's name
```elixir
def handle({:command, "pin", _}, context) do
context
|> answer("Important announcement!")
|> on_result(fn
{:ok, %{message_id: msg_id}}, name ->
# Pin the message we just sent
ExGram.pin_chat_message(extract_id(context), msg_id, bot: name)
error, _name ->
error
end)
end
def handle({:command, "forward_to_admin", _}, context) do
admin_chat_id = Application.get_env(:my_app, :admin_chat_id)
context
|> answer("Message sent to admin!")
|> on_result(fn
{:ok, message}, name ->
# Forward the confirmation to admin
ExGram.forward_message(admin_chat_id, extract_id(message), extract_message_id(message), bot: name)
error, _name ->
error
end)
end
```
**Note:** `on_result/2` receives the result of the previous action. What you return will be treated as the new result of that action.
## Context Helper Functions
ExGram provides helper functions to extract information from the context:
### `extract_id/1`
Get the origin id from the update, if it's a chat, will be the chat id, if it's a private conversation will be the user id.
Used to know who to answer.
```elixir
chat_id = extract_id(context)
```
### `extract_user/1`
Get the user who triggered the update:
```elixir
%{id: user_id, username: username} = extract_user(context)
```
### `extract_chat/1`
Get the chat where the update occurred:
```elixir
chat = extract_chat(context)
```
### `extract_message_id/1`
Get the message ID:
```elixir
message_id = extract_message_id(context)
```
### `extract_callback_id/1`
Get callback query ID
```elixir
callback_id = extract_callback_id(context)
```
### `extract_update_type/1`
Get the update type:
```elixir
case extract_update_type(update) do
:message -> # ...
:callback_query -> # ...
:inline_query -> # ...
end
```
### `extract_message_type/1`
Get the message type:
```elixir
case extract_message_type(message) do
:text -> # ...
:photo -> # ...
:document -> # ...
end
```
### Other Helpers
```elixir
extract_response_id(context) # Get response ID for editing
extract_inline_id_params(context) # Get inline message params
```
## Complete Example
Here's a bot that demonstrates multiple DSL features:
```elixir
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot, setup_commands: true
import ExGram.Dsl.Keyboard
command("start", description: "Start")
command("menu", description: "Show menu")
command("info", description: "Information")
def handle({:command, :start, _}, context) do
user = extract_user(context)
context
|> answer("Welcome, #{user.first_name}!")
|> answer("I'm here to help you. Use /menu to see options.")
end
def handle({:command, :menu, _}, context) do
markup = keyboard :inline do
row do
button "📊 Stats", callback_data: "stats"
button "⚙️ Settings", callback_data: "settings"
end
row do
button "ℹ️ Info", callback_data: "info"
button "❌ Close", callback_data: "close"
end
end
answer(context, "Main Menu:", reply_markup: markup)
end
def handle({:callback_query, %{data: "stats"}}, context) do
user = extract_user(context)
stats = get_user_stats(user.id)
context
|> answer_callback()
|> edit("📊 Your Stats:\n\nMessages: #{stats.messages}\nCommands: #{stats.commands}")
end
def handle({:callback_query, %{data: "close"}}, context) do
context
|> answer_callback("Closing menu")
|> delete()
end
defp get_user_stats(user_id) do
# Fetch from database
%{messages: 42, commands: 15}
end
end
```
## Next Steps
- [Message Entities](message-entities.md) - Format messages without Markdown or HTML
- [Middlewares](middlewares.md) - Add preprocessing logic
- [Low-Level API](low-level-api.md) - Direct API calls for complex scenarios
- [Cheatsheet](cheatsheet.md) - Quick reference for all DSL functions