Telegram commands are text messages that start with /, such as /start or
/echo hello. TelegramEx treats them as normal message handlers and adds a DSL
for registering command metadata with Telegram.
Why defcommand/3 Exists
Telegram has two separate concerns:
- dispatching
/startwhen the user sends a message - showing
/startin the Telegram command menu
defcommand/3 handles both:
defcommand "start", description: "Show welcome message", bind: [:ctx, :message] do
ctx
|> Message.text("Welcome!")
|> Message.send(message.chat["id"])
endThe command name is written without /. The description is required because
Telegram requires descriptions for setMyCommands.
Macro Expansion
Conceptually, this:
defcommand "start", description: "Show welcome message", bind: [:ctx, :message] do
ctx
|> Message.text("Welcome!")
|> Message.send(message.chat["id"])
endexpands into something close to:
@commands %{command: "start", description: "Show welcome message"}
def handle_message(%{text: "/start" <> _rest} = message, ctx) do
var!(ctx) = ctx
var!(message) = message
ctx
|> Message.text("Welcome!")
|> Message.send(message.chat["id"])
endIf :args is bound, the generated function also parses arguments:
args = String.split(message.text, " ") |> Enum.drop(1)
var!(args) = argsIf :command is bound, the generated function builds a
%TelegramEx.Command{} struct with the command name, description, and original
message.
Bindings
The command body only sees variables listed in :bind. This keeps macro hygiene
explicit and avoids accidental variables appearing in user code.
Available bindings:
:ctx- handler context:message- original Telegram message:args- command arguments split by spaces:command-%TelegramEx.Command{}for the matched command
Example with arguments:
defcommand "echo", description: "Echo text", bind: [:ctx, :message, :args] do
text =
case args do
[] -> "Usage: /echo hello"
args -> Enum.join(args, " ")
end
ctx
|> Message.text(text)
|> Message.send(message.chat["id"])
end/echo hello world sends hello world. The argument parser is intentionally
simple: it splits on spaces. If your command needs quoted strings, flags, or
typed arguments, parse message.text yourself or build a small parser on top.
Registration
When the bot starts, TelegramEx.Server calls:
TelegramEx.Command.register_all(token, bot_module)Registration:
- reads
__commands__/0from the bot module - reads
__commands__/0from configured routers - deduplicates by command name
- sends the list to Telegram with
setMyCommands
This only updates the Telegram client menu. It does not change dispatch.
Dispatch still happens through generated handle_message/2 clauses.
Commands in Routers
Routers can define commands too:
defmodule MyApp.CommandRouter do
use TelegramEx.Router
defcommand "status", description: "Show status", bind: [:ctx, :message] do
ctx
|> Message.text("OK")
|> Message.send(message.chat["id"])
end
endRegister the router in your bot:
defmodule MyBot do
use TelegramEx, name: :my_bot, routers: [MyApp.CommandRouter]
endCommands from routers are included in setMyCommands.
Ordering
defcommand/3 generates a regular handle_message/2 clause. Normal Elixir
clause ordering applies:
defcommand "start", description: "Start", bind: [:ctx, :message] do
# specific command
end
def handle_message(%{text: text}, ctx) do
# broad fallback
endPut command handlers before broad text handlers. If a broad clause appears
first, it can catch /start before the generated command clause runs.
Telegram Command Rules
Telegram command names should be short, lowercase command identifiers. Keep
names without /, avoid spaces, and use descriptions that explain the action in
one phrase. TelegramEx stores whatever you declare, but Telegram validates the
final setMyCommands request.