Wiki
猫Table of Contents
- Bot Lifecycle
- Lifecycle at a glance
- Creating a bot
- Configuration before runtime
- AddPlugins(...) is a configuration commit point
- Configuration freeze phases
- What “ignored” means
- Minimal startup pattern
- RunWithContext(...), Run(), and RunWebhookWithContext(...)
- What happens during runtime
- Runners and shutdown semantics
- Graceful shutdown
- Close() versus CloseRemote()
- Single-use rule
- Recommended production pattern
- Common mistakes
Bot Lifecycle
Russian version: Bot-Lifecycle-RU
This page explains how a Bot is created, configured, started, stopped, and retired. The important rule is that a Bot instance is single-use: configure it fully, run it once, then create a new instance for the next session.
Lifecycle at a glance
- Build
BotOptsand callNewBot. - Configure the bot instance: prefixes, plugins, middleware, runners, localization, payload defaults, and optional app data.
- Start it with
RunWithContext(ctx),Run(), orRunWebhookWithContext(...). - Stop runtime by canceling the context or letting the run method return.
- Call
Close()to release local resources. - Create a new
Botif you need another run.
Creating a bot
NewBot[T](opts) is the construction step. It validates the required options, creates the internal Telegram API client and uploader, initializes loggers, prepares a default draft provider, and calls getMe to verify the token and fetch the bot username.
Construction fails immediately when:
optsisnil;opts.Tokenis empty;- Telegram rejects the token or
getMefails during startup.
Useful defaults applied during construction:
- command prefixes default to
"/"whenBotOpts.Prefixesis empty; - the default callback payload encoding is Base64;
MaxWorkersdefaults to32when not set;- the default error template is
"%s"; - a random draft provider is installed unless you replace it.
Configuration before runtime
The normal pattern is to finish all structural configuration before starting the bot:
SetAppData(...)injects your shared dependency container or database handle.AddPrefixes(...)extends the accepted command prefixes.SetPayloadType(...)andSetStrictPayloadType(...)control callback payload decoding defaults.SetUpdateTypes(...)andAddUpdateType(...)control which Telegram update kinds are requested.AddPlugins(...)registers command, payload, update, and scene handlers.AddMiddleware(...)adds bot-level middleware, sorted by order and then by name.AddRunner(...)registers background or one-time tasks.SetL10n(...)replaces the localization provider.SetDraftProvider(...),SetSessionStore(...), andSetSceneScopePriority(...)replace runtime helpers.SetErrorTemplate(...)adjusts centralized user-facing error text.
For an overview of handlers and plugins, see Commands-and-Plugins. For context helpers available inside handlers, see MessageContext.
AddPlugins(...) is a configuration commit point
Bot.AddPlugins(...) clones the plugin configuration into the bot. That means the original *Plugin should be treated as finished before registration.
In practice:
- add commands, payloads, update handlers, scenes, middleware, logger, and
OnClosecallbacks beforeAddPlugins(...); - do not rely on mutating the original plugin after registration;
- later edits to the original plugin may not appear in the bot, because the bot keeps its own internal snapshot.
This is especially important for:
- command descriptions used by auto-generated command metadata;
- plugin middleware;
- custom plugin loggers;
- plugin shutdown hooks.
Configuration freeze phases
Laniakea has three practical configuration phases:
- Construction and bot setup after
NewBot[T](opts). - Plugin snapshotting at
AddPlugins(...). - Runtime freeze after the first
Run(),RunWithContext(...), orRunWebhookWithContext(...).
That means:
- finish plugin structure before
AddPlugins(...); - finish bot-level structure before the first runtime entry point;
- treat later structural bot mutations as intentionally ignored.
After runtime starts, structural calls such as AddPlugins(...), AddMiddleware(...), AddRunner(...), AddPrefixes(...), SetPayloadType(...), SetStrictPayloadType(...), SetDraftProvider(...), SetSessionStore(...), SetSceneScopePriority(...), SetL10n(...), SetAppData(...), SetUpdateTypes(...), AddUpdateType(...), and SetErrorTemplate(...) are ignored.
What “ignored” means
Ignored means the method returns without changing the bot's structural runtime state.
Laniakea prefers a predictable no-op over partially applying runtime configuration changes while updates may already be queued or handlers may already be running. That avoids:
- unclear ordering between configuration edits and update processing;
- races around shared bot state;
- confusion about whether a change affects only future updates or also already accepted work;
- different mental models for plugin snapshots and bot-level state.
Minimal startup pattern
opts := &laniakea.BotOpts{Token: "TOKEN"}
bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
return err
}
defer bot.Close()
plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.NewCommand(func(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer("pong")
return nil
}, "ping")
bot.AddPlugins(plugin)
if err := bot.Run(); err != nil {
return err
}
RunWithContext(...), Run(), and RunWebhookWithContext(...)
RunWithContext(ctx) is the main polling runtime entry point.
It:
- rejects startup when no prefixes are defined;
- rejects startup when no plugins are registered;
- rejects a second start with
ErrBotAlreadyRun; - starts registered runners;
- begins long polling with
getUpdates; - dispatches updates through a worker pool;
- stops polling when the context is canceled.
Run() is only a shorthand for RunWithContext(context.Background()).
RunWebhookWithContext(...) is the webhook runtime entry point. It shares the same:
- single-use rule;
- runner startup behavior;
- internal update queue;
- worker-pool dispatch model;
- graceful shutdown semantics.
Use RunWithContext(...) for production services that poll Telegram directly. Use RunWebhookWithContext(...) when Telegram should deliver updates through your HTTP endpoint.
If you switch an existing deployment from webhook delivery to polling, remove the current webhook first with CloseWebhook() or low-level tgapi.DeleteWebhook(...). Telegram keeps webhook delivery active until the webhook is deleted.
For the webhook-specific option model, transport behavior, and operational guidance, see Webhook-Runtime.
What happens during runtime
After startup, the bot runs three main pieces of work:
- an update ingress path receives updates from polling or webhook transport;
- an internal queue buffers updates before processing;
- a worker pool handles updates concurrently.
Behavior worth knowing:
- polling retry uses exponential backoff, capped at 30 seconds, when
getUpdateskeeps failing; - the worker pool size is controlled by
BotOpts.MaxWorkers; - when the shutdown context is canceled, update ingress stops first, then queued work is drained, then runners are awaited.
Runners and shutdown semantics
Runners start from runtime entry points, not from NewBot.
Supported runner modes:
- one-time async runners start in their own goroutine;
- one-time sync runners block startup until they finish;
- repeating async runners run on a ticker until the bot context is canceled.
Important caveats:
- repeating synchronous runners are treated as invalid and skipped;
- repeating async runners without a timeout are skipped with a warning;
- one-time synchronous runners that take more than two seconds log a warning because they delay startup.
Graceful shutdown
Canceling the runtime context tells the bot to stop accepting new work and finish work already accepted into the queue. The runtime then waits for:
- all queued update handlers to finish;
- one-time async runners to finish;
- background runners to exit after noticing
ctx.Done().
RunWithContext(...) and RunWebhookWithContext(...) do not automatically release API, uploader, or logger resources. You still need to call Close().
Close() versus CloseRemote()
Close() releases local process resources owned by the bot. It closes, in order:
- registered plugins via
Plugin.Close(); - the webhook logger when webhook runtime initialized it;
- the uploader;
- the local API client internals;
- the optional request logger;
- the main bot logger.
Call Close() once you are done with the bot instance, usually with defer right after successful construction.
CloseRemote(ctx) is different. It sends Telegram's Bot API close request for the remote session. It does not replace Close(), and Close() does not replace it.
Use CloseRemote(ctx) only when you specifically need Telegram-side session shutdown behavior. Most bots only need Close().
Single-use rule
A Bot cannot be started twice. After Run(), RunWithContext(...), or RunWebhookWithContext(...) returns, later start attempts fail with ErrBotAlreadyRun.
That means:
- do not call a runtime entry point again after a graceful stop;
- do not reuse a bot instance across tests that start it;
- create a fresh
Botfor every new run cycle.
Recommended production pattern
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
bot, err := laniakea.NewBot[*sql.DB](opts)
if err != nil {
return err
}
defer bot.Close()
bot.SetAppData(db)
bot.AddPlugins(plugin)
if err := bot.RunWithContext(ctx); err != nil {
return err
}
Common mistakes
- Mutating a plugin after
AddPlugins(...)and expecting the bot to see the change. - Calling a runtime entry point twice on the same bot instance.
- Continuing to mutate bot-level structure after runtime already started.
- Forgetting to call
Close()after runtime returns. - Registering background runners without a timeout.
Navigation
Start here
Runtime and Architecture
- Bot-Lifecycle
- Webhook-Runtime
- Middleware
- Runners
- Error-Handling
- Logging
- Update-Routing-Model
- Policies
- Scenes
Interaction and Telegram API
- Inline-Keyboards-and-Payloads
- Auto-Generated-Commands
- Drafts
- Rich-Messages
- Localization
- Rate-Limiting
- tgapi-Overview