Skip to content

AI assistants

Coding agents integrate this package the way a person would — by reading the docs — and they get the same handful of things wrong, usually because 1.x material is still the first thing a search finds. This page is the brief to hand them.

Paste this in

Everything an assistant needs to wire the package into a Django project correctly, short enough to sit in a system prompt or a CLAUDE.md / AGENTS.md / .cursor/rules file:

Project uses django-aiogram 4.x. Rules:

- Import the shared instance: `from django_aiogram import bot`. Never
  construct TelegramBot() per task or per request — that builds an event loop and
  an HTTP session nothing closes.
- To send from anywhere (view, task, signal): `bot.send(chat_id=..., text=...)`.
  It queues on whichever transport `BROKER` names outside the bot container, and
  calls Telegram directly inside it. Pass another method by name: bot.send('send_photo', chat_id=..., photo=...).
  Only Telegram API methods aiogram exposes are accepted.
- From async code, await `bot.asend(...)` instead: `send()` writes to a socket on
  the thread the loop is running on. Same arguments, same returned id.
- To reach many chats, call `bot.send_many(chat_ids, text=...)`, which is
  synchronous. From async code, `await bot.asend_many(chat_ids, text=...)`: the
  coroutine queues nothing until it is awaited. Both queue a chunk per round trip
  and return one id per chat, and both queue inside the bot container too, where
  `send` would call Telegram directly. With `ENABLED=0` neither writes anything
  and you still get the ids, the same as `send`. They speed up queueing only —
  the rate limits still pace delivery.
- Handlers go in <app>/tg_router.py and are registered with decorators on the
  shared bot: @bot.message(F.text), @bot.callback_query(...). They are ordinary
  async Django code; use afirst()/sync_to_async for the ORM.
- Bot-wide defaults such as parse_mode belong in
  TELEGRAM_BOT['DEFAULT_BOT_PROPERTIES'], not in every send call.
- Settings live in the TELEGRAM_BOT dict; scalars can come from
  DJANGO_AIOGRAM_<NAME> environment variables.
- The package is safe to import with no TOKEN and no Redis. Do not add
  placeholder credentials to make imports work, and do not guard imports in
  try/except.
- `from django_aiogram import bot` loads aiogram (~900 ms), which is the
  cost of sending and is paid once. Import it in the modules that send, not in a
  package `__init__` that every process loads.
- Only the container running `manage.py start_tgbot` runs the bot. Do not set
  DJANGO_AIOGRAM_ENABLED=0 on web or Celery processes: it turns their
  sends into no-ops and the messages are dropped.
- Queued payloads are JSON. Keep SERIALIZER='json'. Writing pickle takes BOTH
  SERIALIZER='pickle' and ALLOW_PICKLE=True — the flag alone only lets the
  reader accept pickle, so a payload JSON cannot describe is still refused at
  the point it is queued. It is the escape hatch for exactly those payloads,
  and turning it on means whoever can write to the queue can execute code
  in the bot container, so do it only on a queue nothing untrusted can write to.
- A queued send cannot raise in the caller. Failures are logged by the worker.
  Use bot.send_raw with RAISE_EXCEPTION only when the caller must see the error.
- To send later, pass `eta` to any form that **queues** —
  send, enqueue, send_many and their awaiting twins, but never send_raw, which
  reaches Telegram from this process and has nothing to schedule:
  `bot.send(chat_id=..., text=..., eta=timezone.now() + timedelta(hours=1))`. It
  writes a row and publishes nothing, so the deployment must run
  `manage.py tgbot_dispatch_scheduled` — from cron or with `--loop` — or the
  message waits for ever. `bot.cancel_scheduled(identifier)` calls it off while it
  is still waiting. Do not reach for Celery's countdown for this. The datetime has
  to match the project's `USE_TZ`: aware where it is on, naive where it is off,
  and the opposite is refused either way.
- To edit or delete a message you queued, read its `message_id` back with
  `bot.outcome(identifier)` — the id `send()` returned is a correlation id and not
  Telegram's. Pass an explicit `correlation_id=uuid4()` to any send whose own
  outcome you will use: inside a handler the id is inherited from the update, so
  every reply shares it and `outcome()` answers about the newest of them. For a
  send_media_group read `answer.sent`, which holds an entry per message of the
  album; `answer.message_id` is only its first.
- Branch on the outcome's state before acting on it, all four of them. Only
  `sent` may be edited, and check `message_id` and `chat_id` are not None even
  then — a call that produced no message, such as send_chat_action, is `sent`
  with neither. `failed` means stop. `pending` and `unknown` mean ask again
  later, within a bound of your own: `unknown` can be permanent, so past that
  bound treat it as unresolved rather than polling on. Do not write a loop that
  blocks a request waiting for it.
