DEV Community

Cover image for "we should go out this weekend": I built a Discord member that holds the clock until we're outside
Arqam Waheed
Arqam Waheed Subscriber

Posted on AI-assisted

"we should go out this weekend": I built a Discord member that holds the clock until we're outside

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

TL;DR. Group plans don't die from disagreement. They die because nobody holds the clock. Quorum is a member of your Discord channel that does. Tell it what limits you, type /plan, and Gemma 4 drafts real places nearby that fit everyone. A Temporal workflow then runs the poll, locks the plan, re-checks the rain at 05:30 and sends the 06:00 reminder, even if my laptop crashes or the agent server is asleep. Total bill: $0.


What I Built

The problem Quorum solves is older than any of that. You know how it goes. Someone says "we should go out this weekend." Three people agree. Nobody picks a place, nobody closes the poll, and on Sunday nobody is outside.

The villain here is the missing clock. Not disagreement. Not apathy. There is just nobody whose job it is to say "the poll closes at 22:00, I'll check the weather at 05:30, see you at 06:00."

So I made that somebody a bot.

For each friend, the whole interaction is a few taps in the channel they already use:

  1. /join, then one line per limit: /me no car, /me home by 11, /me knee hates long descents.
  2. Someone types /plan date:2026-10-17 people:4. Quorum replies with 2-3 real places near home, each with the limits that shaped it.
  3. A vote menu. You pick every option you can make. When enough people can make the same one, it locks. If 22:00 the night before comes first, Quorum says so, and nobody feels guilty.
  4. At 05:30 it re-checks the rain for the exact hours you'll be out. At 06:00 it posts the reminder, or the rain-proof switch. When you're back, it asks for one line about how it went and remembers it for next time.

Split panel on a dark pine background. Left, labelled BEFORE the group chat: three messages from Sara, Omar and Lina saying we should go out this weekend, yes we should, and so are we going on Friday at 23:40, then red text reading Sunday: nobody went. Right, labelled AFTER Quorum in the channel: two Discord bot messages, a Locked plan for Kachnar Park at 08:00 and a 06:00 Good morning reminder

The whole product. The left side is every group chat. The right side is the job nobody wanted.


Demo

The 30-second demo is also in the repo: docs/quorum-demo.mp4.

Try it without joining any chat: quorum-y649.onrender.com/try. Type a few made-up people with their limits, a city and a date. You get the same options the bot would post. It runs on Render's free tier, so the first load can take about a minute while it wakes. That nap turns out to matter below. On the live service, one draft for Islamabad took 72 s: two tool calls, Lake View Park and F-5 Park, zero invented places.

This is what /plan posts in the channel. Every option names the limits it was built around:

Discord message from the quorum app listing three options for Islamabad: Morning walk at Kachnar Park at 08:00 for 2 hours, Visit Childrens Park at 07:30 for 2.5 hours, and Short outing to CDA Park at 08:30 for 1.5 hours, each with a line reading Shaped by no car and home by 11, and a vote menu below

Shaped by your limits, not by a generic "top 10 parks" list.

The poll locks the moment enough people can make one option. Then nothing happens for hours, on purpose, until the reminder:

Discord message from the quorum app at 06:00 reading Good morning. Today: Morning stroll at Kachnar Park. Kachnar Park, start 08:00, about 2 hours. Rain chance during the outing: up to 12%

The 06:00 message. This is the one that actually gets people out of bed.

What's real and what isn't

  • Real: every message and every number in this post comes from runs against the live services (Gemma 4 on the Gemini API, MongoDB Atlas, Discord, Open-Meteo, OpenStreetMap), copied from the plan's own event log.
  • Not real yet: a group outing. The real plan is for Saturday 17 October, which is after the deadline. What you see is a full rehearsal with the timers compressed to minutes, plus three failure drills.
  • Not live after publishing: the Discord bot and the Temporal worker run on my laptop. The Render pages stay up.

Code

GitHub logo ArqamWaheed / quorum

A group-chat member that holds the clock until your friends are actually outside.

Quorum

A member of your friend group's Discord server that turns "we should go out" into an actual outing. A Mastra agent on Gemma 4 drafts plans around everyone's constraints, and a Temporal workflow holds the clock: poll, quorum deadline, weather re-check, morning reminder, recap.

