---
title: Build a Slack Agent · Cloudflare Agents docs
description: "This guide will show you how to build and deploy an AI-powered
  Slack bot on Cloudflare Workers that can:"
lastUpdated: 2025-11-07T18:20:34.000Z
chatbotDeprioritize: false
source_url:
  html: https://developers.cloudflare.com/agents/guides/slack-agent/
  md: https://developers.cloudflare.com/agents/guides/slack-agent/index.md
---

## 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](https://developers.cloudflare.com/agents/).

You can view the full code for this example [here](https://github.com/cloudflare/awesome-agents/tree/69963298b359ddd66331e8b3b378bb9ae666629f/agents/slack).

## Prerequisites

Before you begin, you will need:

* A [Cloudflare account](https://dash.cloudflare.com/sign-up)
* [Node.js](https://nodejs.org/) installed (v18 or later)
* A [Slack workspace](https://slack.com/create) where you have permission to install apps
* An [OpenAI API key](https://platform.openai.com/api-keys) (or another LLM provider)

## 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](https://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:

* npm

  ```sh
  npm create cloudflare@latest -- my-slack-agent
  ```

* yarn

  ```sh
  yarn create cloudflare my-slack-agent
  ```

* pnpm

  ```sh
  pnpm create cloudflare@latest my-slack-agent
  ```

1. Navigate into your project:

```sh
cd my-slack-agent
```

1. Install the required dependencies:

```sh
npm install agents openai
```

## 3. Set up your environment variables

1. Create a `.dev.vars` file in your project root for local development secrets:

```sh
touch .dev.vars
```

1. Add your credentials to `.dev.vars`:

```sh
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"
```

Note

The `OPENAI_BASE_URL` is optional but recommended. Using [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) gives you caching, rate limiting, and analytics for your AI requests.

1. Update your `wrangler.jsonc` to configure your Agent:

* wrangler.jsonc

  ```jsonc
  {
    "$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"
        ]
      }
    ]
  }
  ```

* wrangler.toml

  ```toml
  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](https://github.com/cloudflare/awesome-agents/blob/69963298b359ddd66331e8b3b378bb9ae666629f/agents/slack/src/slack.ts).

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

```ts
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:

```sh
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](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) to create a secure tunnel:

```sh
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**.

Note

Cloudflare Tunnel URLs are temporary. When testing locally, you will need to update the Request URL each time you restart the tunnel.

### 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:

```sh
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
```

Note

You can skip `OPENAI_BASE_URL` if you're not using AI Gateway.

1. Deploy your agent:

```sh
npx wrangler deploy
```

After deploying, you will get a production URL like:

```plaintext
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](https://api.slack.com/start/distributing).

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](https://developers.cloudflare.com/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`:

```ts
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:

```ts
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`:

```ts
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](https://developers.cloudflare.com/workers-ai/):

```ts
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

* Add [Slack Interactive Components](https://api.slack.com/interactivity) (buttons, modals)
* Connect your Agent to an [MCP server](https://developers.cloudflare.com/agents/model-context-protocol/mcp-client-api/)
* Add rate limiting to prevent abuse
* Implement conversation state management
* Use [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) to track usage
* Add [schedules](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) for scheduled tasks

## Related resources

* [Agents Framework documentation](https://developers.cloudflare.com/agents/)
* [Durable Objects documentation](https://developers.cloudflare.com/durable-objects/)
* [Slack API documentation](https://api.slack.com/)
* [OpenAI API documentation](https://platform.openai.com/docs/)
