Configure multiple bots

Copy Markdown View Source

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)
  end

With 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
end

Note

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!")
end

No 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")
end

Pro 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
end

Summary

  • DSL functions (answer, edit, etc.) → Automatically use context.name
  • Direct API calls (ExGram.send_message, etc.) → Must specify bot: 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