All articles
Build Guides

Build a Daily Standup Bot with Slack, Supabase & Groq (Free Tier)

FDE Coach EditorialJuly 19, 202611 min read

What We're Building

A zero-cost engineering artifact: a Slack bot that functions as a digital scrum master. Every weekday morning, it DMs each team member a structured prompt for their standup update. It waits for their reply, stores the raw text in Supabase, then fires a Groq LLM call to synthesize a single, high-signal summary. That summary lands in a public #standup channel before the coffee kicks in.

Crisp Feature List:

  • Scheduled DM Collector: Cron-triggered Python process sends a templated Slack DM to a configurable list of users.
  • Ephemeral State Machine: Tracks who has replied in a lightweight in-memory or DB-backed loop to avoid duplicate pings.
  • Supabase Persistence: Raw standup responses are stored in a standups table with user ID, timestamp, and text.
  • Groq-Powered Synthesis: A single call to mixtral-8x7b-32768 or llama3-70b-8192 compresses multiple updates into a concise, action-oriented summary.
  • Public Channel Post: The generated summary is posted to a designated channel, complete with a date header and optional blocker highlights.
  • 100% Free Tier Stack: Groq's free API credits, Slack's free workspace, Supabase's free database, and Python running on a tiny VPS or even a Raspberry Pi.

If you enjoy automating operational workflows, our On-Call Incident Summarizer uses a similar Groq pipeline for postmortem drafts.

Architecture & Data Flow

The orchestration is linear but stateful. The Python process acts as the central conductor: it reaches out to Slack for DM delivery, waits a configurable window for responses, then aggregates and summarizes. No webhook complexity—just a scheduled script that treats Slack's API as a request/response system.

Prerequisites (All Free Tier)

Before writing a single line, grab these accounts and keys. Everything here has a generous free tier that won't cost you a cent for a small to mid-size team.

ResourcePurposeSignup Link
Groq CloudLLM inference for summary synthesisconsole.groq.com
Slack AppBot user, OAuth tokens, DM and channel write scopesapi.slack.com/apps
SupabasePostgreSQL database for standup storagesupabase.com
Python 3.10+Runtime (anywhere with cron)Local, VPS, or GitHub Codespaces

Groq Rate Limits: Free tier offers ~30 requests per minute and ~14,400 per day. A single synthesis call per day is negligible. Use llama3-8b-8192 for cost-free experimentation or mixtral-8x7b-32768 for longer context windows when summarizing large teams.

Slack Scopes Needed:

  • chat:write (send DMs and channel messages)
  • users:read (look up user IDs)
  • users:read.email (optional, for mapping emails to IDs)

Step 1: Provision Supabase and Create the Schema

Log into supabase.com, create a new project, and note your SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY. The service role key bypasses Row Level Security—keep it out of client code.

Run this SQL in the Supabase SQL Editor to create the standups table:

