« Back to Blog

Building a Discord To‑Do List Bot That Actually Fits My Brain

Why I stopped hunting for the perfect productivity bot and built my own instead.


I didn’t start this project because I desperately wanted to write a Discord bot. I started it because I wanted something much simpler: a clean, reliable to‑do list that lived inside Discord. No extra apps, no new tabs, no new logins — just a lightweight way to track tasks in the same place where the conversations happen.

Naturally, I did what most of us would do: I went searching for an existing bot.

The Problem: Existing Bots Weren’t It

Typing “to‑do bot” into Discord bot directories should have solved my problem. Instead, it created a new one.

  • Several bots looked promising, but many were old and unmaintained.
  • Some had broken slash commands or just didn’t respond at all.
  • One bot was actually pretty good — but it was way more complex than what I needed and still didn’t work exactly how I wanted.

At some point I realised I was spending more time evaluating bots than it would probably take to build something small and opinionated that matched how I personally think about tasks.

So instead of trying to bend my workflow to fit someone else’s bot, I decided to build my own.

Defining the Bot I Actually Wanted

Before I wrote any code, I wrote down some constraints. This wasn’t meant to be a “productivity super‑app”. It just had to do a few things really well:

  • Simple mental model: one server → multiple named lists → each list contains tasks.
  • Slash commands only: I didn’t want to remember prefixes or weird syntaxes. Everything should live under a single /todo command group.
  • Per‑server state: each Discord server keeps its own lists; nothing leaks across servers.
  • Persistent storage: if the bot restarts, I don’t want my tasks disappearing.
  • Bulk addition: I often plan in batches, so I wanted something like /todo add-bulk "Task 1; Task 2; Task 3".

And there was one more requirement that heavily influenced the design: future‑me should be able to understand the code. That ruled out a lot of over‑engineered architectures and heavy abstractions.

Tech Stack: Choosing Simple Over Fancy

In the repo you’ll see a small index.js using discord.js, but the real heart of the bot lives in todo_bot.py.

Here’s the stack I ended up with:

  • Language: Python
  • Discord library: discord.py with app_commands for slash commands
  • Storage: a simple JSON file named todo_data.json
  • Config: a .env file containing DISCORD_TOKEN

Instead of spinning up a full database like Postgres or MongoDB, I went with plain JSON. For a to‑do bot that only needs to remember lists and tasks, this is more than enough — and it keeps the project extremely portable.

How the Data Model Works

All of the bot’s state lives in one file: todo_data.json. The structure is intentionally simple. At the top level, each key is a Discord guild (server) ID:

{
  "1306311474913873920": {
    "current_list": "default",
    "lists": {
      "default": []
    }
  }
}

For every server, I store two things:

  • current_list – the name of the active list (starts as "default").
  • lists – a mapping from list name to an array of task objects.

Each task is a small object like: {"task": "Write blog post", "done": false}.

Helper Function

In todo_bot.py, I wrapped this with a small convenience function called ensure_server. Any time a command runs, it:

  • converts the guild ID to a string,
  • creates an entry with a default list if it doesn’t exist,
  • and then returns the server’s data structure.

That means all command handlers can safely assume the basic shape of the data is already there.

Wiring Up the Discord Bot

With the data model in place, the next step was wiring up the Discord client. The flow is fairly standard:

  • Load environment variables from .env.
  • Create a commands.Bot instance with default intents.
  • Register a slash command group called todo.
  • Sync the commands once the bot is ready.

Even though I set a traditional text prefix (!), the real UX lives inside slash commands. Users never have to remember a prefix; they just type /todo and let Discord’s UI guide them.

Designing the /todo Command Group

All of the bot’s features live under a single slash command group: /todo. Under that, I added subcommands that mirror the way I naturally manage tasks.

Adding Tasks (Including in Bulk)

The most basic action is adding a single task to the current list:

/todo add "Write blog post"

