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",
state: { pending: [] as SlackThread[] },
vars: { replying: false },
actions: {
receive: async (c, thread: SlackThread) => {
c.state.pending.push(thread);
await c.schedule.after(0, "drain");
},
// Answers the oldest message. Messages that arrive mid-run wait their turn.
drain: async (c): Promise<void> => {
const thread = c.state.pending[0];
if (!thread || c.vars.replying) return;
c.vars.replying = true;
try {
const self = c.client<typeof registry>().agent.getForId(c.actorId);
await self.prompt(thread.text);
await postReply(thread, await self.getLastAssistantText());
} catch (error) {
c.log.error({ msg: "reply failed", error });
} finally {
c.state.pending.shift();
c.vars.replying = false;
}
if (c.state.pending.length > 0) await c.schedule.after(0, "drain");
},
},
});
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. Thereceiveaction adds the message to the agent’s queue and schedulesdrain, which prompts the agent with the oldest message and posts the answer in the thread with the Slack Web API. A message that arrives while the agent is answering waits in the queue. The queue is saved with the agent, so replies still run if the request that delivered a 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. For another provider, change model and set its key instead, such as OPENAI_API_KEY or OPENROUTER_API_KEY. See LLM 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.