Extensions
Each feature lives in its own folder under src/extensions/<name>/. Botkit loads every enabled extension at startup and connects it to Discord and/or your HTTP API.
Finding and enabling extensions
- Put code in
src/extensions/<name>/(one folder per feature). - Export
defaultand at least one ofsetup/setup_webserverfrom the package. - Enable in config, or set
default = {"enabled": True}and omit YAML.
extensions:
hello:
enabled: true
Config keys match folder names; my_ext and my-ext are equivalent (Configuration).
Zip installs
Drop something.zip into src/extensions/. Botkit extracts it on startup and removes the zip. Useful for shipping extensions without unpacking manually.
Required exports
| Export | Required | Purpose |
|---|---|---|
default |
Yes | Must include enabled: bool. Default settings when YAML has no block for this extension. |
setup |
One of setup / setup_webserver |
Register cogs, listeners, slash commands (use.bot) |
setup_webserver |
One of setup / setup_webserver |
Register FastAPI routes (use.backend) |
on_startup |
No | Async hook before Discord connects; do not call Discord APIs that need a logged-in bot |
Hook functions only receive arguments you declare:
def setup(bot, config): ... # bot + config
def setup(bot): ... # bot only
def setup_webserver(app, bot, config): ...
async def on_startup(bot, config): ...
Minimal bot-only extension
src/extensions/hello/
__init__.py
main.py
# __init__.py
from .main import default, setup
__all__ = ["default", "setup"]
# main.py
import discord
from discord.ext import commands
from src import custom
class HelloCog(commands.Cog):
def __init__(self, bot: custom.Bot) -> None:
self.bot = bot
@discord.slash_command(name="hello", description="Say hello")
async def hello(self, ctx: discord.ApplicationContext) -> None:
await ctx.respond(f"Hello, {ctx.author.name}!")
def setup(bot: custom.Bot) -> None:
bot.add_cog(HelloCog(bot))
default = {"enabled": True}
Bot + HTTP extension
Discord command and health route (pattern from the bundled ping extension):
import discord
from discord.ext import bridge, commands
from fastapi import FastAPI
from src import custom
from src.log import logger
default = {"enabled": True}
class BridgePing(commands.Cog):
def __init__(self, bot: custom.Bot) -> None:
self.bot = bot
@bridge.bridge_command()
async def ping(self, ctx: custom.Context, *, ephemeral: bool = False) -> None:
await ctx.defer(ephemeral=ephemeral)
await ctx.respond(f"Pong! {round(self.bot.latency * 1000)}ms", ephemeral=ephemeral)
def setup(bot: custom.Bot) -> None:
bot.add_cog(BridgePing(bot))
def setup_webserver(app: FastAPI, bot: discord.Bot) -> None:
@app.get("/ping")
async def ping_route() -> dict[str, str]:
if not bot.user:
return {"message": "Bot is offline"}
return {"message": f"{bot.user.name} is online"}
async def on_startup(config: dict[str, object]) -> None:
logger.info("Ping extension started")
Set use.bot: true for the cog and use.backend: true for the route. With both enabled, the same bot instance is shared.
Reading extension config
YAML under extensions.<name> is passed as config:
def setup(bot: custom.Bot, config: dict[str, object]) -> None:
url = config.get("webhook_url")
if not url:
logger.warning("my_ext: set extensions.my_ext.webhook_url in config")
return
bot.add_cog(MyCog(bot, str(url)))
default = {
"enabled": False,
"webhook_url": "",
}
Config keys are lowercase. Strings from translations.yml appear as config["translations"] — see Internationalization.
Translations
Optional translations.yml next to the extension, or src/translations/<name>.yml.
Without it, the extension still loads; you just won't get localized command names or ctx.translations. See Internationalization.
Patch files (patch.py)
Optional patch.py with a patch() function, run before the rest of the bot starts, only when the extension is enabled. Use rarely — for global behavior that must run early (for example error handling hooks).
def patch() -> None:
...
Prefer normal extension code when you can.
Common mistakes
| Problem | Fix |
|---|---|
| Extension not loading | Check enabled: true in YAML or default |
setup never called |
Set use.bot: true |
| Routes missing | Set use.backend: true and implement setup_webserver |
| Import error on startup | Fix Python errors in the extension package; check logs |
Missing default or enabled |
Add default = {"enabled": True} (or False) |
Logging
Use Botkit's logger — not print():
from src.log import logger
log = logger.getChild("hello")
def setup(bot: custom.Bot) -> None:
log.info("Loading hello extension")
bot.add_cog(HelloCog(bot))
Levels respect logging.level in config (Configuration):
DEBUG— detailed tracingINFO— startup, successful operationsWARNING— missing config, degraded behaviorERROR/CRITICAL— failures
Log files are written when logging.file: true.
Layout tips
- One feature per folder; split large extensions into
commands.py,views.py, etc. - Re-export
setup,default, and optional hooks from__init__.py. - Document extension-specific config in a local
readme.md.