In this way we will explore different ways to configure multiple bots in the same application.
In this guide, the elixir application is called my_bot and the bot's modules will be MyBot.Bot1, MyBot.Bot2, ...
Manually
The simpler and easiest way to start different bots, is to setup in a specific configuration value the bot's configuration:
config :my_bot,
bots: [
bot_name_1: [method: :polling, token: "TOKEN_BOT_1"],
bot_name_2: [method: :polling, token: "TOKEN_BOT_2"]
]NOTE: I recommend using the same name here than the one you use in your bots when doing use ExGram.Bot, name: :bot_name_1
And now in your application.ex, manually configure the childs:
def start(_type, _args) do
bots = Application.get_env(:my_bot, :bots)
bot_config_1 = bots[:bot_name_1]
bot_config_2 = bots[:bot_name_2]
children = [
ExGram,
{MyBot.Bot1, bot_config_1},
{MyBot.Bot2, bot_config_2}
]
opts = [strategy: :one_for_one, name: MyBot.Supervisor]
Supervisor.start_link(children, opts)
endWith a Dynamic Supervisor
If you plan to have many bots, that you maybe want to be able to start/stop as you want, or to add/delete new bots easily, using a DynamicSupervisor will help you with it.
We can keep the same configuration style, just change it to have the bot's module:
config :my_bot,
bots: [
bot_name_1: [bot: MyBot.Bot1, method: :polling, token: "TOKEN_BOT_1"],
bot_name_2: [bot: MyBot.Bot2, method: :polling, token: "TOKEN_BOT_2"]
]Now we will create a bot's dynamic supervisor:
lib/my_bot/bot_supervisor.ex
defmodule MyBot.BotSupervisor do
use DynamicSupervisor
@spec start_link(any()) :: Supervisor.on_start() | :ignore
def start_link(_init_arg) do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def start_bots() do
bots = Application.get_env(:my_bot, :bots)
bots
|> Enum.with_index()
|> Enum.map(fn {{bot_name, bot}, index} ->
%{
id: index,
token: Keyword.fetch!(bot, :token),
method: Keyword.fetch!(bot, :method),
bot_name: bot_name,
extra_info: Keyword.get(bot, :extra_info, %{}),
bot: Keyword.fetch!(bot, :bot)
}
end)
|> Enum.each(&start_bot/1)
end
def start_bot(bot) do
name = String.to_atom("bot_#{bot.bot_name}_#{bot.id}")
bot_options = [
token: bot.token,
method: bot.method,
name: name,
id: name,
bot_name: bot.bot_name,
extra_info: bot.extra_info
]
child_spec = {bot[:bot], bot_options}
{:ok, _} = DynamicSupervisor.start_child(__MODULE__, child_spec)
end
endNote
NOTE: This sets the bot's name explicit (bot_name: bot.bot_name), this is done in order to allow to use the same bot module with different tokens.
But it also implies that the name in the configuration is the one that will be used, and not the one setup in use ExGram.Bot, name: <name>, it only matters if you make direct calls to ExGram like ExGram.send_message/3 with bot: :bot_name, if you don't need to release different bots with the same bot's module, I recommend deleting that line.
Using context.name for Direct API Calls
When running multiple bots, it's crucial to understand how to identify which bot received an update. This is especially important when using the low-level API directly instead of the DSL.
The DSL Handles This Automatically
The ExGram DSL (functions like answer, edit, delete) automatically uses context.name to identify the correct bot:
def handle({:command, "start", _}, context) do
# This automatically uses the correct bot
answer(context, "Hello!")
endNo additional configuration needed - the DSL knows which bot to use!
Manual API Calls Require bot: Option
When making direct ExGram method calls (outside the DSL), you must explicitly specify which bot to use. The context.name field contains the bot's name:
def handle({:command, "notify_admin", _}, context) do
admin_chat_id = get_admin_id()
user = extract_user(context)
# CORRECT: Use context.name to identify the bot
ExGram.send_message(
admin_chat_id,
"User #{user.id} triggered notify_admin",
bot: context.name # This is crucial!
)
answer(context, "Admin has been notified")
endPro tip
Never hardcode the bot's name (@name) or use MyBot.name(), always use the context.name and pass it
around if you need it, like that you will always do API calls with the correct bot
Why This Matters
Without specifying bot: context.name, ExGram will use the default token from config, which might be the wrong bot:
# ❌ WRONG: May use wrong bot's token
ExGram.send_message(admin_chat_id, "Message")
# ✅ CORRECT: Uses the bot that received the update
ExGram.send_message(admin_chat_id, "Message", bot: context.name)Example: Background Task with Multiple Bots
If you need to send messages from a background task or GenServer, store the bot name and use it:
defmodule MyApp.NotificationWorker do
use GenServer
def start_link(bot_name) do
GenServer.start_link(__MODULE__, bot_name, name: __MODULE__)
end
def init(bot_name) do
{:ok, %{bot_name: bot_name}}
end
def handle_info(:send_notification, state) do
# Use the stored bot name
ExGram.send_message(
chat_id,
"Scheduled notification",
bot: state.bot_name
)
{:noreply, state}
end
endSummary
- DSL functions (
answer,edit, etc.) → Automatically usecontext.name - Direct API calls (
ExGram.send_message, etc.) → Must specifybot: context.name - Background tasks → Store and reuse the bot name
See the Low-Level API guide for more information on direct API calls.
And finally, we just need to change our application.ex to start the supervisor and the bots:
lib/my_bot/application.ex@impl true def start(_type, _args) do children = [ ExGram, MyBot.BotSupervisor, {Task, &MyBot.BotSupervisor.start_bots/0}, # ... ] opts = [strategy: :one_for_one, name: MyBot.Supervisor] Supervisor.start_link(children, opts) end