In this tutorial you take Chat SDK Go from zero to a running Slack bot that replies when you mention it. You will create a Slack app, run the bundled hello-world example against your workspace, and then make your first change to the bot's behavior.
Expect the whole tutorial to take under 30 minutes.
- Go 1.26.3 or newer (
go version). - A Slack workspace where you are allowed to create and install apps.
- A way to expose local port 8080 to the public internet over HTTPS, such as
Tailscale Funnel,
ngrok, orcloudflared. Slack delivers events by calling your bot over HTTPS.
You do not need Docker, Redis, Postgres, or any other service: this tutorial uses the in-memory state backend.
Clone the repository and make sure the example compiles:
git clone https://github.com/coder/chat.git
cd chat
go build ./examples/slack-hello-worldIf go build succeeds, your toolchain is ready.
The example you are about to run lives in
examples/slack-hello-world/main.go.
Its whole job is:
- Build a Slack adapter from a signing secret and a bot token.
- Build a
chat.Chatruntime with in-memory state and that adapter. - Register one handler: when the bot is mentioned, reply with
**hello** _world_in the same thread. - Serve the Slack webhook on
http://localhost:8080/webhooks/slack.
Open the Slack app dashboard and create a new app (from scratch) in your workspace:
Then configure it:
-
In OAuth & Permissions, under Bot Token Scopes, add:
Scope Why the bot needs it chat:writePost the reply with chat.postMessage.app_mentions:readReceive app_mentionevents when the bot is mentioned.im:historyOnly needed if you also want direct messages to reach the bot. -
In App Home, under Show Tabs, enable the Messages Tab and allow users to send messages from it (Slack labels this "Allow users to send Slash commands and messages from the messages tab"). This matters only for direct messages; mentions in channels work without it.
-
In OAuth & Permissions, click Install to Workspace and approve the app.
You need two secrets. Treat both like passwords.
- In OAuth & Permissions, copy the Bot User OAuth Token. It starts
with
xoxb-. This becomesSLACK_BOT_TOKEN. - In Basic Information, under App Credentials, copy the
Signing Secret. This becomes
SLACK_SIGNING_SECRET.
From the repository root:
export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."
export CHAT_DEMO_IN_MEMORY_STATE=1
export PORT=8080
go run ./examples/slack-hello-worldCHAT_DEMO_IN_MEMORY_STATE=1 is a deliberate speed bump: it acknowledges that
in-memory state is lost on restart, which is fine for this tutorial and wrong
for production. The state backend guide
covers the durable options.
On startup the adapter calls Slack's auth.test with your bot token to
discover the bot's own identity. If the token is wrong you find out now, not
on the first message. When the bot is up you should see a log line like:
level=INFO msg=listening addr=:8080
Slack must be able to reach your machine over public HTTPS. In a second terminal, expose port 8080 with your tunnel of choice. For example, with Tailscale Funnel:
tailscale funnel --bg --https=443 localhost:8080
tailscale funnel statusor with ngrok:
ngrok http 8080Either way you end up with a public HTTPS URL such as
https://your-host.example.com. Keep the tunnel running.
Back in the Slack app dashboard:
-
In Event Subscriptions, enable events.
-
Set the Request URL to:
https://YOUR_PUBLIC_HOST/webhooks/slackSlack immediately sends a
url_verificationchallenge. The Slack adapter answers it automatically; the dashboard should show Verified within a few seconds. If it does not, check that the bot from Step 4 and the tunnel from Step 5 are both still running. -
Under Subscribe to bot events, add
app_mention(andmessage.imif you addedim:historyin Step 2). -
Save changes. If Slack prompts you to reinstall the app, do it from OAuth & Permissions.
In Slack, invite the bot to a channel and mention it:
/invite @your-bot
@your-bot hello
The bot replies in a thread on your message with **hello** _world_,
rendered with bold and italics. You have a running Slack bot.
The bot currently answers every mention but forgets the conversation immediately. Make it stay in the conversation.
First, let Slack deliver unmentioned channel messages to your bot — without this, only mentions ever reach it:
- In OAuth & Permissions, add the
channels:historybot scope. - In Event Subscriptions, add the
message.channelsbot event. - Reinstall the app from OAuth & Permissions.
(If you set up message.im in Step 2, you can skip this and test the
follow-up flow in a direct message instead.)
Then open examples/slack-hello-world/main.go and replace the
OnNewMention handler registration with:
bot.OnNewMention(func(ctx context.Context, ev *chat.MessageEvent) error {
if err := ev.Thread.Subscribe(ctx); err != nil {
return err
}
_, err := ev.Thread.Post(ctx, chat.Markdown("**hello** _world_"))
return err
})
bot.OnSubscribedMessage(func(ctx context.Context, ev *chat.MessageEvent) error {
_, err := ev.Thread.Post(ctx, chat.Text("You said: "+ev.Message.Text))
return err
})Restart the bot (Ctrl-C, then go run ./examples/slack-hello-world again)
and mention it once more. From then on, follow-up messages in that thread get
echoed back — no mention required. (If you type several messages faster than
the bot replies, some may be skipped: the default concurrency strategy drops
events that arrive while the thread's previous event is still being handled.)
Two things to notice:
- Replying never subscribes a thread.
Thread.Subscribeis always an explicit decision, and it lasts until you callThread.Unsubscribe. - Subscriptions live in runtime state. Because this example uses in-memory state, restarting the bot forgets them.
- Choose a state backend to keep subscriptions, dedupe marks, and locks across restarts.
- Defer long-running work before your handlers start doing anything slower than a quick reply.
- Handle slash commands and interactive components.
- Read the architecture explanation to understand the model behind what you just built.
The example's own README repeats the Slack app setup with more detail (including Tailscale Funnel specifics) if you need to revisit it later.