Tools
Tools: How to Build an AI Slack Bot with Claude, GPT, Grok, & Gemini Using Hot Dev
2026-02-12
0 views
admin
What You'll Build ## Prerequisites ## Step 1: Create a Slack App ## Step 2: Set Up the Project ## Step 3: Understand the Bot Code ## Imports ## AI Model Selection ## AI Dispatch ## Handling Messages ## Polling for Messages ## Step 4: Run Locally ## Set Context Variables ## Test It ## Switch AI Models ## Taking It to Production ## Sign Up and Get an API Key ## Set Context Variables ## Comment Out Polling ## Deploy ## Configure Slack Webhooks ## How the Webhook Handler Works ## Get Started What if you could build a Slack bot that talks to Claude, GPT, Grok, and Gemini — and switch between them with a single chat command? And what if the whole thing fit in a single file? That's what we're building today. No framework. No boilerplate. Just Hot Dev. Full source code: github.com/hot-dev/hot-demos/tree/main/slack-bot An AI-powered Slack bot that: We'll get this running locally first, then deploy it to production with real-time webhooks. Before we start, you'll need: First, we need a Slack app with the right permissions. Next, add the bot permissions. Go to OAuth & Permissions and add these Bot Token Scopes: Now install the app to your workspace: Finally, invite the bot to a channel: Note the Channel ID — you can find it by right-clicking the channel name → "View channel details" → the ID is at the bottom (starts with C). The project configuration lives in hot.hot. Here's the key part — the dependencies: These are Hot Dev packages — first-class integrations for Slack and all four AI providers. You don't need to install them separately. Hot Dev resolves them automatically. The entire bot lives in one file: hot/src/slack-bot/bot.hot. Let's look at the key parts. If you're new to Hot: :: denotes a namespace path. ::channels ::slack::channels creates a short alias so we can write ::channels/conversations-history(...) instead of the full ::slack::channels/conversations-history(...). No import keyword, no curly braces — just alias full-namespace-path. The bot lets users switch AI models live in the Slack channel with !ai commands. Here's how that's configured: Notice: in Hot, assignment uses a space, not =. MODEL_ALIASES {…} means "bind the name MODEL_ALIASES to this map." This is one of Hot's core syntax rules — no equals sign. This is the cleanest part. Hot's match flow dispatches to the right AI provider: Four AI providers, four lines. Each Hot Dev AI package exposes the same chat(model, message, system) interface, so switching providers is just matching on the enum. The handle-message function does the heavy lifting. It checks for !ai commands first, then falls through to the AI reply: The cond flow is Hot's branching construct — it evaluates conditions top-to-bottom and takes the first match. The => with no condition at the end is the default branch. One clever detail: detect-selection scans the last 100 messages in the channel for the most recent !ai command. The model selection is stored in the chat history itself — no database, no state file, no Redis. Just Slack messages. For local development, the bot polls the channel on a schedule: The meta block is how Hot attaches metadata to functions. Here it says: "Run this every 15 seconds, and also run it when someone sends the slack-bot:check event." You can also trigger it manually at any time with hot eval 'send("slack-bot:check")'. One thing you might notice: there are no println or logging statements anywhere in the code. That's because Hot Dev lets you inspect every function call, its arguments, return values, and timing — all from the app. No manual logging needed. That's all the code you need to understand for now. Let's run it. Start the dev server: This starts the Hot Dev runtime locally and opens the app in your browser at http://localhost:4680. Hot Dev uses context variables for configuration and secrets. In the app, go to Context Variables and set the following: If you're using a different AI provider as your default, set that provider's key instead (e.g., openai.api.key, xai.api.key, or gemini.api.key). The bot will check the channel every 15 seconds. To trigger a check immediately: Type a message in your Slack channel and wait for the bot to reply. Type !ai in the Slack channel to see the current model and all options: The bot replies with the active model and a list of available providers. To switch: The selection sticks — the bot scans the channel history for the most recent !ai command and uses that model for all replies. No config change, no restart. Just type a command. At this point you have a working AI Slack bot. But there's a catch: it's polling every 15 seconds. That's fast enough for testing, but not ideal for production — it makes unnecessary API calls when no one is talking, and there's still a small delay. The fix is webhooks. Instead of polling, Slack sends your bot a message the instant someone types in the channel. The response is immediate. The reason we can't use webhooks during local development is simple: Slack's Events API needs a public URL to send events to, and it can't reach localhost. To use webhooks, you need to deploy your bot to Hot Dev Cloud. Hot Dev gives your bot a public URL and handles scaling — all with a single command. Same as local — go to Context Variables in the Hot Dev App and set the same keys: Since webhooks handle messages in real time, you don't need the polling schedule in production. Comment it out in bot.hot: The function still exists — you can trigger it manually with hot eval if you ever need to — but it won't run on a schedule. That's it. Your bot is live. Now tell Slack to send events to your bot in real time: Note: Subscribing to an event locks its required scope — you won't be able to remove the scope until you remove the event subscription first. The bot already has the webhook code — it just wasn't doing anything during local development. Here's what kicks in when deployed: The meta { webhook: {...} } block tells Hot Dev to register this function as an HTTP endpoint. In production, it gets a public URL automatically. The handler verifies Slack's request signature before processing — this prevents unauthorized requests. Since you commented out the polling schedule, only the webhook handler is active in production. Here's the full picture: Both sides call the same handle-message() function. The only difference is how messages arrive. Can I use this with a private channel?
Yes. Add the groups:history and groups:read scopes to your Slack app and invite the bot to the private channel. How do I change the default AI model?
Edit the DEFAULT_SELECTION line in bot.hot. Set service to one of "Anthropic", "OpenAi", "Xai", or "Gemini" and model to the model name you want. What if my AI API key is missing?
The bot will return an error for that provider. It won't crash — Hot's error handling propagates the failure as a Result. The other providers still work fine. Is Hot Dev free to use?
Hot Dev is free for local development. See hot.dev/pricing for cloud deployment options. Install Hot Dev and try it yourself: hot.dev/download Built something cool with Hot Dev? Share it with us on X @hotdotdev — we'd love to see it. What would you build with Hot Dev? Drop a comment — I'd love to hear what integrations you'd tackle first. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
/invite @HotAIBot Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
/invite @HotAIBot CODE_BLOCK:
/invite @HotAIBot COMMAND_BLOCK:
git clone https://github.com/hot-dev/hot-demos.git
cd hot-demos/slack-bot Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
git clone https://github.com/hot-dev/hot-demos.git
cd hot-demos/slack-bot COMMAND_BLOCK:
git clone https://github.com/hot-dev/hot-demos.git
cd hot-demos/slack-bot CODE_BLOCK:
hot.project.slack-bot.deps { "hot.dev/hot-ai": "1.0.0", "hot.dev/slack": "1.0.4", "hot.dev/anthropic": "1.0.3", "hot.dev/openai": "1.0.4", "hot.dev/xai": "1.0.3", "hot.dev/gemini": "1.0.3"
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
hot.project.slack-bot.deps { "hot.dev/hot-ai": "1.0.0", "hot.dev/slack": "1.0.4", "hot.dev/anthropic": "1.0.3", "hot.dev/openai": "1.0.4", "hot.dev/xai": "1.0.3", "hot.dev/gemini": "1.0.3"
} CODE_BLOCK:
hot.project.slack-bot.deps { "hot.dev/hot-ai": "1.0.0", "hot.dev/slack": "1.0.4", "hot.dev/anthropic": "1.0.3", "hot.dev/openai": "1.0.4", "hot.dev/xai": "1.0.3", "hot.dev/gemini": "1.0.3"
} CODE_BLOCK:
::slack-bot::bot ns // Namespace aliases — short names for the packages we use
::channels ::slack::channels
::messaging ::slack::messaging
::misc ::slack::misc
::webhooks ::slack::webhooks
// ... other aliases (see full code on GitHub) // AI provider aliases
::anthropic-chat ::anthropic::messages
::openai-chat ::openai::chat
::xai-chat ::xai::responses
::gemini-chat ::gemini::chat Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
::slack-bot::bot ns // Namespace aliases — short names for the packages we use
::channels ::slack::channels
::messaging ::slack::messaging
::misc ::slack::misc
::webhooks ::slack::webhooks
// ... other aliases (see full code on GitHub) // AI provider aliases
::anthropic-chat ::anthropic::messages
::openai-chat ::openai::chat
::xai-chat ::xai::responses
::gemini-chat ::gemini::chat CODE_BLOCK:
::slack-bot::bot ns // Namespace aliases — short names for the packages we use
::channels ::slack::channels
::messaging ::slack::messaging
::misc ::slack::misc
::webhooks ::slack::webhooks
// ... other aliases (see full code on GitHub) // AI provider aliases
::anthropic-chat ::anthropic::messages
::openai-chat ::openai::chat
::xai-chat ::xai::responses
::gemini-chat ::gemini::chat CODE_BLOCK:
MODEL_ALIASES { "claude": {service: "Anthropic", model: "claude-sonnet-4-5"}, "opus": {service: "Anthropic", model: "claude-opus-4-6"}, "gpt": {service: "OpenAi", model: "gpt-5.2"}, "grok": {service: "Xai", model: "grok-4-1-fast"}, "gemini": {service: "Gemini", model: "gemini-3-flash-preview"} // ... plus shorthand aliases (see full code on GitHub)
} DEFAULT_SELECTION {service: "Anthropic", model: "claude-sonnet-4-5"} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
MODEL_ALIASES { "claude": {service: "Anthropic", model: "claude-sonnet-4-5"}, "opus": {service: "Anthropic", model: "claude-opus-4-6"}, "gpt": {service: "OpenAi", model: "gpt-5.2"}, "grok": {service: "Xai", model: "grok-4-1-fast"}, "gemini": {service: "Gemini", model: "gemini-3-flash-preview"} // ... plus shorthand aliases (see full code on GitHub)
} DEFAULT_SELECTION {service: "Anthropic", model: "claude-sonnet-4-5"} CODE_BLOCK:
MODEL_ALIASES { "claude": {service: "Anthropic", model: "claude-sonnet-4-5"}, "opus": {service: "Anthropic", model: "claude-opus-4-6"}, "gpt": {service: "OpenAi", model: "gpt-5.2"}, "grok": {service: "Xai", model: "grok-4-1-fast"}, "gemini": {service: "Gemini", model: "gemini-3-flash-preview"} // ... plus shorthand aliases (see full code on GitHub)
} DEFAULT_SELECTION {service: "Anthropic", model: "claude-sonnet-4-5"} COMMAND_BLOCK:
AiService enum { Anthropic, OpenAi, Xai, Gemini } ask-ai fn match (service: AiService, model: Str, message: Str, system: Str): Str { AiService.Anthropic => { ::anthropic-chat/chat(model, message, system) } AiService.OpenAi => { ::openai-chat/chat(model, message, system) } AiService.Xai => { ::xai-chat/chat(model, message, system) } AiService.Gemini => { ::gemini-chat/chat(model, message, system) }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
AiService enum { Anthropic, OpenAi, Xai, Gemini } ask-ai fn match (service: AiService, model: Str, message: Str, system: Str): Str { AiService.Anthropic => { ::anthropic-chat/chat(model, message, system) } AiService.OpenAi => { ::openai-chat/chat(model, message, system) } AiService.Xai => { ::xai-chat/chat(model, message, system) } AiService.Gemini => { ::gemini-chat/chat(model, message, system) }
} COMMAND_BLOCK:
AiService enum { Anthropic, OpenAi, Xai, Gemini } ask-ai fn match (service: AiService, model: Str, message: Str, system: Str): Str { AiService.Anthropic => { ::anthropic-chat/chat(model, message, system) } AiService.OpenAi => { ::openai-chat/chat(model, message, system) } AiService.Xai => { ::xai-chat/chat(model, message, system) } AiService.Gemini => { ::gemini-chat/chat(model, message, system) }
} COMMAND_BLOCK:
handle-message fn (channel: Str, message: Map, bot-user-id: Str): Map { text or(message.text, "") cmd parse-ai-command(text) cond { // !ai — show current model and available options eq(cmd, "help") => { // ... format and post model list (see full code on GitHub) } // !ai <selection> — acknowledge the switch is-some(cmd) => { svc get(SERVICE_INFO, cmd.service) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: `${svc.emoji} Switched to *${svc.name}* \`${cmd.model}\`` })) } // Regular message — detect model from history and reply with context => { sel detect-selection(channel, bot-user-id) service to-service(sel.service) context fetch-context(channel, bot-user-id) prompt if(is-empty(context), text, `Recent conversation:\n\n${context}\n\n---\nRespond to the latest message.` ) ai-response ask-ai(service, sel.model, prompt, SYSTEM_PROMPT) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: ai-response })) } }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
handle-message fn (channel: Str, message: Map, bot-user-id: Str): Map { text or(message.text, "") cmd parse-ai-command(text) cond { // !ai — show current model and available options eq(cmd, "help") => { // ... format and post model list (see full code on GitHub) } // !ai <selection> — acknowledge the switch is-some(cmd) => { svc get(SERVICE_INFO, cmd.service) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: `${svc.emoji} Switched to *${svc.name}* \`${cmd.model}\`` })) } // Regular message — detect model from history and reply with context => { sel detect-selection(channel, bot-user-id) service to-service(sel.service) context fetch-context(channel, bot-user-id) prompt if(is-empty(context), text, `Recent conversation:\n\n${context}\n\n---\nRespond to the latest message.` ) ai-response ask-ai(service, sel.model, prompt, SYSTEM_PROMPT) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: ai-response })) } }
} COMMAND_BLOCK:
handle-message fn (channel: Str, message: Map, bot-user-id: Str): Map { text or(message.text, "") cmd parse-ai-command(text) cond { // !ai — show current model and available options eq(cmd, "help") => { // ... format and post model list (see full code on GitHub) } // !ai <selection> — acknowledge the switch is-some(cmd) => { svc get(SERVICE_INFO, cmd.service) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: `${svc.emoji} Switched to *${svc.name}* \`${cmd.model}\`` })) } // Regular message — detect model from history and reply with context => { sel detect-selection(channel, bot-user-id) service to-service(sel.service) context fetch-context(channel, bot-user-id) prompt if(is-empty(context), text, `Recent conversation:\n\n${context}\n\n---\nRespond to the latest message.` ) ai-response ask-ai(service, sel.model, prompt, SYSTEM_PROMPT) ::messaging/chat-post-message(::messaging/ChatPostMessageRequest({ channel: channel, text: ai-response })) } }
} CODE_BLOCK:
check-channel-poll
meta { schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
}
fn (event) { channel get-channel-id() bot-user-id get-bot-user-id() // ... fetch and filter messages (see full code on GitHub) for-each(new-messages, (msg) { handle-message(channel, msg, bot-user-id) })
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
check-channel-poll
meta { schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
}
fn (event) { channel get-channel-id() bot-user-id get-bot-user-id() // ... fetch and filter messages (see full code on GitHub) for-each(new-messages, (msg) { handle-message(channel, msg, bot-user-id) })
} CODE_BLOCK:
check-channel-poll
meta { schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
}
fn (event) { channel get-channel-id() bot-user-id get-bot-user-id() // ... fetch and filter messages (see full code on GitHub) for-each(new-messages, (msg) { handle-message(channel, msg, bot-user-id) })
} CODE_BLOCK:
hot dev --open Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
hot dev --open CODE_BLOCK:
hot dev --open CODE_BLOCK:
hot eval 'send("slack-bot:check")' Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
hot eval 'send("slack-bot:check")' CODE_BLOCK:
hot eval 'send("slack-bot:check")' CODE_BLOCK:
!ai Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
!ai gpt → GPT-5.2
!ai grok → Grok 4.1 Fast
!ai gemini → Gemini 3 Flash
!ai claude → Claude Sonnet 4.5
!ai opus → Claude Opus 4.6 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
!ai gpt → GPT-5.2
!ai grok → Grok 4.1 Fast
!ai gemini → Gemini 3 Flash
!ai claude → Claude Sonnet 4.5
!ai opus → Claude Opus 4.6 CODE_BLOCK:
!ai gpt → GPT-5.2
!ai grok → Grok 4.1 Fast
!ai gemini → Gemini 3 Flash
!ai claude → Claude Sonnet 4.5
!ai opus → Claude Opus 4.6 CODE_BLOCK:
export HOT_API_KEY=your-api-key-here Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
export HOT_API_KEY=your-api-key-here CODE_BLOCK:
export HOT_API_KEY=your-api-key-here CODE_BLOCK:
check-channel-poll
meta { // schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
check-channel-poll
meta { // schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
} CODE_BLOCK:
check-channel-poll
meta { // schedule: "every 15 seconds", // comment out scheduled polling in Hot Dev Cloud in favor of webhooks + Slack Events API on-event: "slack-bot:check"
} CODE_BLOCK:
hot deploy Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
$ hot deploy
No build ID provided, creating new bundle build from current source...
Discovering namespaces in: hot/src
Found namespace ::slack-bot::bot with 11 functions, 1 types
Discovered 1 namespaces ### package doc gen lines omitted ### Inserted 5 event handler(s) Inserted 1 webhook(s)
✓ Created bundle build 019c51a7-a5b9-7450-8025-43f5787d8bc1 Size: 229013 bytes
✓ Successfully uploaded build 019c51a7-a5b9-7450-8025-43f5787d8bc1
✓ Successfully deployed build 019c51a7-a5b9-7450-8025-43f5787d8bc1 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
$ hot deploy
No build ID provided, creating new bundle build from current source...
Discovering namespaces in: hot/src
Found namespace ::slack-bot::bot with 11 functions, 1 types
Discovered 1 namespaces ### package doc gen lines omitted ### Inserted 5 event handler(s) Inserted 1 webhook(s)
✓ Created bundle build 019c51a7-a5b9-7450-8025-43f5787d8bc1 Size: 229013 bytes
✓ Successfully uploaded build 019c51a7-a5b9-7450-8025-43f5787d8bc1
✓ Successfully deployed build 019c51a7-a5b9-7450-8025-43f5787d8bc1 COMMAND_BLOCK:
$ hot deploy
No build ID provided, creating new bundle build from current source...
Discovering namespaces in: hot/src
Found namespace ::slack-bot::bot with 11 functions, 1 types
Discovered 1 namespaces ### package doc gen lines omitted ### Inserted 5 event handler(s) Inserted 1 webhook(s)
✓ Created bundle build 019c51a7-a5b9-7450-8025-43f5787d8bc1 Size: 229013 bytes
✓ Successfully uploaded build 019c51a7-a5b9-7450-8025-43f5787d8bc1
✓ Successfully deployed build 019c51a7-a5b9-7450-8025-43f5787d8bc1 COMMAND_BLOCK:
on-slack-event
meta { webhook: { service: "slack-bot", path: "/events", method: "POST", description: "Receive Slack Events API callbacks" }
}
fn (request: HttpRequest): HttpResponse { cond { // Slack URL verification challenge (one-time setup) eq(request.body.type, "url_verification") => { HttpResponse({status: 200, headers: {"content-type": "application/json"}, body: {challenge: request.body.challenge}}) } // Verify the request signature not(::webhooks/verify-request(request)) => { HttpResponse({status: 401, body: {error: "invalid signature"}}) } // Handle the event — top-level messages only (simplified — see full code on GitHub) eq(request.body.type, "event_callback") => { event request.body.event cond { and(eq(event.type, "message"), is-null(event.subtype), is-null(event.bot_id), is-null(event.thread_ts)) => { bot-user-id get-bot-user-id() handle-message(or(event.channel, get-channel-id()), event, bot-user-id) } } HttpResponse({status: 200, body: {ok: true}}) } => { HttpResponse({status: 200, body: {ok: true}}) } }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
on-slack-event
meta { webhook: { service: "slack-bot", path: "/events", method: "POST", description: "Receive Slack Events API callbacks" }
}
fn (request: HttpRequest): HttpResponse { cond { // Slack URL verification challenge (one-time setup) eq(request.body.type, "url_verification") => { HttpResponse({status: 200, headers: {"content-type": "application/json"}, body: {challenge: request.body.challenge}}) } // Verify the request signature not(::webhooks/verify-request(request)) => { HttpResponse({status: 401, body: {error: "invalid signature"}}) } // Handle the event — top-level messages only (simplified — see full code on GitHub) eq(request.body.type, "event_callback") => { event request.body.event cond { and(eq(event.type, "message"), is-null(event.subtype), is-null(event.bot_id), is-null(event.thread_ts)) => { bot-user-id get-bot-user-id() handle-message(or(event.channel, get-channel-id()), event, bot-user-id) } } HttpResponse({status: 200, body: {ok: true}}) } => { HttpResponse({status: 200, body: {ok: true}}) } }
} COMMAND_BLOCK:
on-slack-event
meta { webhook: { service: "slack-bot", path: "/events", method: "POST", description: "Receive Slack Events API callbacks" }
}
fn (request: HttpRequest): HttpResponse { cond { // Slack URL verification challenge (one-time setup) eq(request.body.type, "url_verification") => { HttpResponse({status: 200, headers: {"content-type": "application/json"}, body: {challenge: request.body.challenge}}) } // Verify the request signature not(::webhooks/verify-request(request)) => { HttpResponse({status: 401, body: {error: "invalid signature"}}) } // Handle the event — top-level messages only (simplified — see full code on GitHub) eq(request.body.type, "event_callback") => { event request.body.event cond { and(eq(event.type, "message"), is-null(event.subtype), is-null(event.bot_id), is-null(event.thread_ts)) => { bot-user-id get-bot-user-id() handle-message(or(event.channel, get-channel-id()), event, bot-user-id) } } HttpResponse({status: 200, body: {ok: true}}) } => { HttpResponse({status: 200, body: {ok: true}}) } }
} CODE_BLOCK:
Local Dev (polling): Production (webhooks): Schedule (every 15s) Slack Events API │ │ ▼ ▼ check-channel-poll() on-slack-event(request) │ │ ├─ fetch recent messages ├─ verify signature ├─ filter bot/system msgs ├─ filter to top-level msgs └─ for each message: └─ handle-message() └─ handle-message() │ │ ├─ !ai command? → switch ├─ !ai command? → switch model └─ regular msg? → ask AI → reply └─ regular msg? → ask AI → reply Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Local Dev (polling): Production (webhooks): Schedule (every 15s) Slack Events API │ │ ▼ ▼ check-channel-poll() on-slack-event(request) │ │ ├─ fetch recent messages ├─ verify signature ├─ filter bot/system msgs ├─ filter to top-level msgs └─ for each message: └─ handle-message() └─ handle-message() │ │ ├─ !ai command? → switch ├─ !ai command? → switch model └─ regular msg? → ask AI → reply └─ regular msg? → ask AI → reply CODE_BLOCK:
Local Dev (polling): Production (webhooks): Schedule (every 15s) Slack Events API │ │ ▼ ▼ check-channel-poll() on-slack-event(request) │ │ ├─ fetch recent messages ├─ verify signature ├─ filter bot/system msgs ├─ filter to top-level msgs └─ for each message: └─ handle-message() └─ handle-message() │ │ ├─ !ai command? → switch ├─ !ai command? → switch model └─ regular msg? → ask AI → reply └─ regular msg? → ask AI → reply - Talks to four AI providers — Claude, GPT, Grok, and Gemini
- Switches models live — Type !ai gpt in the channel and the next reply comes from GPT
- Reads conversation context — The bot knows what was said recently
- Deploys with one command — hot deploy to go live on Hot Dev Cloud - Hot Dev CLI — Download from hot.dev/download
- VS Code extension (optional) — Search "Hot" by hot-dev in the Extensions panel for syntax highlighting, autocomplete, and error checking
- A Slack workspace where you can create apps
- At least one AI API key — Anthropic (Claude) is the default, but OpenAI, xAI, or Google Gemini work too - Go to api.slack.com/apps
- Click Create New App → From scratch
- Give it a name (e.g., "Hot AI Bot") and select your workspace - Go to Install App in the sidebar
- Click Install to Workspace and approve
- Copy the Bot User OAuth Token — it starts with xoxb- - Create an account at app.hot.dev
- Go to API Keys and create a new key
- Set it in your terminal or in a .env file in your project root: - slack.api.key — your bot token
- slack.channel.id — the channel ID
- slack.signing.secret — your Signing Secret (find it under Basic Information in your Slack app — needed for webhook verification)
- anthropic.api.key — your AI provider key(s) - Go to your Slack app → Event Subscriptions → toggle Enable Events
- Set the Request URL to your webhook endpoint — find the webhook URL at Hot Dev App Webhooks. - Subscribe to Bot Events: message.channels — messages in public channels message.groups — messages in private channels (optional — requires groups:history scope) message.im — direct messages to the bot (optional — requires im:history scope)
- message.channels — messages in public channels
- message.groups — messages in private channels (optional — requires groups:history scope)
- message.im — direct messages to the bot (optional — requires im:history scope)
- Click Save Changes — Slack will prompt you to reinstall the app to pick up the new event permissions. - message.channels — messages in public channels
- message.groups — messages in private channels (optional — requires groups:history scope)
- message.im — direct messages to the bot (optional — requires im:history scope) - Full demo source code
- Hot Dev documentation
- Hot Language Guide
- Hot Dev on X
how-totutorialguidedev.toaiopenaigptserverswitchdatabasegitgithub