I keep forgetting birthdays (most of us have this issue, I guess). I wanted a solution that was automated and free (obviously, I am not going to pay for this).
While looking around, you’ll notice a pattern in many “WhatsApp automation” GitHub repos: they spin up a full browser using Selenium/Puppeteer/Playwright, open WhatsApp Web, and click UI elements. That works, but it’s also the kind of thing that breaks the moment WhatsApp changes a selector, plus it feels unnecessarily heavy for something that should quietly run once a day.
The fun part of this build was discovering a cleaner approach and wiring the whole thing end‑to‑end: WhatsApp connectivity → birthday logic → monitoring → scheduling on Arch
1. The discovery: Baileys
The big turning point is Baileys. It’s a TypeScript/JavaScript library that implements the WhatsApp Web protocol so your script can connect without launching a browser window.
Why it’s a win for personal automation:
- Low resource usage (no Chrome sitting in RAM).
- Runs fully headless (terminal-only).
- Saves auth state locally, so QR scan is usually a one-time thing.
2. The Setup
Initialize a Node.js project and install the needed packages:
mkdir whatsapp-birthday-bot
npm init -y
npm install @whiskeysockets/baileys qrcode-terminal pino
Next, create a simple birthdays.json database in the root folder. This is where you store the dates:
[
{ "name": "Rahul", "phone": "919876543210", "date": "01-14" },
{ "name": "Mom", "phone": "918765432100", "date": "05-20" }
]
3. Bot logic + “self-report” monitoring
The script connects to WhatsApp, checks the date, and sends messages.
However, I realized I needed to know if the bot was actually running. If it fails silently, I’m back to forgetting birthdays. I added a Daily Report feature that sends me a message every time it runs.
Also, one important learning: Baileys uses a persistent WebSocket connection, so if you don’t explicitly exit, the process will just keep running (even after “Sent!”). For a scheduled job, you want it to send and exit.
Here is the core logic in index.js:
async function checkBirthdaysAndSend(sock) {
const today = new Date();
// Format Month-Day (e.g., 01-14)
const dateString = String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
let wishedList = [];
// Check matches
for (const person of birthdays) {
if (person.date === dateString) {
await sock.sendMessage(person.phone + "@s.whatsapp.net", { text: `Happy Birthday ${person.name}! 🎂` });
wishedList.push(person.name);
}
}
// Send Daily Report to Myself
const myJid = jidNormalizedUser(sock.user.id);
const statusMsg = wishedList.length > 0
? `✅ Sent wishes to: ${wishedList.join(", ")}`
: `🤖 System Active. No birthdays today (${dateString}).`;
await sock.sendMessage(myJid, { text: statusMsg });
// IMPORTANT: Kill the process so the cron/systemd job finishes
setTimeout(() => process.exit(0), 2000);
}
4. Scheduling on Arch: systemd user timer
Since I am running Omarchy Linux (an Arch-based distro), I decided to use systemd timers. On modern Arch-based systems, systemd timers are the clean way to schedule recurring tasks. They integrate nicely with logs (journalctl) and let you declare dependencies (like waiting for the network).
This setup uses user units (no root required):
- birthday-bot.service → what to run.
- birthday-bot.timer → when to run.
Create the systemd user directory
mkdir -p ~/.config/systemd/user
Create the service file
nano ~/.config/systemd/user/birthday-bot.service
Paste (update WorkingDirectory and Node path):
[Unit]
Description=WhatsApp Birthday Bot Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/home/yourusername/Projects/birthday-bot
ExecStart=/usr/bin/node index.js
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
Create the timer file
nano ~/.config/systemd/user/birthday-bot.timer
Paste:
[Unit]
Description=Daily Timer for WhatsApp Birthday Bot
[Timer]
OnCalendar=*-*-* 00:05:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Enable + start
Reload systemd and enable the timer:
systemctl --user daemon-reload
systemctl --user enable --now birthday-bot.timer
Confirm the next run time:
```bash
systemctl --user list-timers --all | grep birthday
5. Testing & debugging (the practical part)
Manually run the service (no waiting till midnight)
This is the fastest way to verify everything works:
systemctl --user start birthday-bot.service
Check logs using journalctl
Since the service writes logs to the journal, you can view them like this:
journalctl --user -u birthday-bot.service
6. Make it run even when you’re logged out (enable linger)
User services often stop when you log out. To keep the timer effective even without an active login session, enable linger:
loginctl enable-linger $USER
Verify it with:
loginctl show-user $USER --property=Linger
Expected output:
```ini
Linger=yes
If you see below output then you can be happy 😁.
Linger=yes
Outcome
Now, every night at 12:05 AM:
- Systemd wakes up the timer.
- Node.js starts, Baileys connects to WhatsApp via WebSocket.
- Birthdays are checked.
- Wishes are sent.
- I get a notification from myself confirming the system worked.
- The process kills itself to save resources.
It was a fun build.