Build a Slack Bot
Receive Slack events, verify signatures, and reply to mentions.
This guide builds a Slack bot that receives events over HTTP, verifies each
request's signature, answers Slack's URL-verification handshake, and replies to
@-mentions by name. It follows
samples/slack-bot.yaml.
Set up the Slack app
Create a Slack app at api.slack.com/apps. Under
OAuth & Permissions, give the bot the chat:write, users:read, and
users:read.email scopes, then install it to your workspace and copy the
bot token (xoxb-...).
Under Basic Information, copy the signing secret — Slack signs every request with it, and the flow verifies that signature.
Under Event Subscriptions, subscribe the bot to the app_mention event and
point the Request URL at this service's /slack/events route. While
developing, expose your local port with a tunnel such as ngrok. Slack sends a
one-time verification challenge to the URL when you save it — the flow below
answers it automatically.
Export the credentials and run:
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_SIGNING_SECRET=...
octo run --config samples/slack-bot.yamlThe connectors
The config declares the HTTP server that receives events and a slack connector
holding the credentials every Slack block uses:
connectors:
- name: api
type: http
settings:
port: ${HTTP_PORT}
- name: slack
type: slack
settings:
botToken: ${SLACK_BOT_TOKEN}
signingSecret: ${SLACK_SIGNING_SECRET}Capture the raw request
Slack computes its signature over the exact request bytes, so the source
must keep them: rawBodyVar copies the raw body into a variable, and headers
captures Slack's two signature headers.
source:
connector: api
type: http
settings:
path: /slack/events
headers: [X-Slack-Signature, X-Slack-Request-Timestamp]
rawBodyVar: rawBodyVerify, then handle
The first block authenticates the request; the rest of the flow branches between Slack's one-time handshake and real events:
process:
# 1. Authenticate the request (aborts on a bad or stale signature) and
# flag Slack's URL-verification handshake.
- type: slack-verify-request
name: verify
settings:
connector: slack
# 2. Either echo the URL-verification challenge, or handle the event.
- type: if
name: challenge-or-event
condition: has(vars.slackChallenge)
then:
process:
# Slack's one-time handshake: reply 200 with the challenge value.
- type: set-payload
settings:
value: '{"challenge": body.challenge}'
else:
process:
# Keep only @-mentions, ignoring the bot's own messages.
- type: slack-event
name: mentions
settings:
eventTypes: [app_mention]
filter: body.botId == null
# Resolve the mentioning user (users.info) into vars.slackUser.
- type: slack-lookup-user
name: whois
settings:
connector: slack
by: id
user: body.user
# Reply in the same channel, greeting the user by name.
- type: slack-send-message
name: reply
settings:
connector: slack
target: body.channel
text: '"thanks " + vars.slackUser.real_name + ", you said: " + body.text'Walking through the pieces:
slack-verify-requestrecomputes the HMAC signature overvars.rawBodyusing the connector's signing secret and aborts the flow on a mismatch or a stale timestamp — unauthenticated requests never reach your logic. When the request is Slack'surl_verificationhandshake, it setsvars.slackChallengeso the flow can branch.- The challenge branch replies with
{"challenge": body.challenge}; the flow's final body is the HTTP response, which is exactly what Slack expects to confirm your Request URL. slack-eventfilters the event stream:eventTypeskeeps onlyapp_mention, and the CELfilterdrops messages sent by bots (body.botId == null) so the bot never answers itself. It also flattens the event into a friendly body —body.user,body.channel,body.text,body.ts,body.threadTs— with the untouched original underbody.raw.slack-lookup-usercallsusers.infofor the mentioning user and stores the user object invars.slackUser(look upby: emailinstead when you have an email address).slack-send-messageposts totarget— a channel ID, and the channel the mention came from isbody.channel. It stores the sent message's timestamp invars.slackTs.
Reply in a thread
To answer in a thread instead of the channel, pass the triggering message's
timestamp as threadTs:
- type: slack-send-message
settings:
connector: slack
target: body.channel
threadTs: body.ts
text: '"on it!"'Replying to a message that is already in a thread? Use its body.threadTs so
the reply lands in the same thread rather than starting a new one.
Try it
Mention the bot in a channel it has been invited to:
@yourbot hello there
The bot replies in-channel: thanks Ada Lovelace, you said: <@U123ABC> hello there.
Slack expects a 2xx within 3 seconds, or it retries the event. Keep this
flow fast — acknowledge first and offload slow work (LLM calls, database
writes) with queue-dispatch. See Events and Queues.
Where to go next
This bot echoes; the capstone thinks. The Slack AI Agent guide keeps this exact receive-verify-filter skeleton and swaps the echo for an AI agent with memory and tools.