CREATE TABLE standups (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id TEXT NOT NULL,
  user_name TEXT,
  response_text TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_standups_created_at ON standups (created_at DESC);

Why user_id as TEXT: Slack user IDs are strings like U07ABCDEF. No need for UUIDs or foreign keys—we're building a simple log, not an ERP.

Step 2: Bootstrap the Slack App and Secure Tokens

  1. Go to api.slack.com/apps and click Create New AppFrom Scratch.
  2. Name it Daily Standup Bot and pick your workspace.
  3. Under OAuth & Permissions, add the following Bot Token Scopes:
    • chat:write
    • users:read
    • users:read.email
  4. Install the app to your workspace. Copy the Bot User OAuth Token (starts with xoxb-).
  5. Invite the bot to your target channel (#standup) by typing /invite @Daily Standup Bot in that channel.

Critical: The bot must be a member of any channel it posts to. Slack's API will silently fail if the bot isn't in the channel—no helpful error, just a 200 OK with no visible message. Test by manually calling chat.postMessage via their Tester tab.

Step 3: Write the Python Core

Install dependencies:

pip install slack-sdk supabase groq python-dotenv

Create a .env file:

SLACK_BOT_TOKEN=xoxb-your-token
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
GROQ_API_KEY=gsk_your_key

Below is the complete orchestrator. Save it as standup_bot.py.

import os, time
from datetime import datetime, timezone
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from supabase import create_client, Client
from groq import Groq
from dotenv import load_dotenv

load_dotenv()

# --- Config ---
SLACK_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
GROQ_API_KEY = os.getenv("GROQ_API_KEY")

TARGET_CHANNEL = "#standup"
TEAM_MEMBERS = ["U07ABCDEF", "U07GHIJKL"]  # Slack user IDs
DM_WINDOW_SECONDS = 600  # 10 minutes to wait for replies

slack = WebClient(token=SLACK_TOKEN)
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
groq = Groq(api_key=GROQ_API_KEY)

# --- DM Prompt Template ---
DM_PROMPT = (
    "Good morning! :sunny: Time for your daily standup. Reply with:\n"
    "1. What did you accomplish yesterday?\n"
    "2. What are you working on today?\n"
    "3. Any blockers or need for help?"
)

def send_dm(user_id: str) -> str | None:
    """Send standup prompt DM. Returns the DM channel ID on success."""
    try:
        resp = slack.conversations_open(users=[user_id])
        channel_id = resp["channel"]["id"]
        slack.chat_postMessage(channel=channel_id, text=DM_PROMPT)
        print(f"[DM] Sent to {user_id}")
        return channel_id
    except SlackApiError as e:
        print(f"[DM] Failed for {user_id}: {e.response['error']}")
        return None

def collect_replies(dm_map: dict[str, str]) -> list[dict]:
    """Poll DM channels for replies. Returns list of {user_id, text}."""
    deadline = time.time() + DM_WINDOW_SECONDS
    collected = []
    pending = set(dm_map.keys())

    while pending and time.time() < deadline:
        for user_id in list(pending):
            try:
                history = slack.conversations_history(
                    channel=dm_map[user_id], limit=2
                )
                for msg in history["messages"]:
                    if msg.get("user") == user_id and not msg.get("bot_id"):
                        collected.append({"user_id": user_id, "text": msg["text"]})
                        pending.remove(user_id)
                        print(f"[Reply] Collected from {user_id}")
                        break
            except SlackApiError:
                pass
        time.sleep(10)  # Poll interval

    return collected

def save_to_supabase(updates: list[dict]):
    """Persist raw standup responses."""
    for u in updates:
        supabase.table("standups").insert({
            "user_id": u["user_id"],
            "response_text": u["text"],
        }).execute()
    print(f"[DB] Saved {len(updates)} responses")

def synthesize_summary(updates: list[dict]) -> str:
    """Call Groq to synthesize a team standup summary."""
    if not updates:
        return "No standup updates received today."

    combined = "\n\n".join(
        f"User {u['user_id']}: {u['text']}" for u in updates
    )

    system_prompt = (
        "You are a concise scrum master. Summarize the following standup updates "
        "into a single Slack post. Group by theme: accomplishments, planned work, blockers. "
        "Highlight any blockers that need immediate attention. Use emojis sparingly. "
        "Keep the tone professional but warm."
    )

    response = groq.chat.completions.create(
        model="llama3-8b-8192",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": combined}
        ],
        temperature=0.3,
        max_tokens=600
    )

    summary = response.choices[0].message.content
    print(f"[Groq] Summary generated ({len(summary)} chars)")
    return summary

def post_summary(summary: str):
    """Post the synthesized summary to the target Slack channel."""
    today = datetime.now(timezone.utc).strftime("%A, %B %d")
    header = f"*:mega: Standup Summary — {today}*\n\n"
    try:
        slack.chat_postMessage(
            channel=TARGET_CHANNEL,
            text=header + summary
        )
        print(f"[Post] Summary posted to {TARGET_CHANNEL}")
    except SlackApiError as e:
        print(f"[Post] Failed: {e.response['error']}")

# --- Main Orchestration ---
def run_standup():
    print(f"[Start] Standup bot triggered at {datetime.now(timezone.utc).isoformat()}")

    # 1. Send DMs
    dm_map = {}
    for uid in TEAM_MEMBERS:
        channel = send_dm(uid)
        if channel:
            dm_map[uid] = channel

    if not dm_map:
        print("[Error] No DMs sent successfully. Aborting.")
        return

    # 2. Collect replies
    updates = collect_replies(dm_map)

    # 3. Persist
    save_to_supabase(updates)

    # 4. Synthesize
    summary = synthesize_summary(updates)

    # 5. Post
    post_summary(summary)

    print("[Done] Standup cycle complete.")

if __name__ == "__main__":
    run_standup()

Design Decisions:

  • Polling, not Events: We poll conversations.history instead of using Slack's Events API. This avoids needing a public HTTP endpoint and keeps the bot runnable from any cron-capable machine.
  • 10-Minute Window: Adjust DM_WINDOW_SECONDS based on team responsiveness. A shorter window means faster summary; a longer window catches stragglers.
  • Groq Model Choice: llama3-8b-8192 is blazing fast (sub-second) and free-tier friendly. Swap to mixtral-8x7b-32768 if you have 10+ team members with verbose updates.

For a deeper dive into synthesizing operational data with Groq, see our On-Call Incident Summarizer guide.

Step 4: Deploy the Cron Scheduler

The bot is a single Python script. It needs to run once per weekday. The simplest free approach:

Option A: GitHub Actions (Free) Create .github/workflows/standup.yml:

name: Daily Standup
on:
  schedule:
    - cron: '30 8 * * 1-5'  # 8:30 AM UTC, Mon-Fri
jobs:
  standup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install slack-sdk supabase groq python-dotenv
      - run: python standup_bot.py
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
          GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}

