01
How it works
Every module is its own folder in the modules/ directory of the bot repository. The bot loads modules at startup, and the website reads the manifest to build the module page and settings form on its own.
Modules are isolated: each has its own commands, events, settings and translations. A module can’t read other modules’ data directly — only through the bot’s public API.
02
Quick start
You’ll need Python 3.12+, uv and a test Discord server.
- Fork the
mosaicbot-dev1/mosaic-bot-corerepository and clone it. - Install dependencies:
uv sync. - Scaffold a module:
uv run mosaic create birthdays. - Run the bot locally against your test server:
uv run mosaic dev.
git clone https://github.com/<you>/mosaic-bot-core.git
cd mosaic-bot-core
uv sync
uv run mosaic create birthdays
uv run mosaic dev --guild 12345678901234567803
Module structure
A minimal module is a manifest and an entry point. Everything else is optional.
modules/birthdays/
├── module.toml
├── __init__.py
├── commands.py
├── events.py
├── locales/
│ ├── uk.json
│ ├── en.json
│ └── pl.json
└── README.md- module.toml
- manifest: id, version, author, permissions and settings schema
- __init__.py
- entry point: a Python package that registers commands, events and hooks
- commands.py
- slash commands and command groups
- events.py
- Discord event handlers and scheduled jobs
- locales/
- module translations, at least uk and en
- README.md
- description and command list for the catalog
04
Manifest
The website builds the settings form straight from settings. Fields are grouped with section; available types: boolean, number, text, select, color, channel and channels, role and roles, plus pairs — “number → value” rules such as “level 10 → role”.
[module]
id = "birthdays"
name = "Birthdays"
version = "1.2.0"
category = "community"
author = "olha-codes"
permissions = ["ManageRoles", "SendMessages"]
network = []
[[settings]]
section = "general"
key = "announceChannel"
type = "channel"
channel_type = "text"
[[settings]]
section = "general"
key = "birthdayRole"
type = "role"
[[settings]]
section = "general"
key = "timezone"
type = "select"
options = ["Europe/Kyiv", "Europe/Warsaw", "UTC"]
default = "Europe/Kyiv"
[[settings]]
section = "messages"
key = "birthdayMessage"
type = "text"
default = "Happy birthday, {user}!"
max_length = 30005
Code example
A module is a Python package with a Module object that reads the manifest. Settings are typed from the manifest schema, and translations are available through ctx.t.
from mosaic import Context, Module
module = Module.from_manifest(__file__)
@module.on_enable
async def on_enable(ctx: Context) -> None:
ctx.log.info("birthdays enabled on %s", ctx.guild.name)
# Importing the files registers their commands and events on `module`
from . import commands, events # noqa: E402,F401Commands are async functions with the @birthday.command decorator. Store data only through ctx.store: that’s how the bot isolates modules and cleans up data on removal.
from datetime import date
from discord import app_commands
from mosaic import CommandContext
from . import module
birthday = module.group("birthday", description="Birthdays")
@birthday.command("set", description="Set your birthday")
@app_commands.describe(day="Day of the month", month="Month, 1-12")
async def set_birthday(
ctx: CommandContext,
day: app_commands.Range[int, 1, 31],
month: app_commands.Range[int, 1, 12],
) -> None:
try:
when = date(2000, month, day) # a leap year, so 29 February is valid
except ValueError:
await ctx.reply(ctx.t("errors.invalidDate"), ephemeral=True)
return
# ctx.settings comes from the dashboard, ctx.store keeps this module's data
await ctx.store.user(ctx.user.id).set("birthday", when.strftime("%m-%d"))
await ctx.reply(ctx.t("set.success", date=when.strftime("%d.%m")), ephemeral=True)06
Checking locally
Before opening a PR, run the same checks as CI: ruff, pytest and mosaic validate, which verifies the manifest, permissions and translations.
uv run ruff check modules/birthdays
uv run pytest
uv run mosaic validate birthdays07
Pull request rules
A PR is accepted when the module follows these rules:
- One module per PR. Title format:
feat(module): birthdays. ruff,pytestandmosaic validatepass without errors, and public functions have type hints.- The module requests only the Discord permissions it actually uses and explains them in the README.
- No external requests without a
networkfield in the manifest. Collect only the personal data you need. - Translations in at least Ukrainian and English.
- A README with a description, command list and a screenshot of the settings.
- MIT-licensed code, with no borrowed code under an incompatible license.
08
Review and release
Maintainers review PRs within seven days. Automated checks start immediately: build, tests, security analysis and manifest validation.
After merging, the module appears in the catalog with a “Community” badge in the next release — and you appear in the contributors list.