- It needs EVENT_LOG=True and an EVENT_LOG_KINDS that is either empty or keeps
  outbound.sent, outbound.failed, outbound.dropped and outbound.queued — **in the
  start_tgbot container as well as in the process that asks.** The rows are
  written by whichever process sent the message, and the refusal can only speak
  for the settings it can see: a bot container with the log off leaves the
  lookup answering `unknown` with nothing to refuse.
- `python manage.py check` validates the settings; treat its E0xx/W0xx output as
  the spec.
- Run `python manage.py migrate` after upgrading. The package ships two tables,
  created whether or not you turn the event log on.
- bot.send() returns a correlation id. Store it next to your own model if you
  want to join your records to the event log later.
- For metrics, connect a receiver to `events_recorded` from
  `django_aiogram.eventlog.signals` in an AppConfig.ready(). Do not invent a settings
  hook: there is none. It fires with EVENT_LOG off, so metrics need no table and no
  migration, and the exporter must run in the start_tgbot container because that is
  where send outcomes are recorded.
- The event log is off by default (TELEGRAM_BOT['EVENT_LOG']). Turning it on
  needs a retention job — `manage.py tgbot_prune_events` — or the table grows
  without bound. Message bodies are not stored unless EVENT_LOG_PAYLOAD='full',
  which is a personal-data decision, not a verbosity one.

Prompts that work

Add a notification. "In orders/views.py, notify the reviewer over Telegram when an order is approved. Use bot.send from django_aiogram so the request does not wait on Telegram, and add a test that asserts the message was queued — see the Testing page of the django-aiogram wiki for the fakeredis recipe."

Add a handler. "Add support/tg_router.py with a /status command that answers with the caller's open ticket count. Register it with @bot.message from django_aiogram, keep the ORM access async, and do not touch INSTALLED_APPS — autodiscover imports tg_router from every installed app."

Set up the containers. "Add a telegram_bot service to docker-compose.yml running python manage.py start_tgbot, restarting always, depending on redis, sharing the same image and .env as back. Give it a healthcheck running python -m django_aiogram.healthcheck with DJANGO_SETTINGS_MODULE in its environment: — the probe is a separate process, and manage.py only sets that variable inside its own process. Not manage.py tgbot_healthcheck in a healthcheck: it runs django.setup() first and Docker kills it at the timeout. Leave DJANGO_AIOGRAM_ENABLED unset on the other services — they queue messages."

Turn on the event log. "Run manage.py migrate first, then enable TELEGRAM_BOT['EVENT_LOG'] in django-aiogram — a process that starts recording before the table exists drops everything it records until someone notices. Then set EVENT_LOG_RETENTION_DAYS and schedule manage.py tgbot_prune_events daily. Leave EVENT_LOG_PAYLOAD at its default so message bodies stay out of the table, and grant support only view_telegramevent." See Event log.

Migrate an older project. "This project imports telegram_bot, which django-redis-aiogram 3.0 removed, and that distribution is now django-aiogram. Move it to django_aiogram 4.x following the wiki's Upgrading page, newest section first: rename it in INSTALLED_APPS, replace the imports with the 4.0 layout, move parse_mode into DEFAULT_BOT_PROPERTIES, drop the placeholder token from settings, and use bot.router instead of bot._router." See Upgrading.

Debug delivery. "Messages are queued but never arrive. Check in this order: is the start_tgbot container running and is ENABLED true there, does bot.queue_depth() grow — it asks whichever transport BROKER names, where a redis-cli llen answers for the list transport alone — and what does the django_aiogram logger say. The wiki's Troubleshooting page lists the causes per symptom."

What assistants get wrong

Each of these has been seen in real integrations, and each is a 1.x habit:

Mistake Why it happens What to do instead
A placeholder TOKEN in settings so imports work 1.x built the bot at import time and crashed without one Nothing. Since 2.0 it imports fine with no credentials
parse_mode in every send call 1.x had no other way DEFAULT_BOT_PROPERTIES once
DJANGO_AIOGRAM_ENABLED=0 on web and Celery it reads like "do not run the bot here" Leave it unset; only start_tgbot runs the bot
TelegramBot() inside a task the shared instance looks stateful Import bot
bot._router it was private for a long time bot.router
try/except around the import defensive habit from the crashing version Import it plainly
Expecting a queued send to raise the call looks synchronous The worker logs it; use send_raw if the caller must know
SERIALIZER: 'pickle' "for keyboards" true in 1.0.4, false since aiogram 3 JSON round-trips keyboards, media and files

Working on this package, not with it

AGENTS.md in the repository root is the brief for that: layout, the commands CI runs, and the invariants that have dedicated tests. Anything an agent changes here needs a test that fails when the change is reverted.