Vercel Logo

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.

Adding Slack should leave the tools, skill, state, and approval logic untouched. A channel normalizes incoming messages, tracks how to resume the conversation, and sends replies. One file in channels/ makes Slack another client of the dispatcher.

Credentials are the new concern here. Vercel Connect manages them, so your code never handles 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

Add the Slack channel. 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. eve 0.41.0 guides the whole setup:

npx eve add channel/slack

The command links or creates a Vercel project, creates or reuses a Slack connector, opens Slack authorization in your browser, registers /eve/v1/slack as the trigger destination, installs @vercel/connect, and writes agent/channels/slack.ts. Follow the prompts and finish the workspace authorization in the browser.

Use the current command shape

The old eve channels add slack command was removed. The registry form is eve add channel/slack. Let the guided setup reuse an existing connector when it finds one; repeatedly creating connectors can leave duplicate Slack apps in the workspace.

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.

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.

Inspect and extend the channel. The generated agent/channels/slack.ts defines where credentials come from, when to dispatch a turn, and how to deliver the reply. Replace it with the version in the solution below to make the dispatch and final-thread delivery explicit.

  • 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 trusted Slack identity and ignore bot chatter.
  • Delivery: on message.completed, post the final reply to the thread, skipping interim tool-call narration.
The thread maps to the session

You don't manage Slack threading by hand. The channel maps a thread to a durable session, so a follow-up mention in the same thread resumes the conversation, just as posting to the session ID did in 1.3.

Slack identity is not a shop membership tier

defaultSlackAuth stamps Slack attributes such as user, team, channel, and thread IDs. It does not know the customer's Spoke & Mirror membership, so the dynamic playbook uses the plain desk on Slack. A production extension can map the trusted Slack user ID to a server-side customer record and add attributes.tier; never derive the tier from message text.

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:

npx eve deploy

Try It

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

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

The bot replies in a thread, runs lookup_service, and quotes the catalog price. A reply in the thread continues the session. The approval gate also carries over: ask it to book the Full Overhaul and Slack renders the approve/deny prompt as buttons.

Bot shows up in Slack but never replies? Re-run npx eve add channel/slack; it inspects the current project, connector, Slack installation, and trigger destination without blindly creating another app. Confirm the destination is /eve/v1/slack. By default, the channel gives the model the triggering mention rather than the earlier thread backlog. Opt into thread context if you need that history.

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

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 Slack identity, but does not invent a shop membership tier.
  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);
    },
  },
});

The same agent now has web and Slack entrypoints. Before shipping, we'll replace the test identity with authenticated customer data.

Was this helpful?

supported.