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:
-
/join, then one line per limit:/me no car,/me home by 11,/me knee hates long descents. - 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. - 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.
- 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.
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:
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:
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
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.tsandsrc/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
);
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
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.
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.
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:
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:
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%:
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
Guildsintent, no Message Content, so it sees slash commands and taps on its own buttons, nothing else. - Do not store people who didn't
/join./forgetdeletes 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.tswith 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. Survivedkill -9of 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
Agentover Gemma 4 withcreateTooltools (weather, places),Memorywith 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 viaMongoDBVector, 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
/tryrun on one free web service from onerender.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-itplans 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/v1branch, 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)
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.