Bot Init hooks allow you to run custom code during the bot's startup sequence, before it begins processing updates. They are the right place for initialization that depends on the bot's token or identity - fetching external configuration, pre-warming caches, validating credentials, or anything else that must succeed before the bot can function.

Overview

A Bot Init hook is a module that implements the ExGram.BotInit behaviour. The single callback on_bot_init/1 is called once during startup. Each hook can pass data forward to subsequent hooks and to every handler call via context.extra.

Hooks run in this sequence during startup:

  1. ExGram.BotInit.GetMe (built-in, enabled by default)
  2. ExGram.BotInit.SetupCommands (built-in, enabled when setup_commands: true)
  3. Your custom hooks (in declaration order)
  4. init/1 callback on the bot module
  5. Bot starts receiving updates

If any hook returns {:error, reason}, the bot supervisor shuts down cleanly with reason {:on_bot_init_failed, module, reason}.

Declaring Hooks

Use the on_bot_init/1-2 macro inside your bot module:

defmodule MyApp.Bot do
  use ExGram.Bot, name: :my_bot

  on_bot_init(MyApp.ConfigHook)
  on_bot_init(MyApp.CacheWarmHook, ttl: 300)

  command("start")

  def handle({:command, :start, _}, context) do
    answer(context, "Hello! Config: #{context.extra[:app_config]}")
  end
end

Implementing a Hook

Implement the ExGram.BotInit behaviour with a single callback:

defmodule MyApp.ConfigHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    token = opts[:token]
    bot = opts[:bot]
    current_extra = opts[:extra_info]

    case MyApp.Config.fetch(token) do
      {:ok, config} ->
        # Return a map to merge into context.extra for all subsequent hooks and handlers
        {:ok, %{app_config: config}}

      {:error, reason} ->
        # Returning {:error, reason} stops startup and shuts down the bot
        {:error, {:config_fetch_failed, reason}}
    end
  end
end

Callback Return Values

ReturnEffect
:okHook succeeded; extra_info is unchanged
{:ok, map}Hook succeeded; map is merged into extra_info for subsequent hooks and handlers
{:error, reason}Hook failed; bot shuts down with {:on_bot_init_failed, module, reason}

Hook Options

on_bot_init/1 receives a keyword list with:

KeyTypeDescription
:botatom()The bot's registered name
:tokenString.t()The bot's token
:extra_infomap()Accumulated extra data from previous hooks
custom keysany()Any options passed to on_bot_init/2

Passing Options to Hooks

Use on_bot_init/2 to pass custom options to a hook at declaration time:

on_bot_init(MyApp.CacheWarmHook, ttl: 300, namespace: "my_bot")

The options are merged into the keyword list received by on_bot_init/1:

defmodule MyApp.CacheWarmHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    ttl = Keyword.get(opts, :ttl, 60)
    namespace = Keyword.get(opts, :namespace, "default")

    MyApp.Cache.warm(namespace, ttl: ttl)
    :ok
  end
end

Sharing Data Between Hooks

Each hook receives extra_info containing all data produced by hooks that ran before it. Return {:ok, map} to merge new data in:

defmodule MyApp.AuthHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    bot = opts[:bot]

    case MyApp.Auth.validate_bot(bot) do
      {:ok, permissions} -> {:ok, %{bot_permissions: permissions}}
      {:error, :invalid} -> {:error, :invalid_token}
    end
  end
end

defmodule MyApp.SetupHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    # Permissions were set by AuthHook, accessible via extra_info
    permissions = opts[:extra_info][:bot_permissions]

    if :admin in permissions do
      {:ok, %{admin_chat_id: MyApp.Config.admin_chat_id()}}
    else
      :ok
    end
  end
end

Accessing Hook Data in Handlers

Data added by hooks is available in every handler call via context.extra:

defmodule MyApp.Bot do
  use ExGram.Bot, name: :my_bot

  on_bot_init(MyApp.AuthHook)
  on_bot_init(MyApp.SetupHook)

  command("status")

  def handle({:command, :status, _}, context) do
    permissions = context.extra[:bot_permissions]
    answer(context, "Permissions: #{inspect(permissions)}")
  end

  def handle(_, context), do: context
end

Built-in Hooks

ExGram provides two built-in hooks that are automatically injected by the dispatcher at startup.

ExGram.BotInit.GetMe

Calls ExGram.get_me/1 to fetch the bot's identity from Telegram. Enabled by default (get_me: true).

The result, the bot's information in an ExGram.Model.User struct, is stored as state.bot_info in the dispatcher and becomes available as context.bot_info in every handler call.

Disable it when you don't need the bot's identity or want to avoid the startup API call:

{MyApp.Bot, [method: :polling, token: token, get_me: false]}

ExGram.BotInit.SetupCommands

Registers the bot's declared commands with Telegram via setMyCommands. Only runs when setup_commands: true:

use ExGram.Bot, name: :my_bot, setup_commands: true
# or at startup:
{MyApp.Bot, [method: :polling, token: token, setup_commands: true]}

Execution Order

Built-in hooks always run before custom hooks:

GetMe -> SetupCommands (if enabled) -> your custom hooks -> init/1

Error Handling

When a hook returns {:error, reason}, the dispatcher:

  1. Logs an error: ExGram: on_bot_init hook MyHook failed for bot :my_bot: reason
  2. Stops the dispatcher with reason {:shutdown, {:on_bot_init_failed, MyHook, reason}}

The bot's supervisor propagates this shutdown, which means the whole bot process tree stops. This is intentional - if a required initialization step fails, there is no safe state to operate in.

defmodule MyApp.RequiredHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    case fetch_required_config(opts[:token]) do
      {:ok, config} ->
        {:ok, %{config: config}}

      {:error, reason} ->
        # Bot will not start - logged and supervisor shuts down
        {:error, {:required_config_missing, reason}}
    end
  end
end

Testing Hooks

Hooks participate in the normal test adapter lifecycle. Use ExGram.Test.stub/2 or ExGram.Test.expect/2 to control any API calls your hook makes.

By default, ExGram.Test.start_bot/3 sets get_me: false and setup_commands: false to avoid unnecessary API calls. Your own hooks declared with on_bot_init/1-2 always run.

If you want to optionally enable/disable your init hooks, you can stop running them if a specific field exists in the extra_info map, and start your bots with that value.

defmodule MyHook do
  @behaviour ExGram.BotInit
  
  @impl ExGram.BotInit
  def on_bot_init(opts) do
    if opts[:extra_info][:my_hook_disable] do
      :ok
    else
      do_init(opts)
    end
  end
end

defmodule MyApp.BotTest do
  use ExUnit.Case, async: true
  use ExGram.Test

  import ExGram.TestHelpers

  setup context do
    {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot, extra_info: %{__my_hook_disable: true})
    {:ok, bot_name: bot_name}
  end
end

To test get_me: true or setup_commands: true startup hooks, pass them explicitly:

ExGram.Test.stub(:get_me, %{id: 1, is_bot: true, username: "my_bot"})
{bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot, get_me: true)

Next Steps