vercel-labs/slack-agent
Use when building Slack agents/bots with eve (Vercel's filesystem-first agent framework), @vercel/connect, or eve/channels/slack. Covers defineAgent/defineTool patterns, Vercel Connect credential brokering, Slack channel setup, testing requirements, and quality standards.
npx skills add https://github.com/vercel-labs/slack-agent-skill --skill slack-agent
This skill builds Slack agents with eve — Vercel's filesystem-first framework for durable backend agents — using Vercel Connect for Slack credentials:
eve package) — agent runtime, tools, channels, durabilityWhen this skill is invoked via /slack-agent, check for arguments and route accordingly:
| Argument | Action |
|----------|--------|
| new | Run the setup wizard from Phase 1. Read ./wizard/1-project-setup.md and guide the user through creating a new Slack agent. |
| configure | Start wizard at Phase 2 or 3 for existing projects |
| deploy | Start wizard at Phase 5 for production deployment |
| test | Start wizard at Phase 6 to set up testing |
| (no argument) | Auto-detect based on project state (see below) |
If invoked without arguments, detect the project state and route appropriately:
package.json with eve and no agent/ directory → Treat as new, start Phase 1agent/channels/slack.ts → Start Phase 2 (Slack connector + channel)SLACK_CONNECTOR configured → Start Phase 3Detect an eve project by either signal:
package.json contains "eve" as a dependencyagent/ directory with instructions.md and/or agent.ts existsIf neither is present, this is a new project: scaffold with npx eve@latest init (Node 24+ required).
The wizard is located in ./wizard/ with these phases:
1-project-setup.md - Understand purpose, generate custom implementation plan, scaffold with npx eve@latest init1b-approve-plan.md - Present plan for user approval before scaffolding2-create-slack-app.md - Create the Slack connector with Vercel Connect and add the Slack channel3-configure-environment.md - Set up env vars (SLACK_CONNECTOR, model credentials)4-test-locally.md - Test agent logic locally with the eve dev TUI (Slack surface tests happen after deploy)5-deploy-production.md - Deploy with eve deploy, verify the Slack surface6-setup-testing.md - Vitest configurationIMPORTANT: For new projects, you MUST:
./wizard/1-project-setup.md first./reference/agent-archetypes.mdYou are working on a Slack agent project built with eve. Follow these mandatory practices for all code changes.
eve/channels/slack + @vercel/connect for credentialsanthropic/claude-sonnet-5); tool schemas with zodnpx eve@latest init installs with npm){
"engines": { "node": "24.x" },
"dependencies": {
"eve": "latest",
"ai": "latest",
"zod": "^3.x",
"@vercel/connect": "latest"
}
}
In eve, a file's location says what it does, and its path usually gives it its name. The whole agent lives under agent/:
agent/
├── instructions.md # Always-on system prompt
├── agent.ts # Runtime config (defineAgent): model, reasoning, compaction
├── tools/ # Tools — filename (snake_case ASCII) = tool name the model sees
│ ├── get_weather.ts
│ └── search_docs.ts
├── skills/ # Load-on-demand instructions (*.md with description frontmatter)
│ └── incident-triage.md
├── channels/
│ └── slack.ts # Slack channel — filename registers it at /eve/v1/slack
├── connections/ # MCP / OpenAPI connections (optional)
└── hooks/ # Subscribe to runtime stream events (optional)
Even a two-file agent (instructions.md + agent.ts) gets file, shell, web, and delegation tools out of the box from the default harness. Full docs are bundled at node_modules/eve/docs/ once eve is installed — read them when a detail isn't covered here.
// agent/agent.ts
import { defineAgent } from "eve";
export default defineAgent({
model: "anthropic/claude-sonnet-5", // routed via Vercel AI Gateway
});
These quality requirements MUST be followed for every code change. There are no exceptions.
pnpm lint
pnpm lint --write for auto-fixespnpm lint to verifyfoo.ts, check if foo.test.ts existsYou MUST run all quality checks and fix any issues before marking a task complete:
# 1. TypeScript compilation - must pass
pnpm typecheck
# 2. Linting - must pass with no errors
pnpm lint
# 3. Tests - all tests must pass
pnpm test
Do NOT complete a task if any of these fail. Fix the issues first.
For ANY code change, you MUST write or update unit tests.
*.test.ts files (e.g. agent/tools/get_weather.test.ts)execute() (including error paths) must have testsExample test structure:
import { describe, it, expect, vi } from 'vitest';
import { myFunction } from './my-module';
describe('myFunction', () => {
it('should handle normal input', () => {
expect(myFunction('input')).toBe('expected');
});
it('should handle edge cases', () => {
expect(myFunction('')).toBe('default');
});
});
If you modify:
onAppMention, onDirectMessage, onInteraction)You MUST add or update tests that verify the full flow. Remember: the Slack surface itself cannot be exercised locally (see Gotchas), so E2E coverage means unit/integration tests around your handlers plus a post-deploy smoke test.
agent/channels/slack.ts)The Slack channel is a single file. Its filename registers the slack channel, served at /eve/v1/slack — this is the canonical trigger path everywhere in this skill.
// agent/channels/slack.ts
import { connectSlackCredentials } from "@vercel/connect/eve";
import { slackChannel } from "eve/channels/slack";
export default slackChannel({
credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR!),
});
connectSlackCredentials(connectorUid) returns { botToken, webhookVerifier }:
There is no SLACK_BOT_TOKEN and no SLACK_SIGNING_SECRET in this stack. The only Slack env var is SLACK_CONNECTOR (the connector UID, e.g. slack/my-agent).
Create a Slack connector and point its trigger at eve's Slack route:
npm install -g vercel@latest
# Create the connector with event forwarding enabled
vercel connect create slack --triggers
# Attach the project as a trigger destination on eve's route
# (the default trigger path is /slack — set it explicitly):
vercel connect attach <uid> --triggers --trigger-path /eve/v1/slack --yes
--triggers is required. Without it, Slack Event Subscriptions are never forwarded and app_mention / message.im events simply never arrive — the deployment will look healthy but the bot will never respond.
You can also add the channel with eve channels add slack, which scaffolds agent/channels/slack.ts for you.
eve deploy
# wraps: vercel deploy --prod
Then invite the bot to a channel and @mention it. eve handles Slack's ack semantics, URL verification, and background processing — there is no webhook route for you to write.
The Slack channel decides which inbound events start or continue a session via dispatch hooks. Each hook returns { auth } to dispatch, null to drop the event, or { auth, context } to inject background context into the session:
export default slackChannel({
credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR!),
// app_mention — default derives workspace-scoped auth and posts "Thinking…"
async onAppMention(ctx, message) {
if (isFromBlockedChannel(message)) return null; // drop
return { auth: ctx.defaultAuth };
},
// message.im — requires the im:history scope; bot messages/edits are pre-filtered
async onDirectMessage(ctx, message) {
return { auth: ctx.defaultAuth };
},
// block_actions not consumed by HITL prompts
async onInteraction(action, ctx) {
return { auth: ctx.defaultAuth };
},
});
The triggering Slack user's id is attached to the model message automatically, preserving speaker attribution in multi-user threads.
Override delivery per stream event with the events map. Handlers receive (eventData, channel, ctx) with channel.thread and channel.slack handles:
export default slackChannel({
credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR!),
events: {
"message.completed"(eventData, channel, ctx) {
if (eventData.finishReason === "tool-calls") return;
if (eventData.message) channel.thread.post(eventData.message);
},
},
});
Key stream events: session.started, actions.requested, action.result, message.completed, session.completed; incremental reasoning.appended / message.appended are optional.
Give the agent prior thread messages when it's triggered mid-thread:
export default slackChannel({
credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR!),
threadContext: { since: "last-agent-reply" },
});
since options:
"thread-root" — all prior messages (default when thread context is enabled)"last-agent-reply" — incremental, only messages since the agent last spoke(message: SlackThreadMessage) => boolean as a custom cutoff — includes messages after the last match (loadThreadContextMessages exists for arbitrary filtering)Cost: one conversations.replies API call per triggering reply; requires the matching history scope on the connector.
Approval-gated tool calls and sign-in challenges render natively in Slack:
authorization.completedpostEphemeral, postDirectMessage (needs im:write), and state — no public post, no raw API accessStart a session that posts into Slack without an inbound trigger — e.g. from a schedule:
import { receive } from "eve";
import slack from "../channels/slack";
await receive(slack, {
message: "Post the daily standup summary for #eng.",
target: { channelId: "C0123456789" },
auth,
});
threadTs get a temporary continuation token; the first post anchors the threadinitialMessage (optionally a Card) and threadTs are mutually exclusivectx.slack.request(operation, body)callSlackApi({ botToken, operation, body }) and resolveSlackBotToken from eve/channels/slackThese form-encode request bodies for you — Slack's JSON support is only partial, so prefer these helpers over hand-rolled fetch calls.
Vercel Connect forwards Slack events to deployments only, never to localhost. There is no ngrok/Socket Mode escape hatch in this stack. Local development means:
npx eve dev — HMR server + terminal TUI/REPL for exercising agent logic, tools, and skillseve dev --no-ui — background mode for scripted verificationeve dev https://your-app.vercel.app — drive a *deployed* app interactivelyTo test @mentions and DMs, deploy (preview or production) and test in Slack itself.
Connect may deliver the same forwarded event more than once. Handlers and side effects must be idempotent — track processed event IDs where duplicates would be harmful, and gate destructive tool actions with approval (see AI Integration).
--triggers Is Required or Events Never ArriveA Slack connector created without --triggers (or attached without a trigger path) will authenticate fine but forward nothing. If the bot never responds to @mentions:
--triggers/eve/v1/slackplaceholderAuth() Fails Closed in ProductionScaffolded projects ship with placeholderAuth() for the HTTP API, which rejects everything in production. Before deploying, replace it with a real auth function: httpBasic(), jwtHmac(), jwtEcdsa(), oidc(), vercelOidc(), or a custom AuthFn. (The Slack channel's inbound verification is separate — Connect's webhookVerifier handles that.)
Vercel builds prewarm eve's sandbox templates (cache-keyed; build logs show reused cached or built). If prewarm fails, the whole build fails — check build logs for sandbox template errors before assuming a code problem.
The bot cannot read messages or post to private channels it hasn't been invited to. When creating features that will later post to a channel (e.g. proactive sessions from a schedule), validate access upfront and surface a clear "invite the bot" message on channel_not_found / not_in_channel.
When fetching channel context (e.g. via ctx.slack.request("conversations.history", ...)) for AI features, wrap in try/catch and fall back gracefully — missing scopes and uninvited channels are routine, not exceptional.
If you add custom cron endpoints (beyond eve schedules), protect them with a CRON_SECRET:
export async function GET(request: Request) {
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Run cron job logic...
return Response.json({ success: true });
}
{
"crons": [
{
"path": "/api/cron/my-job",
"schedule": "0 * * * *"
}
]
}
Prefer eve schedules for agent-driven recurring work; use Vercel crons for plain HTTP jobs.
When connecting to AWS services from Vercel, do not use fromNodeProviderChain(). Use Vercel's OIDC mechanism:
import { awsCredentialsProvider } from "@vercel/functions/oidc";
const s3Client = new S3Client({
credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN! }),
});
eve routes model-ID strings through the Vercel AI Gateway — on Vercel, project OIDC authenticates automatically, so no AI API key is needed:
// agent/agent.ts
import { defineAgent } from "eve";
export default defineAgent({
model: "anthropic/claude-sonnet-5", // string → AI Gateway → OIDC auth on Vercel
});
If model is omitted, eve defaults to anthropic/claude-sonnet-5. Off Vercel, set AI_GATEWAY_API_KEY.
CRITICAL: Never use model IDs from memory. Model IDs change frequently. Before writing code that pins a model, run curl -s https://ai-gateway.vercel.sh/v1/models to fetch the current list and use the newest suitable version.
agent/tools/*.ts)Tools are files: the filename (snake_case ASCII) is the model-facing tool name — agent/tools/get_weather.ts → get_weather. No registration step.
// agent/tools/get_weather.ts
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string().min(1) }), // required, even if empty
outputSchema: z.object({
city: z.string(),
condition: z.string(),
temperatureF: z.number(),
}), // optional — types/validates the return
async execute({ city }, ctx) {
return { city, condition: "Sunny", temperatureF: 72 };
},
});
Rules and capabilities:
process.env, not in the sandboxinputSchema accepts Zod, Standard Schema, or JSON Schema — but it is required even for zero-input toolsctx provides ctx.session (metadata, turn, auth, lineage), ctx.callId, ctx.toolName, ctx.abortSignal, ctx.getSandbox(), ctx.getSkill(id)Gate risky tools with the approval field — helpers come from eve/tools/approval:
import { defineTool } from "eve/tools";
import { always, once, never } from "eve/tools/approval";
import { z } from "zod";
export default defineTool({
description: "Delete a document permanently.",
inputSchema: z.object({ documentId: z.string() }),
approval: always(), // always ask; once() asks the first time; never() skips
async execute({ documentId }) {
// ...
},
});
An input-dependent policy function is also supported. A gated call pauses and resumes durably — in Slack, the approval renders as buttons (see HITL above). Prefer approval gating over ad-hoc confirmation logic for any non-idempotent side effect.
toModelOutput — Rich Slack Output the Model Never SeesShow the model a compact projection while channels/hooks receive the full output on action.result — ideal for rendering rich Slack Block Kit from a tool result without stuffing JSON blocks into the model's context:
export default defineTool({
description: "Look up an order.",
inputSchema: z.object({ orderId: z.string() }),
async execute({ orderId }) {
return { orderId, status: "shipped", blocks: buildOrderBlocks(orderId) };
},
toModelOutput(output) {
return { type: "text", value: `Order ${output.orderId}: ${output.status}` };
},
});
agent/skills/*.md)Markdown files with a description frontmatter, loaded on demand via the built-in load_skill tool when a request matches the description. Skills add instructions only, never new actions. Install published skills with npx skills add <owner>/<repo>.
agent/connections/*.ts)For external APIs the agent should drive (MCP servers or OpenAPI-described HTTP APIs), use connections with Connect-brokered auth:
// agent/connections/linear.ts
import { connect } from "@vercel/connect/eve";
import { defineMcpClientConnection } from "eve/connections";
export default defineMcpClientConnection({
url: "https://mcp.linear.app/mcp",
description: "Linear workspace: issues, projects, cycles, and comments.",
auth: connect("linear/my-agent"),
});
Connection tokens are never seen by the model and never land in conversation history. Inside authored tools, resolve tokens with await ctx.getToken(connect("...")) and call ctx.requireAuth(...) on a downstream 401 to re-run consent.
Don't wrap LLM calls in tools. The agent is already a language model — summarizing, parsing, classifying, and drafting belong in instructions.md or a skill, not in a tool that calls the AI SDK. Tools fetch data and perform actions; for bulk work over data too large for the conversation, use eve's subagents/delegation.
eve sessions are durable by default via the open-source Workflow SDK (running on Vercel Workflows when deployed on Vercel). You do not wire up Redis or a workflow engine yourself.
Consequences for your code:
always() / once()) so a replayed step pauses for a human instead of double-executingcontinuationToken; Slack threads map to sessions automaticallyFor application data (not agent session state — eve owns that):
IMPORTANT: Vercel KV has been deprecated. Do NOT recommend Vercel KV.
agent/
├── instructions.md # System prompt — keep focused; push detail into skills
├── agent.ts # defineAgent: model, reasoning effort, compaction
├── tools/
│ ├── get_weather.ts # One tool per file; filename = tool name
│ ├── get_weather.test.ts # Co-located tests
│ └── search_docs.ts
├── skills/
│ └── report-format.md # description frontmatter + on-demand instructions
├── channels/
│ └── slack.ts # slackChannel(...) — served at /eve/v1/slack
├── connections/
│ └── linear.ts # MCP/OpenAPI connections (optional)
└── hooks/
└── audit.ts # Runtime stream event subscribers (optional)
Conventions:
instructions.md; situational guidance in skills/*.md so it loads only when needed| Variable | Required | Purpose |
|----------|----------|---------|
| SLACK_CONNECTOR | Yes | Vercel Connect connector UID (e.g. slack/my-agent). The only Slack variable — no bot token, no signing secret. |
| AI_GATEWAY_API_KEY | Off Vercel only | AI Gateway auth. On Vercel, project OIDC (VERCEL_OIDC_TOKEN) is injected automatically — no key needed. |
| ROUTE_AUTH_BASIC_PASSWORD / JWT keys | Per auth choice | Secrets for the HTTP-API auth function that replaces placeholderAuth() |
| VERCEL_AUTOMATION_BYPASS_SECRET | If deployment protection is on | Lets eve dev https://<app> and smoke tests reach protected deployments |
| CRON_SECRET | Optional | Authenticates custom cron endpoints |
Local dev: vercel link + vercel env pull fetches short-lived Connect/OIDC credentials into .env.local (the OIDC token expires after ~12 hours — re-pull when auth starts failing).
No AI API keys needed on Vercel. Never hardcode credentials. Never commit .env files.
The Slack channel handles progressive delivery for you:
turn.startedreasoning.appended; action labels on actions.requestedDon't rebuild typing indicators or streaming loops — customize via the events map only when the defaults don't fit.
Use <@USER_ID> or channel.thread.mentionUser(userId). A bare @name stays literal text in Slack.
Use Slack mrkdwn (not standard markdown):
*text*_text_ code <@USER_ID><#CHANNEL_ID>For rich tool results (tables, buttons, status cards), return full data from the tool and use toModelOutput to keep the model's view compact; render Block Kit in a channel event handler or via ctx.slack.request("chat.postMessage", { blocks, ... }). Always include fallback text alongside blocks for notifications.
For detailed Slack patterns, see ./patterns/slack-patterns.md.
Use conventional commits:
feat: add channel search tool
fix: resolve thread pagination issue
test: add unit tests for agent context
docs: update README with setup steps
refactor: extract Slack client utilities
Never commit:
.env filesnode_modules/.eve/ build artifacts# Scaffold (Node 24+)
npx eve@latest init my-agent # new project (installs deps, inits Git, starts dev TUI)
npx eve@latest init . # add eve to an existing project
# Development
npx eve dev # HMR server + terminal TUI/REPL
npx eve dev --no-ui # background mode for scripted verification
npx eve dev https://<app> # drive a deployed app interactively
npx eve info # project info
# Vercel Connect
vercel connect create slack --triggers
vercel connect attach <uid> --triggers --trigger-path /eve/v1/slack --yes
vercel connect list
# Quality
pnpm lint # Check linting
pnpm lint --write # Auto-fix lint
pnpm typecheck # TypeScript check
pnpm test # Run all tests
# Build & Deploy
npx eve build # Compile into .eve/ (Vercel Build Output when VERCEL is set)
eve deploy # Deploy (wraps vercel deploy --prod)
# Verify a deployment
curl https://<app>/eve/v1/health
Debugging a deployed Slack agent stuck on "Working…": npx eve dev --logs all or /loglevel all in the TUI.
For detailed guidance, read:
./patterns/testing-patterns.md./patterns/slack-patterns.md./reference/env-vars.md./reference/slack-setup.md./reference/vercel-setup.mdnode_modules/eve/docs/ after install)Before marking ANY task as complete, verify:
pnpm lint passes with no errorspnpm typecheck passes with no errorspnpm test passes with no failuresSLACK_CONNECTORinputSchema; outputs are JSON-serializable with secrets redacted/eve/v1/slack and was attached with --triggersplaceholderAuth() replaced before production deployanthropic/claude-sonnet-5 default)Verified guides on the Vercel Knowledge Base for deeper walkthroughs:
defineTool patternsTake vercel-labs/slack-agent from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.