Live: quorum-y649.onrender.com (try it at /try; free tier, first load can take a minute). Demo: docs/quorum-demo.mp4, 30 seconds.

Discord ──► laptop: bot + Temporal dev server + worker ──HTTPS──► Render: Mastra agent (Gemma 4) ──► MongoDB Atlas
                                                                    tools: Open-Meteo, OpenStreetMap
  • Temporal owns time: waiting for votes, the 05:30 weather check, the 06:00 reminder, retrying a sleeping server.
  • Mastra + Gemma 4 own reasoning: which outing, and which constraint shaped it.
  • MongoDB Atlas owns memory: who joined, their limits, plans, recaps, and semantic recall of past outings.
  • Render owns the public face: the agent API, the plan page /p/:id, and the demo at…

Apache-2.0. Interesting files:

  • src/temporal/workflows.ts: the whole outing as one function. Poll, quorum, 05:30, 06:00, recap.
  • src/temporal/activities.ts: calls to the agent server with retries, and every Discord message.
  • src/mastra/agents/quorum.ts and src/mastra/draft.ts: the Gemma 4 agent, its tools, and the check that throws out any place the tools didn't return.
  • src/bot/discord.ts: slash commands, plus votes turned into workflow signals.
  • scripts/test-workflow.ts: the whole workflow against a local Temporal server with fake activities, timers in seconds. 9 checks.

The heart of it is a vote that can wait for days and survive restarts:

const reached = await wf.condition(
  () => tally(votes, options.length).count >= p.quorum, // enough people for one option…
  until(p.pollCloseAt),                                 // …or 22:00 the night before
);
Enter fullscreen mode Exit fullscreen mode
npm install && cp .env.example .env        # Gemini key, Atlas URI, Discord token
./scripts/run-local.sh                     # Temporal dev server, UI on :8233
npm run worker & npm run bot & npm run dev
Enter fullscreen mode Exit fullscreen mode

How I Built It

Four owners, one job each. Temporal owns time. Mastra and Gemma 4 own reasoning. MongoDB Atlas owns memory. Render owns the public page. The laptop holds only the clock and the bot token.

System flow diagram titled Four owners, one job each. Three boxes left to right: Discord channel (join, me no car, plan, vote menu), Laptop running Temporal, worker and bot (wait for quorum or the deadline, sleep until 05:30 then 06:00, retry the agent server 5 s with x2 backoff and 12 attempts), and Render free running a Mastra agent on Gemma 4 26B (weather tool, places tool, plan page). Yellow request arrows labelled slash commands and HTTPS 45 s timeout, a dashed result arrow labelled options and weather verdict, and a bar along the bottom for MongoDB Atlas M0

The split. A workflow can't judge a knee. An LLM can't sleep until 05:30.

Why a workflow, not cron or setTimeout. The outing is one Temporal workflow. Votes arrive as signals. condition() waits for quorum or the deadline. Then sleep() waits until 05:30 and 06:00. The bot does all the time math and hands the workflow absolute timestamps, so the workflow itself stays deterministic. A setTimeout on a free host that sleeps every 15 minutes would die before Friday. Temporal persists the timers, so a restart just resumes them.

I didn't want to take that on faith, so I tried to break it.

Plate titled Three ways I tried to break it, from the 11 October compressed test run. Three columns. Crash mid-poll: kill -9 the worker and restart the Temporal server, the plan came back in the voting step and locked. Agent server asleep: four red failed attempt boxes and a yellow fifth box, with log lines 15:55:14 attempt 1 failed, 15:55:50 attempt 4 failed, 15:56:56 attempt 5 ok, 25.9 s. Rain: with the rain limit forced to 0%, the 06:00 reminder switched to Visit Childrens Park

Durable by design, not by luck. Every step is a timer or a retry that Temporal persisted.

Crash. Mid-poll, I ran kill -9 on the worker, then stopped the Temporal server itself and started it again on the same database file. The workflow came back in voting. My vote landed. The plan locked.

A sleeping server is a test, not a bug. The agent runs on Render's free tier, which sleeps after 15 idle minutes and takes about a minute to wake. So the 05:30 weather check will usually hit a sleeping server. The activity has a 45-second fetch timeout and a retry policy (5 s, doubling, 12 attempts). To prove it, I stopped the agent server right before the check:

