All integrations

Slack

Channel

Mention your agent in channels and DMs, with Connect-managed auth.

At a glance

From Eve docs · synced Jul 22, 2026

Webhook route

POST /eve/v1/slack

Extension hooks

onAppMentiononDirectMessageonInteraction

Capabilities

  • Human-in-the-loopHITL renders as Slack buttons and selects.
  • Proactive sessionsStart a session without an inbound message through receive(slack, { message, target, auth }) from a schedule run handler, or args.receive(slack, ...) from another channel.
  • AttachmentsInbound files behind authenticated Slack URLs are staged with fetchFile.

Derived from Eve's Slack docs.

Install

The eve CLI scaffolds the channel for you. eve channels add slack writes agent/channels/slack.ts, adds @vercel/connect, and runs the Connect setup flow:

eve channels add slack

To wire it up by hand instead, install the framework and the Connect SDK. Slack channels use Vercel Connect for both the outbound bot token and inbound webhook verification:

npm install eve@latest @vercel/connect

Quick start

Create agent/channels/slack.ts. The channel name is derived from the filename, so no name field is needed:

// agent/channels/slack.tsimport { slackChannel } from "eve/channels/slack";import { connectSlackCredentials } from "@vercel/connect/eve";export default slackChannel({  credentials: connectSlackCredentials("slack/my-agent"),});

Link the project and pull OIDC env vars so Connect can authenticate locally:

vercel linkvercel env pull

Configure

Create a Slack Connect client and copy its UID (for example slack/my-agent), then attach this project as the webhook trigger destination at the route eve serves (/eve/v1/slack):

vercel connect create slack --triggers

The channel handles mentions, DMs, typing indicators, delivery, and human-in-the-loop consent with sensible defaults. See the Slack channel docs for customizing each behavior.

How it behaves

Excerpts from Eve's Slack docs. Prefer the source when something looks out of date.

Dispatch

Inbound hooks decide whether to dispatch a turn and with what auth. Return { auth } to dispatch, null to drop, or { auth, context } to inject background into history.

  • onAppMention(ctx, message) handles app_mention events. The default derives workspace-scoped auth and posts a Thinking… indicator.
  • onDirectMessage(ctx, message) handles message.im events (needs im:history scope). Bot-authored messages and edits are filtered out first.
  • onInteraction(action, ctx) handles block_actions callbacks not consumed by HITL.

eve attaches the triggering Slack user id to the same model message as the message text. This keeps speaker attribution intact when several people use one thread without making profile lookup requests.

You get the triggering mention by default, but not the earlier replies in the thread. Enable threadContext to fetch and inject them with every message attributed by stable Slack user id. Use since: "last-agent-reply" so repeated mentions inject only what is new:

import { slackChannel } from "eve/channels/slack";
import { connectSlackCredentials } from "@vercel/connect/eve";

export default slackChannel({
  credentials: connectSlackCredentials("slack/my-agent"),
  threadContext: { since: "last-agent-reply" },
});

since sets the boundary for what each mention injects and accepts three values:

  • "thread-root" (the default): every prior message in the thread, on every mention. threadContext: {} behaves the same.
  • "last-agent-reply": only messages after the agent's last reply, keeping repeated mentions incremental.
  • A predicate (message: SlackThreadMessage) => boolean: only messages after the last one it matches, such as "since the last message that mentioned a particular user".

threadContext performs one conversations.replies request for each triggering thread reply and requires the matching Slack history scope. Omit it when the agent should see only direct mentions. loadThreadContextMessages remains available when you need custom filtering or non-model processing of the raw thread messages.

Slack API calls outside a handler

Inside webhook-side handlers (onAppMention, onInteraction, events), ctx.slack.request(operation, body) is the raw-API escape hatch. Outside those contexts there is no handle — a schedule resolving reactions on old messages, for example, has no inbound Slack request. For that, call the same primitive the handle uses directly:

import { callSlackApi, resolveSlackBotToken } from "eve/channels/slack";
import { connectSlackCredentials } from "@vercel/connect/eve";

const { botToken } = connectSlackCredentials("slack/my-agent");
const response = await callSlackApi({
  botToken,
  operation: "reactions.get",
  body: { channel: "C0123456789", timestamp: "1712345678.000100", full: true },
});
if (!response.ok) throw new Error(String(response.error));

callSlackApi resolves function-form tokens (secret managers, Connect rotation) at call time and form-encodes the body — the only safe default, since Slack's JSON support is partial (conversations.replies rejects JSON). resolveSlackBotToken materializes a SlackBotToken to a string when you need the bearer token itself.

Delivery

