Skip to content
AFM Studio
  • Pages

    AFM Studio
  • Work
  • Lab
  • About
  • CV
  • Contact
  • Projects

    AksaraA searchable library for Otorita IKN's knowledge, where the server checks every reader's access across three levels
  • Daily TaqwaTamper-resistant prayer attendance system for ~2,000 OIKN government employees at Masjid Negara IKN
  • Lantara v2A permit engine for Otorita IKN where administrators add new permit types from a screen, without a developer
  • eKiosk IKNTwo control rooms for Nusantara: kiosk content management and live bus tracking with arrival times that grade themselves
  • E-Monev IKNA monitoring platform for Nusantara's 23-year build, tracking every unit's quarterly progress against 24 national KPIs
  • JDIH OIKNOIKN's public legal document portal, with a controlled publishing workflow and AI summaries that never leave the server
  • Jejak PenanamanA check-in app for IKN tree-planting events that proves each participant was on site with a geofenced selfie
  • Dashboard SAKTIA nightly pipeline and dashboard that replaces OIKN's manual budget downloads for all six of its budget units
  • DoseRxA bedside dose calculator for 127 drugs that rounds each dose to tablets and syrups Indonesian pharmacies sell
  • CubiqA speedcubing trainer with competition-grade timing and a solver I wrote for each of the 8 major puzzles
  • ScimotionAn interactive science library of 86 articles, each explained next to animations the reader can control
  • WA Daily Scrum BotA WhatsApp bot that chases missing standup reports for a 14-person team and flags tasks stuck for days

↑ ↓ to move · Enter to open · Esc to close

WA Daily Scrum Bot

A WhatsApp bot that chases missing standup reports for a 14-person team and flags tasks stuck for days

Screenshots
team members monitored every working day
14
reminder checks a day, each one firmer
4
days before a stuck task is flagged
3
self-service chat commands
3
On this page

The problem

The OIKN THD team, a 14-person software team, runs its daily standup in a WhatsApp group. Each member posts what they did yesterday and what they plan today. Without help, the team lead has to check who hasn't posted and remind them, several times a day, every day. For 14 people that is tedious and easy to forget. Reports got missed or arrived too late to be useful.

The team needed something that watches the group, sends reminders that get firmer through the day, and closes each day with a ranked summary. Reports come in Indonesian and English, so it had to understand both.

What I built

The bot drives WhatsApp Web, because there is no bot API

WhatsApp has no official API for group bots. The business API needs Meta's approval and costs money, which doesn't fit an internal team tool. So the bot runs WhatsApp Web in a browser inside a container. It checks four times a day, at 09:00, 10:00, 11:00 and 14:00, and each reminder is firmer than the last. It only sends a message when someone is still missing. When everyone has reported, it stays quiet.

A report counts when it has both halves

A message counts as a standup report if it contains a pair of phrases, such as "yesterday" and "today", or "kemarin" and "hari ini". That works in both languages and in any format. A second check removes those keywords and looks at what's left. If almost nothing remains, the bot calls out the bare-keyword report in the group. National holidays and each member's leave dates are set in the config, so nobody gets chased on a day off.

It notices tasks that stop moving

The bot also reads each bullet point and follows it across days. It treats "fix login bug" and "fixing the login bug issue" as the same task, based on how many words they share. A task that stays under "today" for three days or more without being reported done gets flagged. Routine duties like piket, cuti and standby are never flagged. For the team's QA lead, who carries a heavy load, a stuck testing task comes with an offer of help from a named teammate.

Leaving a stuck task out doesn't reset it

At first the stuck-task count reset every Monday. A member could also dodge it by leaving the task off one day's list. I changed it so the count carries across weeks and clears only when the same task is reported done. To keep that safe across restarts, the bot rereads the last 500 group messages on startup and replays them in order. A redeploy never loses the tracking.

I tested the Friday recap before letting it post

The Friday recap combines a completion rate, an on-time streak, a team-health comparison with last week and the list of stuck tasks. Any one of them could put a wrong number in front of the whole team. I shipped it with automatic sending switched off and a manual send. After checking the output against a full cycle of team data, I turned automatic sending back on.

Result

The bot runs in production for the team's daily standups and monitors 14 members on working days. I built it in 18 days in June 2026. It started as report detection and reminders, then grew into task tracking, weekly analytics and self-service commands. Members type !mystats for their own on-time rate, streak and completion rate, !rank for the leaderboard, and !help for the list. State lives in plain files on a mounted volume, so a restart keeps the day's progress. I also built a React dashboard as a demo of the schedule and messages. It isn't connected to the live bot.

Under the hoodTechnical detail for engineers
  • Node.js 18 with whatsapp-web.js (Puppeteer and Chromium), node-cron for scheduling and qrcode-terminal for linking the phone. The session depends on a linked phone staying online and can break when WhatsApp updates its web client.
  • The Docker image is node:18-bookworm-slim with system Chromium. Puppeteer's bundled download is skipped and the container hostname is pinned to avoid profile-lock conflicts. An entrypoint.sh deletes stale Chromium Singleton lock files before Node starts.
  • Mid-project, WhatsApp moved contact IDs from the @c.us format to opaque @lid IDs, which broke member matching. I logged sender IDs from live messages and updated the member config.
  • Detection uses two keyword strings plus three bilingual keyword pairs.
  • detectSection() classifies lines as Yesterday, Today or Impediment. parseSectionTasks() extracts bullets. normalizeTask() and taskSimilarity() use Jaccard token overlap with a 0.7 threshold. CONFIG.staleTask.minDays is 3.
  • .week-state.json holds reports, carry, ledger and history. Daily state lives in .bot-state.json.
  • Eight daily cron jobs: kickoff, an 08:02 carry nudge at a two-day threshold, four checks, the end-of-day summary and the weekly recap slot. The Friday recap posts at 17:05. A Monday recap re-posts last week's stuck tasks.
  • On-time means before 09:00 WITA, over a rolling 4-week, 20-working-day window. Streaks tolerate leave days.
  • Per-member settings staleHelp and stalePriority; weeklyRecapAutoSend gates the recap. SIGUSR1 fires a reminder, SIGUSR2 prints the recap to the logs, and SIGHUP sends it.
  • !mystats replies in the group with an @mention. !leaderboard is an alias for !rank.
  • GitHub Actions builds the image to GHCR. bot.js is 1,166 lines and the dashboard 386. 25 commits from 10 to 28 June 2026.

Screenshots

Morning Kickoff
Escalating Reminders
Weekly Leaderboard

Hiring, or building something similar?

I'm open to full-time roles and select freelance work.