That command appends a new object to the server’s current list and sends a confirmation like: ✅ Added task: "Write blog post" to list "default".

But my favourite command might be:

/todo add-bulk "Task 1; Task 2; Task 3"

The bot splits the string by semicolons, trims each piece, filters out empties, and adds them as individual tasks. One command, multiple items — perfect for sprint planning or brain‑dumping a backlog.

Viewing the Current List

To see what’s on a list, I added:

/todo list

The response is intentionally minimal and readable:

  • Each task is numbered starting from 1.
  • A checkmark (✔️) or cross (❌) shows whether it’s done.
  • The list name appears at the top as a title.

This numbered format becomes important for the next set of commands.

Marking Tasks Done or Not Done

Instead of editing task text, I focused on a simple “done/not done” toggle:

  • /todo check <task_number> – marks a task as complete.
  • /todo uncheck <task_number> – marks it as not done.

Both commands use the index shown in /todo list, which keeps the workflow natural: list → pick a number → check/uncheck.

Clearing a List

When a project or sprint finishes, I usually want a clean slate. For that:

/todo clear

This simply resets the current list to an empty array and persists the change to todo_data.json.

Supporting Multiple Lists Per Server

A single list is fine for quick notes, but most servers have different contexts: ideas, bugs, content, chores, and so on. I wanted the bot to reflect that without being overwhelming.

So each server can have multiple named lists, managed with a few simple commands:

  • /todo create-list <name> – create a new empty list.
  • /todo switch <name> – change the active list.
  • /todo delete-list <name> – remove a list you no longer need.

The bot also protects you from shooting yourself in the foot by:

  • preventing deletion of the special default list, and
  • automatically switching back to default if you delete the currently active list.

With just these commands, a server can maintain separate lists for “Sprint”, “Ideas”, “Backlog”, or whatever naming scheme works for the team.

Running and Deploying the Bot

To make the bot easy to reuse (for myself and others), I documented the setup in DEPLOYMENT.md. Running it locally is straightforward:

  1. Install dependencies with pip install -r requirements.txt.
  2. Create a .env file containing DISCORD_TOKEN=your_token_here.
  3. Run python todo_bot.py.

For 24/7 uptime, I listed several deployment options:

  • Railway: simple, with a free tier and GitHub autodeploy.
  • Render: a background worker with a predictable paid tier.
  • Heroku: classic choice, though no longer free.
  • VPS: full control using systemd to keep the bot running.
  • PythonAnywhere: an option for lighter setups with some limitations.

I also added a small security checklist (keep tokens out of Git, regenerate if exposed) and troubleshooting tips, because I know future‑me will eventually forget something obvious.

How I Use It Day to Day

Here’s what a typical session with the bot looks like in a server:

  • Create a focused list for the week: /todo create-list "Sprint"
  • Switch to it: /todo switch "Sprint"
  • Dump tasks in bulk: /todo add-bulk "Fix bug A; Write docs; Refactor X"
  • Check progress with /todo list.
  • Mark finished work: /todo check 2 (for example).
  • Wipe the slate at the end: /todo clear.

All of this happens without ever leaving Discord. No juggling between apps, no syncing issues — just a tiny bot that quietly supports the way I already work.

What I Learned From Building This

This project started as “I just want a to‑do list”, but it left me with a few useful reminders:

  • Simple can be powerful. A JSON file and a few helper functions were enough to solve my real problem.
  • Design from the UX backwards. Thinking in terms of “What command do I wish I could type?” naturally led to /todo add-bulk, /todo switch, and the rest.
  • Per‑server isolation matters. Storing everything under each guild ID keeps data clean and predictable.
  • Documentation isn’t optional. README.md and DEPLOYMENT.md make it much easier for future‑me (and anyone else) to get the bot running in minutes, not hours.

Most importantly, I ended up with exactly what I wanted at the beginning: a maintainable, modern, slash‑command‑based Discord to‑do bot that fits my brain instead of forcing me to adapt to it.