Skip to content

Extensions

Now that we have a model and table for storing tasks, let's give the bot a way to interact with them using extensions — modules that group related commands together, discovered and loaded automatically at startup.

An extension is simply a bot/extensions/*.py module that defines an async setup(bot) function and, conventionally, a discord.ext.commands.Cog subclass. Grace has no custom base class for cogs — it re-exports discord.ext.commands directly (from grace import Cog, hybrid_command, ...), and discovers any module under bot/extensions/ that exposes setup (see Application.extension_modules).

Generating a Cog

Generate your first cog:

grace generate cog Task

This creates a new module at bot/extensions/task_cog.py:

from discord.ext.commands import Cog
from grace.bot import Bot

class TaskCog(Cog, name="Task"):
    def __init__(self, bot: Bot):
        self.bot: Bot = bot

async def setup(bot: Bot):
    await bot.add_cog(TaskCog(bot))

The cog name must be PascalCase (e.g. Task, TaskManager). You can optionally pass a description:

grace generate cog Task "Manage your tasks"

This is the starting point for your command logic. TaskCog is registered automatically when your bot starts (Bot.load_extensions), and you define commands inside it to interact with your models.

Adding Commands

Start by importing what you need:

from discord.ext.commands import Cog, Context, hybrid_command
from bot.models.task import Task

Listing Tasks

@hybrid_command(name="list")
async def list_tasks(self, ctx: Context):
    tasks = Task.all()

    if not tasks:
        await ctx.send("No tasks found.")
        return

    lines = [f"- {'✅' if t.done else '❌'} [{t.id}] **{t.name}**: {t.description or ''}" for t in tasks]
    await ctx.send("\n".join(lines))

This fetches all Task records and sends them as a formatted message.

Adding a Task

@hybrid_command(name="add")
async def add_task(self, ctx: Context, name: str, *, description: str = ""):
    task = Task.create(name=name, description=description, done=False)
    await ctx.send(f"Added task: **{task.name}**")

Users can type:

/add "Write docs" Finish the getting started guide

Deleting a Task

@hybrid_command(name="delete")
async def delete_task(self, ctx: Context, *, task_id: int):
    task = Task.find(task_id)

    if not task:
        await ctx.send(f"No task found with id '{task_id}'.")
        return

    task.delete()
    await ctx.send(f"Deleted task: **{task.name}** 🗑️")

Marking a Task as Done

@hybrid_command(name="done")
async def done_task(self, ctx: Context, *, task_id: int):
    task = Task.find(task_id)

    if not task:
        await ctx.send(f"No task found with id '{task_id}'.")
        return

    task.update(done=True)
    await ctx.send(f"Marked **{task.name}** as done ✅")

Trying It Out

Since hybrid_command registers both a slash command and a text command, you can test with either:

/add "Write docs" Finish the migration section
/list
/done 1
::list
::delete 1

(:: is the default prefix set in config/settings.cfg — see Configuration.)

Next Steps

Once your commands are working, learn how to manage the underlying database from the CLI in Database Management, or extend the generator system itself in Writing Generators.