Build a Slack Bot That Controls Your Smart Lights with OpenClaw
Last month someone in our office Slack typed "it's too bright in here" and I thought: what if that actually did something? Two evenings later, I had a Slack bot that controls our office lights through OpenClaw. It turned out to be a surprisingly satisfying weekend project.
Here's how to build one yourself.
## The architecture
The data flow is simple: Slack message -> your bot server -> OpenClaw agent -> ClawTether -> Home Assistant -> lights. That looks like a lot of hops, but in practice the round trip is under 2 seconds.
You'll need: - A Slack workspace where you can install apps - An OpenClaw agent with ClawTether connected to Home Assistant (see the setup guide) - A server that can receive Slack webhooks (I used a $5 DigitalOcean droplet) - About 3 hours and some coffee
## Step 1: Create the Slack app
Go to api.slack.com/apps and click "Create New App." Choose "From scratch" and give it a name. I went with "LightBot" because creativity wasn't flowing at 9 PM on a Friday.
Under "OAuth & Permissions," add these bot token scopes: - `chat:write` (to respond in channels) - `app_mentions:read` (to hear when someone tags the bot) - `channels:history` (to read messages in channels it's added to)
Install the app to your workspace and grab the Bot User OAuth Token. It starts with `xoxb-`.
## Step 2: Set up event subscriptions
Under "Event Subscriptions," enable events and set your request URL to your server's endpoint. Slack will send a verification challenge, so you need your server running first. I'll cover that next.
Subscribe to these bot events: - `app_mention` (when someone @mentions your bot) - `message.channels` (messages in channels the bot is in)
I originally only subscribed to `app_mention`, but it was annoying to type "@LightBot" every time. Subscribing to channel messages lets you create a dedicated #lights channel where every message goes to the bot.
## Step 3: Build the bot server
Here's the minimal server. I used Bun because I didn't feel like configuring anything:
```typescript import { OpenClaw } from '@openclaw/sdk'
const agent = new OpenClaw({ skills: ['clawtether-home-assistant'], })
Bun.serve({ port: 3000, async fetch(req) { const body = await req.json()
// Slack URL verification if (body.type === 'url_verification') { return Response.json({ challenge: body.challenge }) }
// Ignore bot messages to prevent loops if (body.event?.bot_id) { return new Response('ok') }
const message = body.event?.text?.replace(/<@.*?>\s*/g, '').trim() if (!message) return new Response('ok')
// Send to OpenClaw agent const response = await agent.chat( `You control the office lights via Home Assistant. Respond briefly. If the user wants a light change, do it. Current request: ${message}` )
// Post response back to Slack await fetch('https://slack.com/api/chat.postMessage', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ channel: body.event.channel, text: response.text, thread_ts: body.event.ts, }), })
return new Response('ok') }, }) ```
The regex on line 18 strips the @mention from the message text. Without it, your agent gets confused by the `<@U12345>` user ID format Slack uses.
## Step 4: Configure the OpenClaw agent for light control
Your agent needs a system prompt that keeps it focused. Without one, it'll try to be a general-purpose assistant and give long-winded responses about the philosophy of lighting.
Create a `lightbot-soul.md`:
```markdown You are LightBot. You control office lights through Home Assistant.
Rules: - Keep responses under 2 sentences - If someone describes a mood or preference, translate it to light settings - "too bright" = reduce brightness by 30% - "too dark" = increase brightness by 30% - "movie mode" = all lights to 10%, warm white - "meeting mode" = overhead lights to 80%, cool white - Confirm what you did after each change ```
The specific percentage rules are important. Without them, the agent interprets "too bright" differently every time. Sometimes it dims by 10%, sometimes by 50%. Explicit rules make the behavior predictable.
## Step 5: Handle the weird edge cases
A few things I learned the hard way:
**Slack sends duplicate events.** Sometimes you get the same event twice within a few seconds. Add a simple dedup cache:
```typescript
const processed = new Set
// Inside your handler: const eventId = body.event_id if (processed.has(eventId)) return new Response('ok') processed.add(eventId) setTimeout(() => processed.delete(eventId), 60000) ```
**Slack expects a response within 3 seconds.** If your OpenClaw agent takes longer to process (and it sometimes does), Slack will retry the webhook. Respond with 200 immediately, then process async:
```typescript // Acknowledge immediately const response = new Response('ok')
// Process in background processMessage(body.event).catch(console.error)
return response ```
**People will test boundaries immediately.** The first thing my coworker typed was "turn the lights off and on 100 times." Add a rate limiter or you'll have a strobe party. I cap it at 5 commands per user per minute.
## Step 6: Make it fun
The basic version works, but a few additions made it actually enjoyable to use:
**Emoji reactions.** After executing a command, have the bot react to the original message with a lightbulb emoji. It's a quick visual confirmation that something happened.
**Scene presets.** Add named scenes to your soul.md: "focus mode," "presentation," "friday vibes" (that one sets the RGB strips to slow color cycling). People use presets way more than direct commands.
**Status reporting.** "What lights are on?" returns a quick summary. Surprisingly useful for the last person leaving the office.
## The result
It's been running for three weeks now. The #lights channel gets about 15-20 messages a day. The most popular command is "meeting mode" (we have a lot of meetings). The most creative was "make it look like a sunset in here" which the agent actually handled pretty well, setting the overhead lights to warm amber and the LED strips to a reddish orange.
The whole thing is about 150 lines of code excluding the soul.md file. It took two evenings to build, one evening to fix the edge cases, and zero evenings to maintain since then. For a weekend project, that's a pretty good ratio.
The code is rough and I'd do a few things differently if I rebuilt it (proper queue instead of fire-and-forget, actual error handling, maybe a database for command history), but it works and people actually use it. Sometimes that's enough.