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:
ExGram.BotInit.GetMe(built-in, enabled by default)ExGram.BotInit.SetupCommands(built-in, enabled whensetup_commands: true)- Your custom hooks (in declaration order)
init/1callback on the bot module- 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
endImplementing 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
endCallback Return Values
| Return | Effect |
|---|---|
:ok | Hook 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:
| Key | Type | Description |
|---|---|---|
:bot | atom() | The bot's registered name |
:token | String.t() | The bot's token |
:extra_info | map() | Accumulated extra data from previous hooks |
| custom keys | any() | 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
endSharing 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
endAccessing 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
endBuilt-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/1Error Handling
When a hook returns {:error, reason}, the dispatcher:
- Logs an error:
ExGram: on_bot_init hook MyHook failed for bot :my_bot: reason - 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
endTesting 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
endTo 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
- Middlewares - Add preprocessing logic that runs on every update
- Testing - Test your bot and its initialization hooks
- Handling Updates - Learn about the handler patterns