Temporal Web UI, Pending Activities tab for the plan workflow: activity recheckWeather, state Scheduled, attempt 5 of 12 with 7 remaining, next retry in 38 seconds, and the last failure JSON showing fetch failed caused by connect ECONNREFUSED 127.0.0.1:4111

Attempt 5 of 12. Temporal kept knocking until the server answered.

Temporal doesn't write every retry into the event history. It keeps the attempt count and the last failure. So the activity also logs every attempt to the plan's own record, and the public plan page shows it:

Quorum plan page timeline titled Where it stands: yellow blazes for Options drafted, Poll open, Plan locked, then a red blaze for Weather re-check reading Rain up to 12%. Switched to the backup plan, with four red log lines of failed attempts and a fifth reading attempt 5 ok in 25873 ms, then Morning reminder, Recap asked and Remembered for next time

Four failures and one success, in public. The retry log is a feature, not a debug screen.

The rule is code. The alternative is Gemma. At 05:30 the forecast is checked by plain code against a rain limit. Only if it fails does Gemma get 20 seconds to pick or adapt a rain-proof alternative. If it can't, code takes the option with the least rain in its window. For the drill I forced the limit to 0%:

Discord message from the quorum app reading Good morning. Rain is likely on the original plan, so we're switching to: Visit Childrens Park. Childrens Park, start 07:30

Plan B, posted at 06:00. Nobody had to call a group vote at dawn.

Mastra reasons, and only reasons. The agent has two tools, weather (Open-Meteo) and places (OpenStreetMap), plus memory, and returns structured options. Its instructions forbid any place the tools didn't return. The code checks anyway: an option whose place isn't in the tool's list gets dropped, and if fewer than two survive, code builds them from real places. Across 16 smoke-test drafts it dropped 0 invented places.

Gemma 4, measured rather than assumed. Gemma 4 has built-in function calling, but Mastra's model catalogue doesn't mark the Gemma 4 IDs as tool-capable. So before trusting it, I ran the same planning prompt with a fake 5-person roster, 8 times per model:

Model (same agent, Gemini API free tier) valid plans with tool calls invented places median time Gemini errors retried
gemma-4-26b-a4b-it 8/8 0 128 s 0
gemma-4-31b-it 7/8 0 202 s 4

26B runs Quorum. The 31B returned "Internal error encountered" on about half its calls at first, so transient Gemini errors are now retried inside the draft call, then again by Temporal. I also tried having code fetch the weather and places and handing them to Gemma in the prompt. It was not faster (120-193 s), so the time goes into generation, not tool round trips.

Atlas remembers what a context window forgets. Your limits live in a plain members collection, so they never depend on the model deciding to call a "remember" tool. Everything said to the agent, recaps included, also goes into Mastra memory on Atlas with semantic recall over Atlas Vector Search (3072-dimension Gemini embeddings). So next week's draft can read last week's recap before it proposes anything.

Two honest concessions.

It's slow. About 2 minutes per draft on the free tier. That's fine for a plan that waits until Friday night. It was not fine for my own test: the first compressed poll closed 53 seconds after the options appeared, because drafting ate most of a 3-minute window. I gave test polls 7 minutes. Real polls stay open for days.

OpenStreetMap's public Overpass servers were down all weekend. Every mirror returned 504s or timed out. Places now fall back to Nominatim, the same OpenStreetMap data ranked by importance, and the results are cached in Atlas. Around Islamabad that still gives Margalla Hills National Park, Shakarparian and Rawal Lake Viewpoint. It also gives a few parks named after sector numbers, which is accurate and a little sad.

Things I refused to do:

  • Do not let the bot read the chat. It connects with only the Guilds intent, no Message Content, so it sees slash commands and taps on its own buttons, nothing else.
  • Do not store people who didn't /join. /forget deletes your limits and recaps.
  • Do not let Gemma invent a place. If the name isn't from OpenStreetMap, it doesn't reach the poll.
  • Do not edit the workflow during a live run. I froze workflows.ts with a git tag before starting the real plan, because Temporal replays history against the code.