The default handlers reply in-thread and show progress. Typing indicators post automatically: Thinking… on inbound, Working… on turn.started, a truncated reasoning snippet on reasoning.appended, and an action label on actions.requested — the tool name plus its most telling argument (grep useEveAgent, read_file agent/agent.ts), the subagent or remote-agent name for dispatched calls, and +N more when the model requests several actions at once. The model's own pre-tool narration, when present, takes precedence over the derived label. Reasoning snippets build progressively: extensions of at least four characters appear immediately, while smaller streamed deltas use the five-second refresh interval to avoid one Slack request per token. Override events["reasoning.appended"] if you prefer generic wording. Override onAppMention or the events handlers to customize.

Outbound text preserves bare @ tokens as literal text. To mention a user, embed Slack's <@USER_ID> syntax directly or use channel.thread.mentionUser(userId).

When a session starts without a threadTs (say, from a schedule or receive(slack, ...)), eve gives it a unique temporary continuation token. The first agent post anchors the session to the Slack message timestamp, and later posts and mentions resume that same session. Pass initialMessage with a Card to land a structured anchor first instead. threadTs and initialMessage are mutually exclusive.

The example below overrides onAppMention to gate on an authored message and posts the completed reply to the thread. Event handlers receive (eventData, channel, ctx), with Slack platform handles on channel.thread and channel.slack:

import { defaultSlackAuth, slackChannel } from "eve/channels/slack";
import { connectSlackCredentials } from "@vercel/connect/eve";

export default slackChannel({
  credentials: connectSlackCredentials("slack/my-agent"),
  onAppMention: (ctx, message) =>
    message.author ? { auth: defaultSlackAuth(message, ctx) } : null,
  events: {
    "message.completed"(eventData, channel, ctx) {
      if (eventData.finishReason === "tool-calls") return;
      if (eventData.message) channel.thread.post(eventData.message);
    },
  },
});
Human-in-the-loop (HITL)

HITL renders as Slack buttons and selects. When the user responds, the parked session (paused awaiting input) resumes.

Authorization prompts split public status from private credentials. A sign-in challenge (OAuth URL, device code) is a credential. Anyone who completes it binds their identity to the session's connection. The default authorization.required handler posts a public, link-free status in the thread, delivers the actual challenge ephemerally to the triggering user, device code included, and then updates that public status when authorization.completed fires. The handler receives a private-delivery context with postEphemeral, postDirectMessage (needs the im:write scope), and state. There is, intentionally, no public post and no raw API access.

events: {
  "authorization.required"(eventData, channel) {
    const userId = channel.state.triggeringUserId;
    if (!userId || !eventData.authorization?.url) return;
    return channel.postDirectMessage(userId, `Sign in to continue: ${eventData.authorization.url}`);
  },
},
Proactive sessions

Start a session without an inbound message through receive(slack, { message, target, auth }) from a schedule run handler, or args.receive(slack, ...) from another channel. The proactive target shape is { channelId }.

Attachments

Inbound files behind authenticated Slack URLs are staged with fetchFile. See File uploads for the fetchFile contract.

Agents using Slack

Engineering

PR Review Sentinel

Review pull requests against team conventions, flag risky changes, and post structured, actionable feedback.

+1
QA

Browser QA Runner

Walk critical user flows in a real browser, capture evidence, and file reproducible bug reports.

+1
Engineering

API Contract Guardian

Detect drift between API specs, real behavior, and docs; draft changelogs, migration notes, and fixes.

Research

Competitive Intel Scout

Track competitors' pricing, changelogs, and traffic; report evidence-cited changes since the last check.

+1
Customer Success

Churn Risk Sentinel

Correlate usage drop-off with billing events, remember account history, and propose specific save plays.

+1
Sales

Inbound Lead Qualifier

Qualify site visitors conversationally, score fit against your ICP, and hand sales a context-rich lead.

+1
Marketing

AI Search Visibility Analyst

Measure how your brand appears in AI answers and search, diagnose citation gaps, and prioritize content fixes.

+1
Finance

Spend & Subscription Controller

Flag card-spend anomalies, upcoming SaaS renewals, and duplicate subscriptions with owner-ready actions.

Community

Developer Community Manager

Triage community questions, escalate real bugs with reproductions, and turn recurring pain into roadmap signal.

+1
Starters

Daily Digest Starter

Weekday 9am UTC: fetch one RSS feed and post a five-item summary to Slack. Demos cron schedules.

Starters

Approval Gate Starter

Refunds always need Slack approval; service restarts use once(). Tool bodies log only — swap in real actions.

Starters

Webhook Summarizer Starter

POST any text to a custom webhook and get a classified summary in Slack. Demos custom channels.