Connectors
Slack
Talk to an agent by mentioning it in a Slack channel or thread.
Quickstart
Create a Slack app
In Slack’s app settings, create an app, add the app_mentions:read and chat:write bot token scopes, and install it to your workspace. Copy the bot token and the signing secret.
Add the route
npm add @rivet-dev/pi rivetkit hono @slack/web-api
import { pi } from "@rivet-dev/pi";
import { Hono } from "hono";
import { setup } from "rivetkit";
import { postReply, slack, type SlackThread } from "./slack";
const agent = pi({
model: "anthropic/claude-opus-5-5",
actions: {
receive: async (c, thread: SlackThread) => {
await c.schedule.after(0, "reply", thread);
},
reply: async (c, thread: SlackThread): Promise<void> => {
const self = c.client<typeof registry>().agent.getForId(c.actorId);
await self.prompt(thread.text);
await postReply(thread, await self.getLastAssistantText());
},
},
});
export const registry = setup({ use: { agent } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.route("/slack", slack);
export default app;
import { createHmac, timingSafeEqual } from "node:crypto";
import { WebClient } from "@slack/web-api";
import { Hono } from "hono";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
export type SlackThread = { channel: string; threadTs: string; text: string };
const web = new WebClient(process.env.SLACK_BOT_TOKEN);
const client = createClient<typeof registry>();
export const slack = new Hono();
slack.post("/events", async (c) => {
const body = await c.req.text();
const timestamp = c.req.header("x-slack-request-timestamp") ?? "";
if (!verifySlackSignature(body, timestamp, c.req.header("x-slack-signature") ?? "")) {
return c.text("invalid signature", 401);
}
const payload = JSON.parse(body);
if (payload.type === "url_verification") return c.json({ challenge: payload.challenge });
if (c.req.header("x-slack-retry-num")) return c.body(null, 200);
const event = payload.event;
if (payload.type === "event_callback" && event?.type === "app_mention") {
const threadTs = event.thread_ts ?? event.ts;
const agent = client.agent.getOrCreate([payload.team_id, event.channel, threadTs]);
await agent.receive({ channel: event.channel, threadTs, text: event.text });
}
return c.body(null, 200);
});
export async function postReply(thread: SlackThread, reply: string | undefined) {
await web.chat.postMessage({
channel: thread.channel,
thread_ts: thread.threadTs,
text: reply || "I finished without a reply.",
});
}
function verifySlackSignature(body: string, timestamp: string, signature: string): boolean {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 60 * 5) return false;
const secret = process.env.SLACK_SIGNING_SECRET ?? "";
const expected = `v0=${createHmac("sha256", secret).update(`v0:${timestamp}:${body}`).digest("hex")}`;
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Run the server with any runtime that serves a fetch handler, for example npx srvx --import tsx server.ts. See HTTP Server.
Set the environment
SLACK_SIGNING_SECRET=...
SLACK_BOT_TOKEN=xoxb-...
ANTHROPIC_API_KEY=sk-ant-...
Subscribe to mentions
Under Event Subscriptions, set the request URL to https://<your-worker>/slack/events and subscribe to the app_mention bot event. Slack sends a challenge, which the route answers. Mention the app in a channel to start a conversation.
How it works
- The route verifies Slack’s signature against the raw body and rejects requests older than five minutes.
- Each Slack thread gets its own agent, keyed by team, channel, and thread. Mentioning the app again in the thread continues the same session.
- Slack expects a response within three seconds and an agent run takes longer, so the route only hands the message to the agent’s
receiveaction and returns.receiveschedulesreply, which prompts the agent and posts the answer in the thread with the Slack Web API. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away. - Slack retries events it thinks timed out. The route ignores retries, because the first delivery is already being answered.
Configure
| Variable | Purpose |
|---|---|
SLACK_SIGNING_SECRET | Required. Verifies that requests come from Slack. |
SLACK_BOT_TOKEN | Required. Posts replies with the Slack Web API. |
ANTHROPIC_API_KEY | Required. The agent’s model key. See API Keys. |
Supported events
| Slack surface | Webhook path | What the route does |
|---|---|---|
Event Subscriptions, app_mention | /slack/events | Hands the message to the thread’s agent, which replies in the thread. |
| URL verification | /slack/events | Answers Slack’s challenge. |
To answer direct messages too, subscribe to message.im, add the im:history scope, and handle message events the same way.