Building a Scheduled Standup Bot in Slack That Your Team Won't Mute
Async standup in Slack beats a 9am meeting — if the bot is built right. Here's how to schedule prompts, collect responses, and post a digest people actually read.
- #slack
- #standup
- #async
- #team
- #devops
- #automation
The daily standup meeting is where distributed teams go to waste twenty-five minutes. Half the team is in another timezone, three people are blocked and zoning out, and the one update that mattered gets lost. Async standup in Slack fixes this — if the bot is built right. Built wrong, it’s another notification people mute on day three. The difference is in the details: when it prompts, how it collects, and whether the digest is worth reading.
Here’s how I build a standup bot that survives contact with a real team.
The core loop
A standup bot does four things on a schedule:
- Prompt each member privately at a sensible local time.
- Collect their answers without cluttering a channel.
- Digest the responses into one readable summary.
- Nudge the people who haven’t responded — gently, once.
The mistake most homemade bots make is doing the prompting and collecting in a public channel, which turns standup into a noisy thread nobody scrolls. Collect privately, post publicly. That single decision is most of the battle.
Scheduling the prompt with the right timing
Use Slack’s chat.scheduleMessage or your own cron to fire prompts. The key subtlety is timezone: prompting everyone at 09:00 UTC means someone gets pinged at 2am. Slack exposes each user’s timezone via users.info — use it.
async function scheduleStandupPrompts(members) {
for (const userId of members) {
const { user } = await client.users.info({ user: userId });
const tzOffset = user.tz_offset; // seconds from UTC
const post_at = nextLocalTime(9, 30, tzOffset); // 09:30 their time
await client.chat.scheduleMessage({
channel: userId, // DM the user
post_at,
text: 'Time for standup! Tap below to fill it in.',
blocks: standupPromptBlocks(),
});
}
}
Prompting each person at their 9:30 is the thing that makes people actually answer instead of resenting the bot.
Collect through a modal, not a thread
Give the prompt a button that opens a modal with the three standard fields. A modal keeps responses structured and private until the digest:
app.action('open_standup', async ({ ack, body, client }) => {
await ack();
await client.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
callback_id: 'standup_submit',
title: { type: 'plain_text', text: 'Daily standup' },
submit: { type: 'plain_text', text: 'Submit' },
blocks: [
inputBlock('yesterday', 'What did you do yesterday?'),
inputBlock('today', 'What will you do today?'),
inputBlock('blockers', 'Any blockers?'),
],
},
});
});
On submit, store the response (a small KV store or even an in-memory map keyed by date is fine for one team) and acknowledge privately: “Got it, thanks!” Nobody else sees individual responses until the digest fires.
The digest is the product
At a cutoff time — say 11:00 in the team’s primary timezone — post one digest to the standup channel. This is the only public message of the whole cycle, so make it good:
📋 *Standup — Thu Jun 12*
*Shipped yesterday*
• @ana — finished the rate-limiter, merged #482
• @ben — debugged the flaky deploy job
*Today*
• @ana — start the audit-log relay
• @ben — pairing with @cid on the ArgoCD migration
🚧 *Blockers* (2)
• @ben — needs prod DB access approved
• @cid — waiting on the staging cluster rebuild
🟡 No response: @dev
Surface blockers as their own section at the top of mind — blockers are the entire reason async standup justifies its existence, and burying them defeats the point. A manager skimming this knows in five seconds who’s stuck and on what.
Summarize with AI when the team is big
For a team of five, raw responses are fine. For fifteen, the digest gets long and people stop reading. This is where a summarization pass earns its keep: feed the raw responses to a model and ask it to cluster by theme, pull blockers to the top, and flag anything that looks like a cross-team dependency. Keep that prompt in a versioned library rather than inline — we keep reusable ops prompts for exactly this kind of structured summarization — so you can tune the digest format without redeploying the bot.
Nudge once, then stop
Chase non-responders exactly once, privately, around the cutoff: “Standup digest goes out in 15 min — want to add yours?” Then stop. A bot that nags three times gets muted, and a muted bot collects nothing. Respect that people sometimes skip a day, and mark them “no response” in the digest without ceremony.
Make it configurable, not hardcoded
The bots that survive are the ones a team can self-serve. Expose /standup config to set the channel, the prompt time, the questions, and the member list — stored per channel. The moment another team can adopt your bot without editing your code, it stops being your pet project and starts being infrastructure.
Where to start
Build the minimum loop first: scheduled DM prompt, modal collection, one digest message. Add timezone awareness immediately — it’s the difference between adoption and mutes. Add AI summarization only when the digest gets long enough to need it. For more patterns on scheduled and interactive Slack tooling, see our Slack for ops guides.
Scheduled bots run unattended. Test the timezone math and the digest cutoff before turning it loose on a real team.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.