Build a Daily Standup Bot: Collect Updates via DM, Summarize with Groq, Post to Slack
What We're Building
We're building a fully automated daily standup bot that runs on 100% free infrastructure. The bot will:
- Direct Message (DM) team members at a scheduled time (e.g., 9:00 AM) asking for their standup updates.
- Collect text responses in a persistent database (Supabase) as users reply.
- Wait for a cutoff time (e.g., 9:30 AM).
- Fetch all collected updates, pipe them through Groq's Mixtral model to distill them into a single, coherent summary.
- Post the summary to a designated public Slack channel (e.g.,
#daily-standup).
This mimics the "async standup" pattern used by high-performance remote engineering teams, giving you a searchable log of what the team is shipping every day without the synchronous interruption.
Architecture Overview
Before we touch a line of code, here is the data flow. The system relies on two separate n8n workflows to avoid long-running processes and respect Slack's 3-second response timeout for interactive messages.
Workflow 1 (DM Collector): Triggered by the morning schedule. It iterates through a list of Slack user IDs, sends a DM, and waits. A separate webhook or Slack event listener captures the reply and inserts it into Supabase.
Workflow 2 (Summarizer): Triggered 30 minutes later. It queries Supabase for today's entries, constructs a prompt, sends it to Groq, and posts the result to the channel.
Prerequisites (All Free Tier)
You need four accounts. Every single one has a generous free tier that covers this project completely.
- n8n: You can self-host via Docker on a free-tier cloud VM (e.g., Oracle Cloud Always Free) or use a local tunnel. For this guide, we'll assume you have an n8n instance running. n8n Quickstart
- Groq Cloud: We'll use Mixtral-8x7b. Sign up at console.groq.com and generate an API key.
- Slack: You need a workspace where you can create apps. Go to api.slack.com/apps.
- Supabase: Go to supabase.com and create a new project. The free tier gives you a full Postgres database and 500MB of space.
Internal Tooling Tip: If you're enjoying stitching together AI agents and APIs like this, you are fundamentally doing Forward Deployed Engineering. The difference between a hobbyist and an FDE is understanding the enterprise context. For a deep dive into the reality of the role, check out our diary-based breakdown: /blog/fde-week-in-life-reality.
Step 1: Slack App Configuration and Token Scopes
We need a Slack app with specific permissions to send DMs, read replies, and post to channels.
- Navigate to api.slack.com/apps and click Create New App > From Scratch.
- Name it "Daily Standup Bot" and select your workspace.
- Go to OAuth & Permissions in the sidebar.
- Under Bot Token Scopes, add the following:
users:read(to look up user IDs)users:read.email(optional, useful for mapping)im:write(to send DMs)im:history(to read DM replies)chat:write(to post to public channels)channels:read(to find the channel ID)
- Click Install to Workspace. Copy the Bot User OAuth Token (
xoxb-...). - Enable Event Subscriptions. Set the Request URL to your n8n webhook URL (we'll configure this later). Subscribe to
message.imbot events. This allows n8n to instantly capture DMs without polling.
Step 2: Setting Up a Supabase Table for Standup Responses
We need a simple table to store responses so the Summarizer can query them later.
-
In your Supabase dashboard, go to the SQL Editor.
-
Run the following DDL:
create table standup_responses ( id bigint generated by default as identity primary key, user_id text not null, user_name text, response_text text not null, standup_date date not null default current_date, created_at timestamptz not null default now() ); -- Index for fast lookup by date create index idx_standup_date on standup_responses(standup_date); -
Go to Project Settings > API. Copy your Project URL and the
service_rolekey (oranonkey, but ensure Row Level Security allows inserts). For simplicity in a private bot, we'll use theservice_rolekey in n8n credentials.
Step 3: Building the DM Collector Workflow in n8n
This workflow fires at 9:00 AM. It sends a beautifully formatted DM to every engineer on the list.
Node 1: Schedule Trigger
- Node:
Schedule Trigger - Rule: Every Day at 09:00 AM.
Node 2: Code Node (User List) We'll hardcode a list of Slack Member IDs. To find a user's ID, go to their Slack profile > More > Copy Member ID.
// Returns an array of objects for the Loop Over Items node
return [
{ userId: "U01A1B2C3D4", name: "Alice" },
{ userId: "U05E6F7G8H9", name: "Bob" },
{ userId: "U10I11J12K13", name: "Charlie" }
];
Node 3: Loop Over Items (Split In Batches)
- Connect the Code node to a
Split In Batchesnode. This iterates over the array.
Node 4: Slack > Message > Post (DM)
- Resource:
Message> Operation:Post. - Channel Type:
User. - User ID:
{{ $json.userId }}. - Text: Use a block kit builder for a nice message. Here is a simple Markdown string:
:wave: Good morning, {{ $json.name }}! Time for the daily standup.
Please reply to this message with your update:
1. What did you ship yesterday?
2. What are you shipping today?
3. Any blockers?
Node 5: Webhook (Capture Reply) This is a separate workflow or a separate trigger in the same workflow. The best pattern is a Production n8n instance with a distinct "Listener" workflow.
- Create a new Workflow: "Standup Reply Listener".
- Trigger:
Slack Trigger (Beta)orWebhook. - If using Webhook: Configure the URL in Slack's Event Subscriptions for
message.im. - Filter: Ensure the message is from a user we care about and not the bot itself.
- Use a Supabase node to
Inserta row:- Table:
standup_responses - Data:
{ "user_id": "{{ $json.event.user }}", "user_name": "{{ $json.event.user_name }}", "response_text": "{{ $json.event.text }}", "standup_date": "{{ $today.format('YYYY-MM-DD') }}" }
- Table:
Step 4: Building the Morning Summarizer Workflow in n8n
This workflow fires at 9:30 AM. It reads the database, hits Groq, and posts the summary.
Node 1: Schedule Trigger
- Rule: Every Day at 09:30 AM.
Node 2: Supabase > Execute SQL We query for today's responses. If no one responded, we should handle that gracefully.
SELECT user_name, response_text
FROM standup_responses
WHERE standup_date = CURRENT_DATE
ORDER BY created_at ASC;
Node 3: IF Node (Check for Empty)
- Condition:
{{ $json.length === 0 }} - If
true, use a Slack node to post a "No updates submitted today" message to the channel and terminate.
Node 4: Code Node (Format Prompt) We need to merge the rows into a single text block for the LLM.
const items = $input.all();
if (items.length === 0) return [{ prompt: "" }];
let standupLog = "";
items.forEach((item, index) => {
standupLog += `Team Member ${index + 1} (${item.json.user_name}):\n${item.json.response_text}\n\n`;
});
const systemPrompt = `You are a technical lead summarizing daily standup updates.
Analyze the raw updates below. Generate a concise, bullet-point summary for the team channel.
Group items into: "Shipped Yesterday", "Shipping Today", and "Blockers/Risks".
Maintain a professional, supportive tone. Do not hallucinate details not in the updates.`;
return [{
system: systemPrompt,
user: standupLog
}];
Node 5: Groq Node (HTTP Request or Community Node) If you don't have the community node, use a standard HTTP Request node.
- Method:
POST - URL:
https://api.groq.com/openai/v1/chat/completions - Headers:
Authorization: Bearer {{ $env.GROQ_API_KEY }}Content-Type: application/json
- Body (JSON):
{
"model": "mixtral-8x7b-32768",
"messages": [
{ "role": "system", "content": "{{ $json.system }}" },
{ "role": "user", "content": "{{ $json.user }}" }
],
"temperature": 0.3,
"max_tokens": 1024
}
Node 6: Code Node (Extract Text) Parse the Groq response.
const body = $input.first().json;
const summary = body.choices[0].message.content;
return [{ summary: summary }];
Node 7: Slack > Message > Post (Channel)
- Channel ID: Your public channel (e.g.,
C12345678). - Text:
:robot_face: *Daily Standup Summary - {{ $today.format('MMMM Do') }}*
{{ $json.summary }}
---
_Automated by the FDE Standup Bot. Have a great day._
Enterprise Context: This pattern of scraping logs, formatting prompts, and summarizing them is identical to what we do in incident management. If you want to see how this scales to production systems, read our case study on summarizing on-call incidents: /blog/build-an-incident-summarizer-from-logs-with-whisper-and-gemini.
Step 5: Testing and Running the Bot
- Test the DM Flow: In n8n, click "Test Workflow" on the DM Collector. Check if you received the DM in Slack. Reply to it.
- Verify Database: Check your Supabase table. You should see a row with your reply.
- Test Summarizer: Manually execute the Summarizer workflow. Check the target Slack channel for the summary.
- Activate: Toggle both workflows to "Active" in n8n.
Sensible Extensions
Once the basic loop works, you can harden it with these features:
- Retry Logic: If a user doesn't reply by 9:25 AM, send a follow-up DM.
- Sentiment Analysis: Use Groq to detect negative sentiment in "Blockers" and automatically tag the Engineering Manager in the summary post.
- Thread Summaries: Instead of DMs, have users reply in a Slack thread. The bot scrapes the thread. This is slightly more complex to parse but more transparent.
- Voice Notes: Use a Slack file webhook to capture audio clips, transcribe them with Whisper (via Groq), and include them in the summary.
Skill Path: Building these internal tooling workflows puts you squarely on the path to Forward Deployed Engineering. The role is about exactly this: stitching APIs and LLMs to solve real operational bottlenecks. If you are curious how this differs from traditional consulting, we broke down the operating model here: /blog/fde-consultant-comparison-differences.
Common Pitfalls and Debugging
- "Not Scoped" Error: Your Slack bot token is missing
im:writeorchat:write. Re-install the app to refresh the token. - Duplicate Messages: If you don't filter out the bot's own user ID in the webhook listener, it might capture its own prompt and create a loop. Always add a filter node:
if ($json.event.user === 'BOT_USER_ID') return false; - Supabase RLS Violations: If you use the
anonkey, you must enable Row Level Security policies that allow inserts. For an internal bot, using theservice_rolekey bypasses RLS and is simpler. - Groq Rate Limits: The free tier is generous but has limits on requests per minute. If you have a large team, batch the updates into a single request rather than sending one per user.
- Empty Summaries: If Mixtral outputs "No updates provided," your Code node might be passing empty strings. Check the output of the SQL node in the execution log.
FAQ
Q: Can I use OpenAI instead of Groq? A: Yes, but it won't be free. Groq's API is compatible with the OpenAI SDK/format, so you just change the URL and model name. Mixtral via Groq is extremely fast and currently has a very generous free tier.
Q: How do I find the Channel ID for the summary post?
A: In Slack, right-click the channel name > "View channel details". Scroll to the bottom. The ID starts with C.
Q: What if a team member is on vacation? A: You can maintain a "PTO" list in a Code node. If the user ID matches a vacation list, skip sending the DM. Otherwise, the bot will just report that they didn't submit an update.
Q: Is it safe to hardcode API keys in n8n?
A: No. Use n8n's Environments feature (Variables) or store them as Credentials. The $env.GROQ_API_KEY syntax used above reads from your n8n instance's environment variables.
Q: Can this work for multiple timezones? A: Yes. You'd need to group users by timezone offset and have multiple Schedule Triggers (e.g., 9:00 AM EST, 9:00 AM PST). The Supabase table stores the date in UTC by default, so adjust your SQL query accordingly.
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