# Slack Agent

> Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing requirements, and quality standards.

- **Type:** Skill
- **Install:** `agentstack add skill-vercel-labs-slack-agent-skill-slack-agent-skill`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [vercel-labs](https://agentstack.voostack.com/s/vercel-labs)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [vercel-labs](https://github.com/vercel-labs)
- **Source:** https://github.com/vercel-labs/slack-agent-skill
- **Website:** https://github.com/vercel-labs/slack-agent-skill

## Install

```sh
agentstack add skill-vercel-labs-slack-agent-skill-slack-agent-skill
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Slack Agent Development Skill

This skill supports two frameworks for building Slack agents:

- **Chat SDK** (Recommended for new projects) — `chat` + `@chat-adapter/slack`
- **Bolt for JavaScript** (For existing Bolt projects) — `@slack/bolt` + `@vercel/slack-bolt`

## Skill Invocation Handling

When this skill is invoked via `/slack-agent`, check for arguments and route accordingly:

### Command Arguments

| 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) |

### Auto-Detection (No Argument)

If invoked without arguments, detect the project state and route appropriately:

1. **No `package.json` with `chat` or `@slack/bolt`** → Treat as `new`, start Phase 1
2. **Has project but no customized `manifest.json`** → Start Phase 2
3. **Has project but no `.env` file** → Start Phase 3
4. **Has `.env` but not tested** → Start Phase 4
5. **Tested but not deployed** → Start Phase 5
6. **Otherwise** → Provide general assistance using this skill's patterns

### Framework Detection

Detect which framework the project uses:

- **`package.json` contains `"chat"`** → Chat SDK project
- **`package.json` contains `"@slack/bolt"`** → Bolt project
- **Neither detected** → New project, recommend Chat SDK (offer Bolt as alternative)

Store the detected framework and use it to show the correct patterns throughout the wizard and development guidance.

### Wizard Phases

The wizard is located in `./wizard/` with these phases:
- `1-project-setup.md` - Understand purpose, choose framework, generate custom implementation plan
- `1b-approve-plan.md` - Present plan for user approval before scaffolding
- `2-create-slack-app.md` - Customize manifest, create app in Slack
- `3-configure-environment.md` - Set up .env with credentials
- `4-test-locally.md` - Dev server + ngrok tunnel
- `5-deploy-production.md` - Vercel deployment
- `6-setup-testing.md` - Vitest configuration

**IMPORTANT:** For `new` projects, you MUST:
1. Read `./wizard/1-project-setup.md` first
2. Ask the user what kind of agent they want to build
3. Offer framework choice (Chat SDK recommended, Bolt as alternative)
4. Generate a custom implementation plan using `./reference/agent-archetypes.md`
5. Present the plan for approval (Phase 1b) BEFORE scaffolding the project
6. Only proceed to scaffold after the plan is approved

---

## Framework Selection Guide

| Aspect | Chat SDK | Bolt for JavaScript |
|--------|----------|---------------------|
| **Best for** | New projects | Existing Bolt codebases |
| **Packages** | `chat`, `@chat-adapter/slack`, `@chat-adapter/state-redis` | `@slack/bolt`, `@vercel/slack-bolt` |
| **Server** | Next.js App Router | Nitro (H3-based) |
| **Event handling** | `bot.onNewMention()`, `bot.onSubscribedMessage()` | `app.event()`, `app.command()`, `app.message()` |
| **Webhook route** | `app/api/webhooks/[platform]/route.ts` | `server/api/slack/events.post.ts` |
| **Message posting** | `thread.post("text")` / `thread.post(...)` | `client.chat.postMessage({ channel, text, blocks })` |
| **UI components** | JSX: ``, ``, `` | Raw Block Kit JSON |
| **State** | `@chat-adapter/state-redis` / `thread.state` | Manual / Vercel Workflow |
| **Config** | `new Chat({ adapters: { slack } })` | `new App({ token, signingSecret, receiver })` |

---

## General Development Guidance

You are working on a Slack agent project. Follow these mandatory practices for all code changes.

## Project Stack

### If using Chat SDK

- **Framework**: Next.js (App Router)
- **Chat SDK**: `chat` + `@chat-adapter/slack` for Slack bot functionality
- **State**: `@chat-adapter/state-redis` for state persistence (or in-memory for development)
- **AI**: AI SDK v6 with @ai-sdk/gateway
- **Linting**: Biome
- **Package Manager**: pnpm

```json
{
  "dependencies": {
    "ai": "^6.0.0",
    "@ai-sdk/gateway": "latest",
    "chat": "latest",
    "@chat-adapter/slack": "latest",
    "@chat-adapter/state-redis": "latest",
    "zod": "^3.x",
    "next": "^15.x"
  }
}
```

### If using Bolt for JavaScript

- **Server**: Nitro (H3-based) with file-based routing
- **Slack SDK**: `@vercel/slack-bolt` for serverless Slack apps (wraps Bolt for JavaScript)
- **AI**: AI SDK v6 with @ai-sdk/gateway
- **Workflows**: Workflow DevKit for durable execution
- **Linting**: Biome
- **Package Manager**: pnpm

```json
{
  "dependencies": {
    "ai": "^6.0.0",
    "@ai-sdk/gateway": "latest",
    "@slack/bolt": "^4.x",
    "@vercel/slack-bolt": "^1.0.2",
    "zod": "^3.x"
  }
}
```

**Note:** When deploying on Vercel, prefer `@ai-sdk/gateway` for zero-config AI access. Use direct provider SDKs (`@ai-sdk/openai`, `@ai-sdk/anthropic`, etc.) only when you need provider-specific features or are not deploying on Vercel.

---

## Quality Standards (MANDATORY)

These quality requirements MUST be followed for every code change. There are no exceptions.

### After EVERY File Modification

1. **Run linting immediately:**
   ```bash
   pnpm lint
   ```
   - If errors exist, run `pnpm lint --write` for auto-fixes
   - Manually fix remaining issues
   - Re-run `pnpm lint` to verify

2. **Check for corresponding test file:**
   - If you modified `foo.ts`, check if `foo.test.ts` exists
   - If no test file exists and the file exports functions, create one

### Before Completing ANY Task

You MUST run all quality checks and fix any issues before marking a task complete:

```bash
# 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.

### Unit Tests Required

**For ANY code change, you MUST write or update unit tests.**

### If using Chat SDK

- **Location**: Co-located `*.test.ts` files or `lib/__tests__/`
- **Framework**: Vitest
- **Coverage**: All exported functions must have tests

### If using Bolt for JavaScript

- **Location**: Co-located `*.test.ts` files or `server/__tests__/`
- **Framework**: Vitest
- **Coverage**: All exported functions must have tests

Example test structure:
```typescript
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');
  });
});
```

### E2E Tests for User-Facing Changes

If you modify:
- Bot mention handlers / Slack message handlers
- Slash commands
- Interactive components (buttons, modals)
- Bot responses

You MUST add or update E2E tests that verify the full flow.

---

## Bot Setup Patterns (CRITICAL)

### If using Chat SDK

Use the Chat SDK to define your bot instance. This is the central entry point for all Slack bot functionality.

#### Bot Instance (`lib/bot.ts` or `lib/bot.tsx`)

```typescript
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";

export const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
  },
  state: createRedisState(),
});
```

**Note:** If your bot uses JSX components (Card, Button, etc.), the file must use the `.tsx` extension.

#### Webhook Route (`app/api/webhooks/[platform]/route.ts`)

```typescript
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request, context: { params: Promise }) {
  const { platform } = await context.params;
  const handler = bot.webhooks[platform as keyof typeof bot.webhooks];
  if (!handler) return new Response("Unknown platform", { status: 404 });
  return handler(request, { waitUntil: (task) => after(() => task) });
}
```

The Chat SDK automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- Slack's 3-second ack timeout
- Background processing via `waitUntil`
- Signature verification

### If using Bolt for JavaScript

Use `@vercel/slack-bolt` to handle all Slack events. This package automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- 3-second ack timeout (built-in `ackTimeoutMs: 3001`)
- Background processing via Vercel Fluid Compute's `waitUntil`

#### Bolt App Setup (`server/bolt/app.ts`)

```typescript
import { App } from "@slack/bolt";
import { VercelReceiver } from "@vercel/slack-bolt";

const receiver = new VercelReceiver();
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  receiver,
  deferInitialization: true,
});

export { app, receiver };
```

#### Events Handler (`server/api/slack/events.post.ts`)

```typescript
import { createHandler } from "@vercel/slack-bolt";
import { defineEventHandler, getRequestURL, readRawBody } from "h3";
import { app, receiver } from "../../bolt/app";

const handler = createHandler(app, receiver);

export default defineEventHandler(async (event) => {
  const rawBody = await readRawBody(event, "utf8");
  const request = new Request(getRequestURL(event), {
    method: event.method,
    headers: event.headers,
    body: rawBody,
  });
  return await handler(request);
});
```

**Why buffer the body?** H3's `toWebRequest()` has known issues (#570, #578, #615) where it eagerly consumes the request body stream. When `@vercel/slack-bolt` later calls `req.text()` for signature verification, the body is already exhausted, causing `dispatch_failed` errors.

#### VercelReceiver Options Reference

| Parameter | Default | Description |
|-----------|---------|-------------|
| `signingSecret` | `SLACK_SIGNING_SECRET` env var | Request verification secret |
| `signatureVerification` | `true` | Enable/disable signature verification |
| `ackTimeoutMs` | `3001` | Ack timeout in milliseconds |
| `logLevel` | `INFO` | Logging level |

---

## Event Handler Patterns

### If using Chat SDK

#### Mention Handler

```typescript
bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  const text = message.text;
  await thread.post(`Processing your request: "${text}"`);
});
```

#### Subscribed Message Handler

```typescript
bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});
```

#### Slash Command Handler

```typescript
bot.onSlashCommand("/mycommand", async (event) => {
  const text = event.text;
  await event.thread.post(`Processing: ${text}`);

  // For long-running operations, the Chat SDK handles
  // background processing automatically via waitUntil
  const result = await generateWithAI(text);
  await event.thread.post(result);
});
```

#### Action Handler (Buttons, Menus)

```typescript
bot.onAction("button_click", async (event) => {
  await event.thread.post(`Button clicked with value: ${event.value}`);
});
```

#### Reaction Handler

```typescript
bot.onReaction("thumbsup", async (event) => {
  await event.thread.post("Thanks for the thumbs up!");
});
```

### If using Bolt for JavaScript

#### Mention Handler

```typescript
app.event("app_mention", async ({ event, client }) => {
  await client.chat.postMessage({
    channel: event.channel,
    thread_ts: event.thread_ts || event.ts,
    text: `Processing your request: "${event.text}"`,
  });
});
```

#### Message Handler

```typescript
app.message(async ({ message, client }) => {
  if ("bot_id" in message || !message.thread_ts) return;
  await client.chat.postMessage({
    channel: message.channel,
    thread_ts: message.thread_ts,
    text: `You said: ${message.text}`,
  });
});
```

#### Slash Command Handler

```typescript
app.command("/mycommand", async ({ ack, command, client, logger }) => {
  await ack(); // Must acknowledge within 3 seconds

  // Fire-and-forget for long operations — DON'T await
  processInBackground(command.response_url, command.text)
    .catch((error) => logger.error("Failed:", error));
});

async function processInBackground(responseUrl: string, text: string) {
  const result = await generateWithAI(text);
  await fetch(responseUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ response_type: "in_channel", text: result }),
  });
}
```

#### Action Handler (Buttons, Menus)

```typescript
app.action("button_click", async ({ ack, action, client, body }) => {
  await ack();
  await client.chat.postMessage({
    channel: body.channel.id,
    thread_ts: body.message.ts,
    text: `Button clicked with value: ${action.value}`,
  });
});
```

---

## Implementation Gotchas

### 1. Private Channel Access

Slash commands work in private channels even if the bot isn't a member, but 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, validate access upfront.

### 2. Graceful Degradation for Channel Context

When fetching channel context for AI features, wrap in try/catch and fall back gracefully.

### 3. Vercel Cron Endpoint Authentication

Protect cron endpoints with a `CRON_SECRET` environment variable:

#### If using Chat SDK

```typescript
// app/api/cron/my-job/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  // Run cron job logic...
  return NextResponse.json({ success: true });
}
```

#### If using Bolt for JavaScript

```typescript
// server/api/cron/my-job.get.ts
export default defineEventHandler(async (event) => {
  const authHeader = getHeader(event, "authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    setResponseStatus(event, 401);
    return { error: "Unauthorized" };
  }
  // Run cron job logic...
  return { success: true };
});
```

### 4. vercel.json Cron Configuration

Configure cron jobs in `vercel.json`:

```json
{
  "crons": [
    {
      "path": "/api/cron/my-job",
      "schedule": "0 * * * *"
    }
  ]
}
```

### 5. AWS Credentials on Vercel (Use OIDC)

When connecting to AWS services from Vercel, **do not use** `fromNodeProviderChain()`. Use Vercel's OIDC mechanism:

```typescript
import { awsCredentialsProvider } from "@vercel/functions/oidc";

const s3Client = new S3Client({
  credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN! }),
});
```

### 6. TSConfig for JSX Components (Chat SDK only)

When using Chat SDK JSX components (``, ``, etc.), your `tsconfig.json` must include:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "chat"
  }
}
```

### 7. dispatch_failed Error — Bolt only

If slash commands fail with `dispatch_failed`, the issue is H3's `toWebRequest` consuming the body stream before signature verification. Buffer the body manually. See the Bolt Events Handler section above.

### 8. operation_timeout Error — Bolt only

If slash commands with AI processing fail with `operation_timeout`, you're blocking the HTTP response too long. Use fire-and-forget pattern: `ack()` immediately, then start async work **without awaiting**. Use `command.response_url` to post results. See the Bolt Slash Command Handler example above.

---

## AI Integration

You have two options for AI/LLM integration in your Slack agent.

> **IMPORTANT:** Always verify the project uses `@ai-sdk/gateway`. If the project has `@ai-sdk/openai` which requires an API key, check `package.json` and update imports if necessa

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [vercel-labs](https://github.com/vercel-labs)
- **Source:** [vercel-labs/slack-agent-skill](https://github.com/vercel-labs/slack-agent-skill)
- **License:** Apache-2.0
- **Homepage:** https://github.com/vercel-labs/slack-agent-skill

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-vercel-labs-slack-agent-skill-slack-agent-skill
- Seller: https://agentstack.voostack.com/s/vercel-labs
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
