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
/todocommand 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.pywithapp_commandsfor slash commands - Storage: a simple JSON file named
todo_data.json - Config: a
.envfile containingDISCORD_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}.
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
defaultlist 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.Botinstance 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
defaultlist, and - automatically switching back to
defaultif 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:
- Install dependencies with
pip install -r requirements.txt. - Create a
.envfile containingDISCORD_TOKEN=your_token_here. - 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
systemdto 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.mdandDEPLOYMENT.mdmake 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.