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 patternsAnalyzes meeting transcripts and recordings to uncover behavioral patterns, communication insights, and actionable feedback. Identifies when you avoid conflict, use filler words, dominate conversations, or miss opportunities to listen. Perfect for professionals seeking to improve their communication and leadership skills.
Toolkit for creating animated GIFs optimized for Slack, with validators for size constraints and composable animation primitives. This skill applies when users request animated GIFs or emoji animations for Slack from descriptions like "make me a GIF for Slack of X doing Y".
Analyzes your recent Claude Code chat history to identify coding patterns, development gaps, and areas for improvement, curates relevant learning resources from HackerNews, and automatically sends a personalized growth report to your Slack DMs.
Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack.
A skill that creates new Claude skills and automatically shares them on Slack using Rube for seamless team collaboration and skill discovery.
Prepare meeting materials with Notion context and Codex research; use when gathering context, drafting agendas/pre-reads, and tailoring materials to attendees.
Interactive daily standup/meeting update generator. Use when user says 'daily', 'standup', 'scrum update', 'status update', 'what did I do yesterday', 'prepare for meeting', 'morning update', or 'team sync'. Pulls activity from GitHub, Jira, and Claude Code session history. Conducts 4-question interview (yesterday, today, blockers, discussion topics) and generates formatted Markdown update.
Guide technical communication for software developers. Covers email structure, team messaging etiquette, meeting agendas, and adapting messages for technical vs non-technical audiences. Use when drafting professional messages, preparing meeting communications, or improving written communication.
Take 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.