---
title: "Add Slack"
description: "Put the dispatcher in Slack with slackChannel and Vercel Connect, no SLACK_BOT_TOKEN, no tool changes. One more front door on the same agent."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/add-slack"
md_url: "https://vercel.com/academy/building-agents-with-eve/add-slack.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T17:25:26.205Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Add Slack

# Add Slack

The shop's mechanics live in Slack all day. Making them open a browser tab to ask the dispatcher anything seems rude. So let's meet them where they already are.

Here's the test of whether eve's "an agent is a directory" is real: adding Slack should not touch a single tool, the skill, the state, or the approval logic. A channel's whole job is narrow, normalize the incoming message, own the conversation's resume handle, and decide how replies go back. Everything the dispatcher *does* is already built. We're adding one file in `channels/` and letting Slack become another client of the same agent.

The one genuinely new thing is credentials, and eve hands those to Vercel Connect so you never touch a `SLACK_BOT_TOKEN`.

## Outcome

The dispatcher answers `@mentions` in Slack, in threads, with no changes to any tool, skill, or state.

## Hands-on exercise

**Set up Connect.** Slack delivers events to a public URL and needs a verified bot token to reply. Vercel Connect brokers both, so there's no signing secret or bot token in your code. Connect authenticates your deployment by its Vercel project's OIDC token, so first link the project and pull a development token:

```bash
vercel link
vercel env pull
```

Now create a Slack connector and attach it to the project, with its webhook trigger pointed at eve's Slack route:

```bash
vercel connect create slack --name spoke-and-mirror --triggers
vercel connect attach slack/spoke-and-mirror --triggers --trigger-path /eve/v1/slack
```

Note `--triggers` appears on **both** commands, and both matter:

- `create … --triggers` registers the connector and **turns on webhook forwarding for it**. Your browser opens to set it up and authorize the Slack app for your workspace. Without `--triggers` here, no events ever flow, even if you register a destination, and `attach` will warn you about exactly that.
- `attach … --triggers` registers **this project** as the destination Connect forwards verified events to. `--trigger-path /eve/v1/slack` is required because the default is `/slack` and eve serves its Slack route at `/eve/v1/slack`.

\*\*Warning: Triggers are enabled in two places\*\*

Enabling triggers on the connector (`create --triggers`) and registering a destination (`attach --triggers`) are separate steps, and you need both. If you create the connector through the browser instead, make sure the **Triggers** toggle is **Enabled** on the configure screen, it defaults to off. A silent bot with a registered destination almost always means the connector itself has triggers disabled.

\*\*Warning: Re-running \`create\` installs a new Slack app each time\*\*

Every `vercel connect create slack` installs a *fresh* Slack app into your workspace, and `vercel connect remove` deletes the connector on Vercel's side but does **not** uninstall that app from Slack. So if you recreate the connector a few times while debugging, you'll end up with several identical bots in the `@`-mention list. Before re-creating, uninstall the stale ones in Slack under **Manage apps** (`https://app.slack.com/manage`), so you're only ever mentioning the live one.

\*\*Note: Connect is a beta on all plans\*\*

`vercel connect` ships with the Vercel CLI, no feature flag needed. If your CLI reports `connect` as an unknown subcommand, update it (`npm i -g vercel@latest`) and retry; the command surface is documented under [vercel connect](https://vercel.com/docs/cli/connect).

**Write the channel.** Install the Connect helper and create `agent/channels/slack.ts`:

```bash
npm install @vercel/connect
```

The channel needs three decisions: where its credentials come from, when to dispatch a turn, and how to deliver the reply.

- **Credentials:** `connectSlackCredentials(process.env.SLACK_CONNECTOR ?? "slack/spoke-and-mirror")` returns the bot token and webhook verifier, both managed by Connect. Reading the uid from `SLACK_CONNECTOR` keeps it out of code; the fallback matches the connector you just named, so it works without setting the variable.
- **Dispatch:** `onAppMention` decides whether a mention becomes a turn. Use `defaultSlackAuth` to stamp workspace-scoped auth (the same `auth` the per-tier playbook reads), and ignore bot chatter.
- **Delivery:** on `message.completed`, post the final reply to the thread, skipping interim tool-call narration.

\*\*Note: The continuation token is the thread\*\*

You don't manage Slack threading by hand. The Slack channel maps a thread to a durable session, so a follow-up mention in the same thread resumes the same conversation, the channel owns that resume handle, exactly the role the `continuationToken` played in 1.3.

Because Slack delivers over the public internet, you can't exercise this one on `localhost`. You'll deploy to get a URL. We cover deployment properly in Section 5; for now, ship it with `npx eve deploy`, which wraps `vercel deploy --prod`, installs dependencies, and pulls your environment:

```bash
npx eve deploy
```

## Try It

In a Slack workspace where the app is installed, mention the bot in a channel:

```text
@dispatcher my commuter's front brake is rubbing, what's that cost to fix?
```

It replies in a thread, runs `lookup_service`, and quotes the real price, the same dispatcher you've been talking to all along, now in Slack. Reply in the thread and it continues the same session. And the approval gate still holds: ask it to book the Full Overhaul and the approve/deny prompt renders as **Slack buttons**, which a mechanic can tap from their phone.

Bot shows up in Slack but never replies? The usual culprit is triggers. First confirm forwarding is enabled on the *connector* itself: `vercel connect attach` warns `Triggers are not enabled on this connector` if you created it without `--triggers`, and the fix is to recreate it (`vercel connect remove slack/spoke-and-mirror --disconnect-all --yes`, then `create … --triggers` and re-`attach`). Then confirm the destination: `--triggers` on `attach`, and `--trigger-path` exactly `/eve/v1/slack`. If it responds but can't read earlier thread messages, that's expected, the channel hands the model the triggering mention, not the whole backlog, unless you opt into thread context.

## Done-When

- [ ] A Connect Slack client is attached with trigger path `/eve/v1/slack`.
- [ ] `agent/channels/slack.ts` exports `slackChannel` with `connectSlackCredentials`.
- [ ] `@mentioning` the bot returns a real, tool-backed answer in a thread.
- [ ] An expensive booking renders approve/deny as Slack buttons.

## Solution

```ts title="agent/channels/slack.ts"
import { connectSlackCredentials } from "@vercel/connect/eve";
import { defaultSlackAuth, slackChannel } from "eve/channels/slack";

export default slackChannel({
  // The connector uid lives in SLACK_CONNECTOR (set it on the project, or leave
  // it unset). The fallback matches the connector you named with `vercel connect
  // create slack --name spoke-and-mirror`, so this works out of the box.
  credentials: connectSlackCredentials(
    process.env.SLACK_CONNECTOR ?? "slack/spoke-and-mirror",
  ),

  // Answer @mentions from a real user; ignore bot chatter. defaultSlackAuth
  // stamps workspace-scoped auth, which is what the per-tier playbook reads.
  onAppMention: (ctx, message) =>
    message.author ? { auth: defaultSlackAuth(message, ctx) } : null,

  events: {
    // Post the final reply to the thread, skipping interim tool-call narration.
    // Event handlers receive (eventData, channel, ctx); Slack handles live on `channel`.
    "message.completed"(eventData, channel, ctx) {
      if (eventData.finishReason === "tool-calls") return;
      if (eventData.message) channel.thread.post(eventData.message);
    },
  },
});
```

We now have two entrypoints to the same agent, web and Slack, and the agent didn't change for either. The final step before we ship is to add auth, to make sure the people getting in the door are authorized.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
