Skip to content
Cloudflare Docs

Build a Slack Agent

Deploy your first Slack Agent

This guide will show you how to build and deploy an AI-powered Slack bot on Cloudflare Workers that can:

  • Respond to direct messages
  • Reply when mentioned in channels
  • Maintain conversation context in threads
  • Use AI to generate intelligent responses

Your Slack Agent will be a multi-tenant application, meaning a single deployment can serve multiple Slack workspaces. Each workspace gets its own isolated agent instance with dedicated storage, powered by the Agents SDK.

You can view the full code for this example here.

Prerequisites

Before you begin, you will need:

1. Create a Slack App

First, create a new Slack App that your agent will use to interact with Slack:

  1. Go to api.slack.com/apps and select Create New App.
  2. Select From scratch.
  3. Give your app a name (for example, "My AI Assistant") and select your workspace.
  4. Select Create App.

Configure OAuth & Permissions

In your Slack App settings, go to OAuth & Permissions and add the following Bot Token Scopes:

  • chat:write — Send messages as the bot
  • chat:write.public — Send messages to channels without joining
  • channels:history — View messages in public channels
  • app_mentions:read — Receive mentions
  • im:write — Send direct messages
  • im:history — View direct message history

Enable Event Subscriptions

You will later configure the Event Subscriptions URL after deploying your agent. But for now, go to Event Subscriptions in your Slack App settings and prepare to enable it.

Subscribe to the following bot events:

  • app_mention — When the bot is @mentioned
  • message.im — Direct messages to the bot

Do not enable it yet. You will enable it after deployment.

Get your Slack credentials

From your Slack App settings, collect these values:

  1. Basic Information > App Credentials:
    • Client ID
    • Client Secret
    • Signing Secret

Keep these handy — you will need them in the next step.

2. Create your Slack Agent project

  1. Create a new project for your Slack Agent:
Terminal window
npm create cloudflare@latest -- my-slack-agent
  1. Navigate into your project:
Terminal window
cd my-slack-agent
  1. Install the required dependencies:
Terminal window
npm install agents openai

3. Set up your environment variables

  1. Create a .dev.vars file in your project root for local development secrets:
Terminal window
touch .dev.vars
  1. Add your credentials to .dev.vars:
Terminal window
SLACK_CLIENT_ID="your-slack-client-id"
SLACK_CLIENT_SECRET="your-slack-client-secret"
SLACK_SIGNING_SECRET="your-slack-signing-secret"
OPENAI_API_KEY="your-openai-api-key"
OPENAI_BASE_URL="https://gateway.ai.cloudflare.com/v1/YOUR_ACCOUNT_ID/YOUR_GATEWAY/openai"
  1. Update your wrangler.jsonc to configure your Agent:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-slack-agent",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"durable_objects": {
"bindings": [
{
"name": "MyAgent",
"class_name": "MyAgent",
"script_name": "my-slack-agent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_classes": [
"MyAgent"
]
}
]
}

4. Create your Slack Agent

  1. First, create the base SlackAgent class at src/slack.ts. This class handles OAuth, request verification, and event routing. You can view the full implementation on GitHub.

  2. Now create your agent implementation at src/index.ts:

TypeScript
import { env } from "cloudflare:workers";
import { SlackAgent } from "./slack";
import { OpenAI } from "openai";
const openai = new OpenAI({
apiKey: env.OPENAI_API_KEY,
baseURL: env.OPENAI_BASE_URL
});
type SlackMsg = {
user?: string;
text?: string;
ts: string;
thread_ts?: string;
subtype?: string;
bot_id?: string;
};
function normalizeForLLM(msgs: SlackMsg[], selfUserId: string) {
return msgs.map((m) => {
const role = m.user && m.user !== selfUserId ? "user" : "assistant";
const text = (m.text ?? "").replace(/<@([A-Z0-9]+)>/g, "@$1");
return { role, content: text };
});
}
export class MyAgent extends SlackAgent {
async generateAIReply(conversation: SlackMsg[]) {
const selfId = await this.ensureAppUserId();
const messages = normalizeForLLM(conversation, selfId);
const system = `You are a helpful AI assistant in Slack.
Be brief, specific, and actionable. If you're unsure, ask a single clarifying question.`;
const input = [{ role: "system", content: system }, ...messages];
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: input
});
const msg = response.choices[0].message.content;
if (!msg) throw new Error("No message from AI");
return msg;
}
async onSlackEvent(event: { type: string } & Record<string, unknown>) {
// Ignore bot messages and subtypes (edits, joins, etc.)
if (event.bot_id || event.subtype) return;
// Handle direct messages
if (event.type === "message") {
const e = event as unknown as SlackMsg & { channel: string };
const isDM = (e.channel || "").startsWith("D");
const mentioned = (e.text || "").includes(
`<@${await this.ensureAppUserId()}>`
);
if (!isDM && !mentioned) return;
const conversation = await this.fetchConversation(e.channel);
const content = await this.generateAIReply(conversation);
await this.sendMessage(content, { channel: e.channel });
return;
}
// Handle @mentions in channels
if (event.type === "app_mention") {
const e = event as unknown as SlackMsg & {
channel: string;
text?: string;
};
const thread = await this.fetchThread(e.channel, e.thread_ts || e.ts);
const content = await this.generateAIReply(thread);
await this.sendMessage(content, {
channel: e.channel,
thread_ts: e.thread_ts || e.ts
});
return;
}
}
}
export default MyAgent.listen({
clientId: env.SLACK_CLIENT_ID,
clientSecret: env.SLACK_CLIENT_SECRET,
slackSigningSecret: env.SLACK_SIGNING_SECRET,
scopes: [
"chat:write",
"chat:write.public",
"channels:history",
"app_mentions:read",
"im:write",
"im:history"
]
});

