Build a Slack Channel Digest Bot Using Cloudflare Workers AI Free Tier
What We're Building
A cron-driven Slack bot that wakes up every morning, grabs the last 24 hours of messages from a designated channel, runs them through Cloudflare Workers AI (using the free tier), and posts a clean, structured digest back to the channel. No servers, no databases, no credit card. Just a single wrangler.toml and a few hundred lines of TypeScript.
Feature list:
- Fetches messages from a specific Slack channel (yesterday’s window)
- Strips bot messages, joins, and noise
- Summarizes threads and key decisions using Llama 3 8B on Workers AI
- Posts a formatted digest with headers, bullet points, and action items
- Runs on a cron schedule (e.g., 8 AM daily)
- Entirely within Cloudflare’s free tier limits (100k Workers AI neurons/day, 10ms CPU per invocation)
Architecture and Data Flow
The flow is linear and stateless. Cron fires the Worker’s scheduled handler. The Worker calls Slack’s conversations.history with a time range, strips irrelevant messages, concatenates them into a single prompt, sends it to Workers AI, formats the response into Slack-flavored Markdown, and posts it via chat.postMessage. No R2, no D1, no KV. The only state is the Slack token and channel ID stored as secrets.
Prerequisites (All Free Tier)
| Requirement | How to Get It | Free Tier Limit |
|---|---|---|
| Cloudflare account | dash.cloudflare.com/sign-up | Workers: 100k req/day, AI: 100k neurons/day |
| Node.js 18+ | nodejs.org | N/A |
| Wrangler CLI | npm install -g wrangler | N/A |
| Slack workspace with permissions to create apps | api.slack.com/apps | Free for small workspaces |
Create a Slack App from api.slack.com/apps. Add these Bot Token Scopes under OAuth & Permissions:
channels:historychannels:readchat:write
Install to your workspace and copy the Bot User OAuth Token (starts with xoxb-). Invite the bot to your target channel with /invite @your-bot-name.
Step 1: Scaffold the Worker Project
npm create cloudflare@latest slack-digest-bot -- --type=hello-world
cd slack-digest-bot
npm install
Replace src/index.ts with the skeleton below. We’ll fill in each function as we go.
export interface Env {
SLACK_BOT_TOKEN: string;
SLACK_CHANNEL_ID: string;
AI: Ai;
}
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
const messages = await fetchYesterdaysMessages(env);
if (messages.length === 0) {
await postToSlack(env, "No messages yesterday — enjoy the silence.");
return;
}
const summary = await summarizeWithAI(env, messages);
await postToSlack(env, formatDigest(summary));
},
async fetch(request: Request, env: Env): Promise<Response> {
// Manual trigger for testing
const messages = await fetchYesterdaysMessages(env);
const summary = await summarizeWithAI(env, messages);
return new Response(JSON.stringify({ summary, messageCount: messages.length }), {
headers: { 'Content-Type': 'application/json' },
});
},
};
Step 2: Configure Slack App and Secrets
Set secrets with Wrangler. Never hardcode tokens.
wrangler secret put SLACK_BOT_TOKEN
# Paste your xoxb-... token
wrangler secret put SLACK_CHANNEL_ID
# Paste your channel ID (right-click channel name -> Copy ID)
Update wrangler.toml to bind Workers AI and cron:
name = "slack-digest-bot"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[ai]
binding = "AI"
[triggers]
crons = ["0 8 * * *"]
The cron expression 0 8 * * * fires at 8:00 AM UTC daily. Adjust for your timezone.
Step 3: Fetch Channel Messages via Slack API
Slack’s conversations.history returns messages in reverse chronological order. We’ll ask for the last 24 hours and filter aggressively.
async function fetchYesterdaysMessages(env: Env): Promise<string[]> {
const now = Math.floor(Date.now() / 1000);
const yesterday = now - 86400; // 24 hours ago
const url = new URL('https://slack.com/api/conversations.history');
url.searchParams.set('channel', env.SLACK_CHANNEL_ID);
url.searchParams.set('oldest', yesterday.toString());
url.searchParams.set('latest', now.toString());
url.searchParams.set('limit', '200');
const response = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${env.SLACK_BOT_TOKEN}` },
});
const data = (await response.json()) as SlackHistoryResponse;
if (!data.ok) {
console.error('Slack API error:', data.error);
return [];
}
return (data.messages || [])
.filter((msg) => !msg.subtype) // exclude bot messages, joins, etc.
.filter((msg) => msg.text && msg.text.trim().length > 0)
.map((msg) => {
const time = new Date(parseFloat(msg.ts) * 1000).toISOString();
const user = msg.user || 'unknown';
return `[${time}] <@${user}>: ${msg.text}`;
});
}
interface SlackHistoryResponse {
ok: boolean;
error?: string;
messages?: Array<{
subtype?: string;
text?: string;
ts: string;
user?: string;
}>;
}
Why filter subtype? Slack emits channel_join, channel_topic, bot_message, and other noise. Stripping them keeps the prompt clean and reduces token count — critical for staying under Workers AI’s free-tier context window.
Step 4: Summarize with Workers AI (Llama 3)
Cloudflare Workers AI offers Llama 3 8B for free up to 100k neurons/day. At ~300 neurons per summary, you can run this bot 300+ times daily without hitting the cap.
async function summarizeWithAI(env: Env, messages: string[]): Promise<string> {
const transcript = messages.join('\n');
// Truncate to ~6000 chars to stay safe within context limits
const truncated = transcript.length > 6000
? transcript.slice(0, 6000) + '\n[... truncated ...]'
: transcript;
const prompt = `You are a helpful engineering digest writer. Given the following Slack messages from the last 24 hours, produce a morning digest with these sections:
## Key Decisions Made
## Active Discussions
## Action Items
## Worth Noting
Be concise. Use bullet points. Preserve names when mentioned. Do not hallucinate.
Messages:
${truncated}`;
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [{ role: 'user', content: prompt }],
max_tokens: 1024,
temperature: 0.3,
});
// @ts-ignore — Workers AI types vary by model
return response.response || response.choices?.[0]?.message?.content || 'Summary unavailable.';
}
Why temperature 0.3? You want deterministic, fact-grounded output. Creativity here invents decisions that never happened. Low temperature reduces hallucination risk.
Truncation strategy: Llama 3 8B has an 8k context window, but Workers AI free tier may impose additional limits. 6,000 characters of Slack transcript (~1,500 tokens) leaves ample room for the system prompt and response.
Step 5: Post the Digest Back to Slack
Slack’s Block Kit gives you rich formatting, but a simple mrkdwn string is faster to implement and easier to debug.
async function postToSlack(env: Env, text: string): Promise<void> {
const response = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: env.SLACK_CHANNEL_ID,
text: `*📋 Morning Digest — ${new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}*\n\n${text}`,
mrkdwn: true,
}),
});
const data = (await response.json()) as { ok: boolean; error?: string };
if (!data.ok) {
console.error('Failed to post to Slack:', data.error);
}
}
function formatDigest(rawSummary: string): string {
// Clean up common LLM artifacts
return rawSummary
.replace(/^Here is the morning digest[:\s]*/i, '')
.replace(/^Certainly![:\s]*/i, '')
.trim();
}
The formatDigest function strips LLM pleasantries. Llama 3 loves to start with "Here is the morning digest:" — useful once, annoying every day. Strip it before posting.
Step 6: Wire Up Cron Triggers
Cloudflare Cron Triggers invoke the scheduled handler on your Worker. The wrangler.toml already has the cron expression. One nuance: cron triggers only work on deployed Workers, not in wrangler dev. Test manually via the fetch handler first.
Testing manually:
wrangler dev
# Hit http://localhost:8787 in your browser or with curl
curl http://localhost:8787
This invokes fetch(), which runs the full pipeline and returns JSON. Once you’ve confirmed it works, deploy.
Step 7: Deploy and Verify
wrangler deploy
After deployment, verify the cron is registered:
wrangler tail
Wait for the next scheduled run, or trigger it manually by temporarily changing the cron to * * * * * (every minute) for testing, then revert.
Free tier cost check:
- Workers requests: 1 per cron invocation (<100k/day)
- Workers AI neurons: ~300 per summary (<100k/day)
- No KV, R2, or D1 usage
- Total: well within free limits
Extensions Worth Building
-
Multi-channel support. Accept a comma-separated list of channel IDs in
SLACK_CHANNEL_IDand loop through them. Each gets its own digest. -
Thread-aware summarization. Use
conversations.repliesto pull thread replies and summarize each thread separately before the main digest. Threads are where decisions actually happen. -
Sentiment and urgency flags. Add a second AI call that classifies messages by urgency. Flag messages containing "incident", "down", "broken", or "P0" with a red emoji in the digest.
-
Historical comparison. Store daily digests in Cloudflare D1 (free tier: 5GB, 100k reads/day) and ask the AI to highlight what changed since yesterday’s digest. See our Codebase Q&A Bot guide for patterns on incremental indexing — the same delta-comparison logic applies.
-
Slack slash command for on-demand digests. Map the
fetchhandler to a Slack slash command that triggers an immediate summary for any time range. This is where a bot graduates from cron toy to daily driver.
Common Pitfalls
Bot can’t see messages. The Slack Bot Token needs the channels:history scope, and the bot must be invited to the channel. Without both, conversations.history returns an empty array or not_in_channel error.
Cron timezone mismatch. Cloudflare Cron Triggers use UTC. If your team works in PST, 8 AM UTC is midnight local time. Adjust the cron expression: 0 15 * * * for 8 AM PST (UTC-7).
Workers AI context overflow. The free tier may silently truncate prompts exceeding model limits. If summaries are nonsensical, check your message count. Keep the concatenated transcript under 6,000 characters.
Rate limiting on Slack’s API. Tier 3 limits allow ~50 requests per minute. Your single conversations.history + chat.postMessage pair won’t hit this, but multi-channel loops might. Add a 1-second delay between channels if you scale up.
Secrets not available in wrangler dev. By default, wrangler dev doesn’t load secrets. Use wrangler dev --env development with a .dev.vars file:
SLACK_BOT_TOKEN=xoxb-your-token
SLACK_CHANNEL_ID=C1234567890
FAQ
Q: Why Workers AI instead of OpenAI or Anthropic?
A: Free tier. Workers AI gives you 100k neurons/day with no credit card. Llama 3 8B is more than capable for summarization. If you need higher quality, swapping to @cf/mistral/mistral-7b-instruct-v0.2 is a one-line change. For production workloads, our LLM router deprecation post explains why single-model setups often win.
Q: What if my channel has more than 200 messages per day?
A: Slack’s conversations.history with limit=200 returns the most recent 200 messages. If your channel exceeds that, implement cursor-based pagination using the response_metadata.next_cursor field. Loop until has_more is false.
Q: Can I run this for private channels?
A: Yes. Replace channels:history and channels:read with groups:history and groups:read. The API endpoint changes from conversations.history to conversations.history (it works for both — Slack unified them). Just ensure the bot is added to the private channel.
Q: How do I debug when the digest is empty or wrong?
A: Add a temporary console.log(transcript) before the AI call. Run wrangler tail to see logs in real-time. Also check that your Slack token hasn’t expired — bot tokens don’t expire, but user tokens do.
Q: Will this work for non-English channels?
A: Llama 3 8B handles multiple languages reasonably well. For better multilingual support, switch to @cf/meta/llama-3.1-8b-instruct which has improved multilingual training. The prompt can stay in English — the model will summarize in the language it detects.
Q: How does this compare to Slack’s built-in recap? A: Slack’s recap is generic and channel-agnostic. This bot gives you a channel-specific, decision-focused digest that you control. It’s the difference between a newspaper and a meeting minutes template. When you’re ready to build more sophisticated internal tools, the patterns here — scheduled Workers, AI summarization, Slack API integration — are the same ones we use in our on-call incident summarizer.
Q: What’s the latency from cron fire to digest post? A: Typically 3-8 seconds. Slack API calls are ~200ms each, and Workers AI cold starts add 1-3 seconds. Once warm, the AI inference is ~2-4 seconds for a full transcript. Total is well under Workers’ 30-second CPU time limit.
Q: Can FDE Coach help me extend this for enterprise use? A: Absolutely. The patterns here — free-tier prototyping, cron-driven automation, AI-powered summarization — are exactly what we teach. When you’re ready to productionize with multi-workspace support, Slack Enterprise Grid, and custom fine-tuned models, FDE Coach has the playbooks.
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