A Gleam library for the Telegram Bot API on BEAM.
- an interface to the Telegram Bot HTTP-based APIs
telega/api - a client for the Telegram Bot API
telega/client - OTP supervision tree for all bot processes (bot actor, chat instances, polling)
- adapter to use with wisp
- long polling with automatic retry and exponential backoff
- session bot implementation
- conversation implementation (multi-message flows)
- declarative single-message dialogs with widgets and sub-dialogs (
telega/dialog, guide) - per-user flood control middleware (
router.with_rate_limit) - inline mode result builders with pagination (
telega/inline_mode) - payments helpers — Telegram Stars first-class (
telega/payments) - paid media —
sendPaidMediaviaapi.send_paid_media/reply.with_paid_media - production observability via telemetry events (
telega/telemetry) - graceful shutdown via
telega.shutdown()
If you are new to Telegram bots, read the official Introduction for Developers written by the Telegram team.
First, visit @BotFather to create a new bot. Copy the token and save it for later.
Initiate a gleam project and add telega as a dependency:
$ gleam new first_tg_bot
$ cd first_tg_bot
$ gleam add telega gleam_erlang telega_httpcReplace the first_tg_bot.gleam file content with the following code:
import gleam/erlang/process
import telega
import telega/reply
import telega/router
import telega/update
import telega_httpc
fn handle_text(ctx, text) {
use ctx <- telega.log_context(ctx, "echo_text")
let assert Ok(_) = reply.with_text(ctx, text)
Ok(ctx)
}
fn handle_command(ctx, command: update.Command) {
use ctx <- telega.log_context(ctx, "echo_command")
let assert Ok(_) = reply.with_text(ctx, "Command: " <> command.text)
Ok(ctx)
}
pub fn main() {
let router = router.new("echo_bot")
|> router.on_any_text(handle_text)
|> router.on_commands(["start", "help"], handle_command)
let client =
telega_httpc.new("BOT_TOKEN")
let assert Ok(_bot) =
telega.new_for_polling(api_client: client)
|> telega.with_router(router)
|> telega.init_for_polling_nil_session()
process.sleep_forever()
}Replace "BOT_TOKEN" with the token you received from the BotFather. Then run the bot:
$ gleam runAnd it will echo all received text messages.
Congratulations! You just wrote a Telegram bot :)
Calling telega.init_for_polling() (or telega.init() for webhooks) starts an OTP supervision tree:
TelegaRootSupervisor (OneForOne)
├── ChatInstances (factory_supervisor, Transient children)
│ ├── ChatInstance {chat1:user1}
│ ├── ChatInstance {chat2:user2}
│ └── ...
├── Bot actor (Permanent)
└── Polling worker (Permanent) — only in polling mode
- Bot actor — dispatches incoming updates to the correct
ChatInstanceby{chat_id}:{from_id}key. - ChatInstance — one per user-chat combination; holds session state and conversation continuations. Transient restart strategy means it restarts only on abnormal exit and re-registers itself in the ETS registry automatically.
- Polling worker — long-polls the Telegram API with exponential backoff on errors.
Each telega.init* call creates an independent tree with its own ETS registry, so multiple bot instances don't conflict.
telega.supervised (webhook) and telega.supervised_for_polling (polling) wrap the corresponding init into a ChildSpecification, so the bot's tree becomes a child of your application's supervisor — a crashed bot is re-initialized by your tree, and ordering against the resources it needs (a database pool, caches) is expressed as child order:
import gleam/otp/static_supervisor as supervisor
let assert Ok(_) =
supervisor.new(supervisor.RestForOne)
|> supervisor.add(db_pool_child)
|> supervisor.add(
telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> telega.with_nil_session()
|> telega.supervised_for_polling(),
)
|> supervisor.startSupervisors don't hand child data back; when you need the Telega instance outside the tree (webhook adapters, manual shutdown), capture it from the with_on_start hook — see the telega.supervised documentation for the pattern.
telega.shutdown(bot)Sends an OTP shutdown signal to the root supervisor, which stops children in reverse start order (polling → bot → chat factory).
Handlers reach shared services — a database pool, an HTTP client, an i18n catalog — through the typed, non-persisted dependencies slot on Context. It is set once at init and is never serialized, unlike session (which holds per-user state):
pub type Dependencies {
Dependencies(db: Connection, catalog: Catalog)
}
telega.new_for_polling_with_dependencies(api_client: client, dependencies: Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init_for_polling()
// in any handler / flow step / middleware:
fn my_bookings(ctx: Context(Nil, String, Dependencies), _cmd) {
let bookings = db.list_bookings(ctx.dependencies.db, ctx.update.from_id)
reply.with_text(ctx, format_bookings(bookings))
}Bots that need no services pay nothing: dependencies defaults to Nil. See the Dependency injection guide.
Telega ships with a testing toolkit under telega/testing/ — mock clients, data factories, and a declarative conversation DSL. No real Telegram API calls needed.
import telega/testing/conversation
pub fn greeting_flow_test() {
conversation.conversation_test()
|> conversation.send("/start")
|> conversation.expect_reply_containing("Hello")
|> conversation.send("Alice")
|> conversation.expect_reply_containing("Alice")
|> conversation.run(build_router(), fn() { MySession(name: "") })
}See the full Testing guide for handler isolation, mock clients, media assertions, and more.
Telega is a monorepo. The core telega package is HTTP-client- and storage-agnostic; pick the adapters you need:
| Package | Purpose |
|---|---|
telega_wisp |
Wisp webhook adapter (endpoint handling, secret-token validation) |
telega_mist |
Minimal webhook adapter directly over mist, without wisp |
telega_httpc |
HTTP client adapter over Erlang httpc |
telega_hackney |
HTTP client adapter over hackney |
telega_storage_postgres |
PostgreSQL session/flow storage adapter |
telega_storage_sqlite |
SQLite session/flow storage adapter |
telega_storage_redis |
Redis/Valkey session/flow storage adapter |
telega_webapp |
Telegram Mini Apps (Web App) initData validation and helpers |
telega_i18n |
Internationalization: TOML/JSON catalogs, locale middleware, interpolation, CLDR pluralization |
Progressive examples in the examples directory:
00-echo-bot— Basic echo with long polling01-commands-bot— Command handling02-session-bot— Stateful sessions03-conversation-bot— Multi-message conversations04-keyboard-bot— Inline keyboards and callbacks05-media-group-bot— Media group handling06-restaurant-booking— Full-featured application with flows and database
gleam build # Build the project
gleam test # Run the tests
gleam format # Format code
gleam shell # Run an Erlang shell