Add your secrets in the repository's Settings → Secrets and Variables → Actions.

Option B: Local Cron (Raspberry Pi / Old Laptop)

crontab -e
# Add: 30 8 * * 1-5 cd /path/to/bot && python standup_bot.py

Option C: Supabase Edge Function (Cron) Supabase now offers built-in cron triggers via pg_cron. You can invoke a Deno Edge Function on a schedule, but Python support is limited. Stick with GitHub Actions for zero-infrastructure deployment.

Running the Bot End-to-End

  1. Set environment variables in your chosen runtime.
  2. Update TEAM_MEMBERS with actual Slack user IDs. Find them by clicking a user's profile → Copy Member ID (you may need to enable this in Slack's advanced settings).
  3. Test manually first: Run python standup_bot.py from your terminal. Check that DMs arrive, replies are collected, and the summary posts to #standup.
  4. Enable the cron and verify the next morning.

Sensible Extensions

  • User Name Resolution: Call slack.users_info(user=uid) to fetch real_name and include it in the summary for human-readable output.
  • Blocker Escalation: If Groq detects a blocker (prompt it to return structured JSON), automatically @channel or DM a lead.
  • Standup History Dashboard: Build a simple frontend on Supabase's REST API to view past standups. This is a natural next step for an FDE who wants visibility into team throughput patterns.
  • Multi-Timezone Support: Offset the cron per user or stagger DM sends based on timezone data from Slack profiles.
  • Sentiment Tracking: Use Groq to score each update's sentiment and track team morale over time in a separate Supabase table.

Common Pitfalls and Debugging

PitfallSymptomFix
Bot not in channel200 OK but no message appears/invite @bot in the target channel
Expired Slack tokeninvalid_auth errorReinstall the Slack app, copy new token
Groq rate limit429 errorsAdd time.sleep(2) between retries or switch to llama3-8b
Supabase RLS blocking insert401 or empty insertUse the service role key, not the anon key
GitHub Actions timezoneCron runs at UTC, not localAdjust the cron expression; 8:30 UTC = 4:30 AM ET
DM window too shortMissing replies from slow respondersIncrease DM_WINDOW_SECONDS to 900 (15 min)

FAQ

Q: Can I use this with a team spread across time zones? Yes. Fetch each user's tz from users.info and stagger DM sends. The collector loop can run longer to accommodate late replies.

Q: What if someone doesn't reply? The summary will simply omit them. Optionally, post a note: "No update from @user today." Add a pending set check after the window expires.

Q: Is Groq really free for this? Absolutely. Groq's free tier is generous. A single synthesis call of ~500 tokens per day is well within limits. You won't hit the rate cap even with retries.

Q: How do I add more team members? Update the TEAM_MEMBERS list. For dynamic teams, fetch the list from a Slack User Group or a Supabase config table.

Q: Can I store standups in a different database? Yes. The Supabase client is just a PostgreSQL wrapper. Swap it for psycopg2, SQLite, or even a JSON file. Supabase is chosen for its free tier and hosted convenience.

Q: What if the bot posts the same summary twice? Add a deduplication check: before posting, query Supabase for a summary with today's date. If one exists, skip or append an "Updated" note.


Ready to level up your automation skills? FDE Coach builds engineers who ship internal tools like this in their first week. Check out our guide on what a Forward Deployed Engineer actually does in a week to see where projects like this fit into the broader FDE workflow.

#standup#slack-bot#groq#supabase#team-productivity

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now