Why Does Open Innovation Matter?

A group chat holds things people wouldn't put in an app: whose knee is bad, who's broke this month, whose parents set a curfew. That shaped three choices.

Open weights mean a local option exists. The same agent switches to gemma4:e4b on Ollama with one environment variable. I haven't benchmarked it yet, so I won't claim a number, but the switch is in the code, and with it nothing leaves the laptop. A closed API offers no such switch.

Honest trade-off. The runs above used Gemma 4 through the Gemini API's free tier, which says content may be used to improve Google's products. /help says so before anyone joins, and the bot only ever sees what you deliberately send it.

Open infrastructure all the way down. Temporal's open-source server made a durable, multi-day workflow possible at $0 on a laptop. Mastra is Apache-2.0. The places come from OpenStreetMap and the forecasts from Open-Meteo. Total bill: $0.


My Agent Session

I built Quorum with Claude Code, with the Entire CLI capturing every session and linking it to the commit it produced. Every checkpoint is browsable on the entire/checkpoints/v1 branch.

The one I'd point at is the fix for the 53-second poll:

● Checkpoint 199ef0496889
commits 68e95ec fix: longer test-run poll window; drafting alone takes ~2 min on the free tier

Inspect it with entire checkpoint explain 68e95ec in a clone. The decision, why test polls are 7 minutes and not 3, now lives next to the code it explains.


What I Learned

  • Durability is a product feature, not infrastructure. The 06:00 reminder is the only message that gets people out of bed. If it depends on a laptop not sleeping, it isn't a feature.
  • Measure the model before you trust the catalogue. The catalogue left Gemma 4's tool column blank. 8 out of 8 says otherwise for 26B.
  • The weakness belongs in the UI. The plan page shows every failed attempt. A tool that hides its retries is a tool you can't trust at 05:30.

Prize Categories

  • Best Use of Temporal: the whole outing is one workflow (planOuting): vote signals, condition() with a quorum deadline, durable timers to 05:30 and 06:00, and activity retry policies. Survived kill -9 of the worker plus a server restart mid-poll, and retried a dead agent server 4 times before attempt 5 succeeded.
  • Best Use of Mastra: an Agent over Gemma 4 with createTool tools (weather, places), Memory with semantic recall, and structured options that name the limits that shaped them, with code that drops any place the tools didn't return. 0 invented places in 16 drafts.
  • Best Use of MongoDB Atlas: Atlas M0 is the agent's long-term memory (Mastra MongoDBStore) and retrieval layer (Atlas Vector Search via MongoDBVector, 3072-d, index READY). It also stores members, plans with every retry attempt, polls, recaps and the places cache.
  • Best Use of Render: the agent API, the plan pages and /try run on one free web service from one render.yaml, live at quorum-y649.onrender.com. Its 15-minute sleep is designed for: the 45-second timeout plus Temporal retries turn a cold start into a few logged attempts.
  • Best Use of Gemma: gemma-4-26b-a4b-it plans every outing with native tool calls through Mastra. Measured against 31B: 8/8 vs 7/8 valid, 0 invented places each, 128 s vs 202 s median.
  • Best Use of Entire: every agent-written commit has an Entire checkpoint, browsable on the entire/checkpoints/v1 branch, and quoted above to explain why the code is the way it is.

Quorum closes the poll at 22:00 the night before and reminds at 06:00. If you've ever organised a group outing: where would you put the clock? The night before, or 48 hours out, so the people who can't make it say so early?

Top comments (2)

Collapse
 
humam_moin profile image
Humam Moin • • Edited

Love this build! The 'Quorum as a member of your Discord' framing is chef's kiss.

The Temporal + Gemma 4 split is exactly how production AI should be built — deterministic code for time, LLM for reasoning. I've been working on a multi-agent debate app with LangGraph, and your point about retries surviving kill -9 is something I'm now implementing (Circuit Breaker FSM with 60s cooldown).

One question: how did you handle the retry count in the UI? I noticed you show every failed attempt on the plan page was that a conscious choice to build trust, or did it come from debugging needs?

Also, huge respect for the 'Do not let the bot read the chat' decision. Privacy by design

Some comments may only be visible to logged-in visitors. Sign in to view all comments.