Building a Winter Olympics Slack Bot with AI-Generated Bilingual Messages
Building a Winter Olympics Slack Bot with AI-Generated Bilingual Messages
By Dave Bitter
6 min read
A deep dive into building a Slack bot that keeps colleagues informed about Dutch athletes at the 2026 Winter Olympics, featuring AI-generated bilingual messages, dynamic schedule discovery, and enthusiastic sports commentary.

- Authors

- Name
- Dave Bitter
A few days ago, our office manager approached me with a fun challenge. He's planning to broadcast the 2026 Winter Olympics on the TV in our office bar whenever Dutch athletes compete. Great idea! But there's a problem: how do colleagues know when to drop what they're doing and head to the bar?
The Netherlands is a speed skating powerhouse. With 133 all-time Olympic medals in the sport (48 gold!), there will be plenty of events worth watching. But Olympic schedules are confusing, and nobody wants to miss Suzanne Schulting racing for gold because they were stuck in a meeting.
The solution? A Slack bot that notifies everyone right before Dutch events start. And because I can't resist overengineering things, I decided to make it generate enthusiastic sports commentary using AI. Like having a virtual Dutch commentator in your pocket!
What I Built
The bot does three main things:
- Morning Digest - Every day at 8 AM, it sends a summary of all Dutch events scheduled for that day
- Pre-Event Reminders - 30 minutes before each event, it sends an urgent "get to the bar!" notification
- Live Results Checker - It automatically discovers when Dutch athletes qualify for finals and announces medals
The messages are bilingual (English first, then Dutch) and packed with context: venue information, athlete storylines, records to beat, and fun facts about the host cities.
Here's what a reminder looks like:
β° STARTING IN 30 MINUTES!
Women's 3000m Speed Skating
π Milano Santa Giulia Arena - Milan
STARTING IN 30 MINUTES! Buckle up, team, because Joy Beune is about
to dominate the Women's 3000m Speed Skating at 16:00! π³π± The
'Allround Queen' is ready to transform her 1500m dominance into a
medal-winning performance. Will Joy break the Olympic record of
3:52.02 set in Vancouver? Grab your seat and cheer!
π Olympic record: 3:52.02 - Martina SΓ‘blΓkovΓ‘ (CZE) - Vancouver 2010
Generated by Bonzai β’ May contain inaccuracies
βββββββββββββββββββββββββ
β° BEGINT OVER 30 MINUTEN!
Vrouwen 3000m Schaatsen
π Milano Santa Giulia Arena - Milaan
Maak je klaar, team! Joy Beune gaat knallen in de 3000m vrouwen
schaatsen om 16:00! π³π± De 'Allround Koningin' staat klaar om haar
dominantie om te zetten in een medaille. Zet je schrap en juich
voor Nederland!
π Olympisch record: 3:52.02 - Martina SΓ‘blΓkovΓ‘ (CZE) - Vancouver 2010
Gegenereerd door Bonzai β’ Kan onnauwkeurigheden bevatten
No boring "Event starting soon" messages here. We're going full commentator mode!
AI Content Generation with Bonzai
For the AI-powered content, I used Bonzai. For those unfamiliar, Bonzai is iO's internal AI platform that we develop and use across projects, making it easy to integrate AI capabilities into our applications.
The key to getting good output is providing rich context. I didn't just ask "write a message about a speed skating event." Instead, I built a comprehensive prompt with athlete backgrounds, venue details, historical records, and even personality traits:
const prompt = `You are an ENTHUSIASTIC Dutch sports commentator writing
a morning message for a Slack channel about today's Winter Olympics events.
DATE: ${daySchedule.date_display}
TODAY'S EVENTS:
${eventSummaries}
ATHLETE BACKGROUNDS:
${athleteContext}
VENUE INFORMATION:
${venueContexts}
NETHERLANDS OLYMPIC HISTORY:
- Total Winter Olympic medals: ${history.total_winter_medals}
- ${history.speed_skating_dominance}
INSTRUCTIONS:
1. Write an EXCITING morning digest in the style of an enthusiastic
Dutch sports commentator
2. Include the schedule with times and athletes
3. Highlight the most dramatic storylines
4. Mention venue/city interesting facts
5. Use emojis sparingly (2-3 per section max)
6. Keep the energy HIGH but professional
Write TWO versions:
1. ENGLISH version first
2. Then DUTCH version
`
The response comes back as structured JSON with both language versions, which makes parsing straightforward.
The Bilingual Challenge
Here's something I learned the hard way: literal translations don't work for sports commentary.
Early versions of the bot would translate "Tune in NOW!" as "Stem af NU!" in Dutch. Technically correct, but no Dutch person would ever say that. It sounds robotic and weird.
The fix was adding explicit instructions to the prompt:
IMPORTANT FOR DUTCH VERSION:
- Write naturally, NOT literal translations!
- Dutch people don't say "stem af" for "tune in"
- Use actual Dutch sports expressions: "Kijk mee!", "Zet 'm op!",
"Kom op!", "Niet missen!"
Now the bot uses expressions that actually sound like something a Dutch commentator would say. Small detail, but it makes a huge difference in how the messages feel.
Dynamic Schedule Discovery
Here's where it gets interesting. At the start of the Olympics, we know when the heats and quarterfinals are. But we don't know which Dutch athletes will qualify for the finals. Those events don't exist in our schedule yet!
The solution: use AI web search to discover new qualifications as they happen.
Every 30 minutes during the Olympics, a cron job runs that asks Bonzai to search the web for Dutch Olympic results. If it finds that someone qualified for a final, it:
- Adds the new event to our dynamic schedule (stored in Vercel Blob)
- Sends a celebratory Slack message
- The new event automatically appears in future morning digests and reminders
// Use Bonzai to search for new events
const discoveryResult = await discoverNewEvents(todayISO)
// Process discovered events
for (const foundEvent of discoveryResult.found_events) {
// Add to dynamic schedule
const savedEvent = await addDiscoveredEvent(foundEvent.date, eventData)
// Send Slack notification
const message = await generateDiscoveryMessage(savedEvent)
await sendSimpleMessage(message)
// Record notification to prevent duplicates
await recordNotification(eventId, 'discovery')
}
The flow looks like this:
βββββββββββββββββββ ββββββββββββββββ βββββββββββββββββ
β Cron Job ββββββΆβ Bonzai ββββββΆβ Vercel Blob β
β (every 30 min) β β Web Search β β Store events β
βββββββββββββββββββ ββββββββββββββββ βββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β All Features Updated β
β Morning Digest β Event Reminders β Schedule View β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Preventing Duplicate Messages
One challenge with serverless functions: there's no persistent state between invocations. If the cron job runs twice and finds the same qualification result, we don't want to spam Slack with duplicate messages.
The solution is simple: store a log of sent notifications in Vercel Blob and check it before sending anything new.
export async function hasNotificationBeenSent(eventId: string): Promise<boolean> {
const data = await getDynamicData()
return data.notifications.some((n) => n.event_id === eventId)
}
export async function recordNotification(
eventId: string,
type: 'discovery' | 'medal' | 'update'
): Promise<void> {
const data = await getDynamicData()
data.notifications.push({
event_id: eventId,
sent_at: new Date().toISOString(),
type,
})
await saveDynamicData(data)
}
Before sending any message, we generate a unique ID for the event and check if it's already in the log. Simple, but effective.
Smart Cron Scheduling
Running AI-powered web searches every 15 minutes, 24/7, would be wasteful. Most of the time there's nothing new to find.
Instead, the live results checker only runs when it makes sense:
- Only during daytime hours (7:00-22:00 CET) when events actually happen
- Only when qualification events are scheduled for that day
- Only within a time window after those events when results would be available
// Only call Bonzai if there are qualification events today
if (!hasQualificationEventsToday()) {
return res.status(200).json({
type: 'skipped',
reason: 'No qualification events today',
})
}
if (!isWithinQualificationWindow()) {
return res.status(200).json({
type: 'skipped',
reason: 'Not within qualification event window',
})
}
// Now it's worth checking for new results
const discoveryResult = await discoverNewEvents(todayISO)
This keeps costs down and avoids unnecessary API calls.
The Data Layer
There's no official API for Netherlands Olympic athlete data, so I had to get creative. I scraped the internet for all the information I needed: athlete profiles from Wikipedia, event schedules from olympics.com, venue details from news articles, and historical medal counts from various sports databases. All of this was compiled into a single JSON file that includes:
- Schedule: All known Netherlands events with ISO dates and times
- Athletes: Profiles with achievements, storylines, and personality traits
- Venues: Milano Santa Giulia Arena, Cortina Sliding Centre, with fun facts
- Cities: Milan and Cortina d'Ampezzo background information
- Records: Olympic and world records for each event
- History: Netherlands' Olympic history and medal counts
Having rich data means the AI can generate contextually relevant messages. It knows that Joy Beune is the "Allround Queen" and that Suzanne Schulting had a breakthrough gold in PyeongChang 2018. This context makes the messages feel informed rather than generic.
Things I Learned
1. Prompt engineering for bilingual content is tricky
You can't just ask for translations. You need to explicitly instruct the AI to write naturally in each language, with examples of what good output looks like.
2. Vercel Blob is perfect for simple serverless state
No need for a full database when you just need to store some JSON. Vercel Blob is simple, fast, and integrates seamlessly with Vercel Functions.
3. Sports commentary style makes messages engaging
Nobody wants to read "Event: Women's 500m. Time: 16:00. Athletes: Femke Kok." The commentator style takes more effort but makes the bot actually fun to follow.
4. Small disclaimers build trust
Every message includes "Generated by Bonzai - May contain inaccuracies." AI can hallucinate, and being upfront about that builds trust with users.
5. Rich context in, rich content out
The more background information you give the AI, the better the output. Athlete storylines, venue history, and Olympic records all contribute to messages that feel researched and valuable.
Tech Stack
- Runtime: Vercel Serverless Functions
- Language: TypeScript
- AI: Bonzai (iO's AI platform)
- Messaging: Slack Incoming Webhooks
- Storage: Vercel Blob for dynamic events and notification tracking
- Scheduling: Vercel Cron
Wrapping Up
What started as "notify people when to go to the bar" turned into a fun exploration of AI-generated content, dynamic data merging, and bilingual prompt engineering.
The real magic is in combining structured data with AI generation. The bot doesn't just say "there's an event." It knows the athletes, their stories, the venue history, and the records to beat. That context transforms a simple notification into something people actually want to read.
Will it get people to the office bar on time? I guess we'll find out in February! If you're at the iO office during the Olympics, keep an eye on Slack. You might just catch Suzanne Schulting winning gold.
π³π± Hup Holland Hup!