Efficient Time Tracking with Auto Break Room Switcher: Discord Status, n8n & Oracle APEX
How we made breaks fully automatic — no buttons, no commands, just Discord status.
In our first article we built a fully automated time tracking system where employees simply join and leave a Discord voice channel — no manual entries, no extra apps. It worked great. But one thing was still missing: breaks.
When someone steps away from their desk, they stay in the voice channel. The system keeps counting those minutes as working time. Over a month, that adds up fast. We needed a smarter solution — and it turned out Discord already had the signal we needed: the user status.
The Idea: Discord Status as a Break Signal
Every Discord user has a status: online, idle, or offline. When someone locks their computer or walks away, Discord automatically switches their status. That's our break trigger — no habit change required.
Here's the full flow:
idle / offline while in a work channel → bot moves them to Break Room, break timer startsonline while in Break Room → bot moves them back to their last work channel, break endsThe bot remembers which work channel each person was in before their break, and moves them back to exactly that channel when they return.
The Stack
| Component | Technology | Role |
|---|---|---|
| BOT Discord Bot | Node.js 20 · Discord.js v14 | Listens to voice & presence events, moves users automatically |
| N8N Workflow | n8n · Docker · Portainer | Receives webhook events, writes to Oracle DB, sends WhatsApp notifications |
| DB Database | Oracle Database · PL/SQL | Stores all JOIN/LEAVE/BREAK events with timestamps |
| UI Dashboard | Oracle APEX | Visualizes working hours, breaks, and monthly calendar |
The entire bot runs as a single Docker container deployed through a Portainer Stack. The Node.js application is written inline inside the docker-compose.yml — no Dockerfile, no image to push, no extra files. Just deploy the stack and it runs.
You only need three environment variables:
DISCORD_BOT_TOKEN — your bot token from the Discord Developer PortalN8N_WEBHOOK_URL — the webhook URL of your n8n workflowPAUSE_CHANNEL_ID — the Discord channel ID of your Break RoomDownload the docker-compose.yml and paste it into your Portainer Stack editor:
The
autoMovedUsers and autoReturnedUsers sets prevent infinite loops — when the bot moves a user, that triggers a new voiceStateUpdate event. Without these guards, the bot would react to its own moves.The
lastWorkChannelByUser map remembers each user's last work channel so the bot can return them to exactly the right place after a break.
The n8n workflow receives every event from the bot via a single webhook and routes them through a Switch node based on the event_type field. Each branch handles one event type: JOIN, LEAVE, LEAVE_BREAK, BREAK_RETURN, and MOVE.
Each branch fetches the user from Oracle DB, looks up their parameters, then calls a PL/SQL stored procedure to persist the event. After that, a WhatsApp notification fires via WaAPI — managers get an instant message every time someone goes on break or returns.
One special node: check_if_go_offline. When a user goes fully offline, Discord fires both a presenceUpdate and a voiceStateUpdate event. This node ensures we don't count that as two separate work events.
Import this n8n workflow directly via Workflows → Import from file:
Nothing changed on the APEX side — the same dashboard from Part 1 now has cleaner, more accurate data. Break time is properly excluded from working hours, so the numbers actually reflect productive work time.
Final Thoughts
The biggest win here isn't the technology — it's the zero behavior change required from employees. They don't click "Start Break." They don't type a command. They just lock their computer when they step away. Discord status does the rest.
For us, this eliminated the biggest data quality problem in our time tracking: inflated working hours from forgotten AFK sessions. The numbers are now reliable, and managers don't need to manually review or adjust anything.
Next up: a manual override slash command for edge cases where status detection isn't accurate enough. We'll cover that in a future post.
{fullWidth}