5. Test locally

Start your development server:

Terminal window
npm run dev

Your agent is now running at http://localhost:8787.

Configure Slack Event Subscriptions

Now that your agent is running locally, you need to expose it to Slack. Use Cloudflare Tunnel to create a secure tunnel:

Terminal window
npx cloudflared tunnel --url http://localhost:8787

This will output a public URL like https://random-subdomain.trycloudflare.com.

Go back to your Slack App settings:

  1. Go to Event Subscriptions.

  2. Toggle Enable Events to On.

  3. Enter your Request URL: https://random-subdomain.trycloudflare.com/slack.

  4. Slack will send a verification request — if your agent is running correctly, it should show Verified.

  5. Under Subscribe to bot events, add:

    • app_mention
    • message.im
  6. Select Save Changes.

Install your app to Slack

Visit http://localhost:8787/install in your browser. This will redirect you to Slack's authorization page. Select Allow to install the app to your workspace.

After authorization, you should see "Successfully registered!" in your browser.

Test your agent

Open Slack. Then:

  1. Send a DM to your bot — it should respond with an AI-generated message.
  2. Mention your bot in a channel (e.g., @My AI Assistant hello) — it should reply in a thread.

If everything works, you're ready to deploy to production!

6. Deploy to production

  1. Before deploying, add your secrets to Cloudflare:
Terminal window
npx wrangler secret put SLACK_CLIENT_ID
npx wrangler secret put SLACK_CLIENT_SECRET
npx wrangler secret put SLACK_SIGNING_SECRET
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put OPENAI_BASE_URL
  1. Deploy your agent:
Terminal window
npx wrangler deploy

After deploying, you will get a production URL like:

https://my-slack-agent.your-account.workers.dev

Update Slack Event Subscriptions

Go back to your Slack App settings:

  1. Go to Event Subscriptions.
  2. Update the Request URL to your production URL: https://my-slack-agent.your-account.workers.dev/slack.
  3. Select Save Changes.

Distribute your app

Now that your agent is deployed, you can share it with others:

  • Single workspace: Install it via https://my-slack-agent.your-account.workers.dev/install.
  • Public distribution: Submit your app to the Slack App Directory.

Each workspace that installs your app will get its own isolated agent instance with dedicated storage.

How it works

Multi-tenancy with Durable Objects

Your Slack Agent uses Durable Objects to provide isolated, stateful instances for each Slack workspace:

  • Each workspace's team_id is used as the Durable Object ID.
  • Each agent instance stores its own Slack access token in KV storage.
  • Conversations are fetched on-demand from Slack's API.
  • All agent logic runs in an isolated, consistent environment.

OAuth flow

The agent handles Slack's OAuth 2.0 flow:

  1. User visits /install > redirected to Slack authorization.
  2. User selects Allow > Slack redirects to /accept with an authorization code.
  3. Agent exchanges code for access token.
  4. Agent stores token in the workspace's Durable Object.

Event handling

When Slack sends an event:

  1. Request arrives at /slack endpoint.
  2. Agent verifies the request signature using HMAC-SHA256.
  3. Agent routes the event to the correct workspace's Durable Object.
  4. onSlackEvent method processes the event and generates a response.

Customizing your agent

Change the AI model

Update the model in src/index.ts:

TypeScript
const response = await openai.chat.completions.create({
model: "gpt-4o", // or any other model
messages: input,
});

Add conversation memory

Store conversation history in Durable Object storage:

TypeScript
async storeMessage(channel: string, message: SlackMsg) {
const history = await this.ctx.storage.kv.get(`history:${channel}`) || [];
history.push(message);
await this.ctx.storage.kv.put(`history:${channel}`, history);
}

React to specific keywords

Add custom logic in onSlackEvent:

TypeScript
async onSlackEvent(event: { type: string } & Record<string, unknown>) {
if (event.type === "message") {
const e = event as unknown as SlackMsg & { channel: string };
if (e.text?.includes("help")) {
await this.sendMessage("Here's how I can help...", {
channel: e.channel
});
return;
}
}
// ... rest of your event handling
}

Use different LLM providers

Replace OpenAI with Workers AI:

TypeScript
import { Ai } from "@cloudflare/ai";
export class MyAgent extends SlackAgent {
async generateAIReply(conversation: SlackMsg[]) {
const ai = new Ai(this.ctx.env.AI);
const response = await ai.run("@cf/meta/llama-3-8b-instruct", {
messages: normalizeForLLM(conversation, await this.ensureAppUserId()),
});
return response.response;
}
}

Next steps