---
title: "Slash Commands"
description: "Create an `/echo` command that demonstrates argument parsing, input validation, and ephemeral responses. Learn the difference between public and private replies in Slack channels."
canonical_url: "https://vercel.com/academy/slack-agents/slash-commands"
md_url: "https://vercel.com/academy/slack-agents/slash-commands.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-09T09:30:36.432Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Slash Commands

# Build slash commands that respond privately

Slash commands are your bot's most discoverable feature—users type `/` and see your commands in autocomplete. But commands that always reply publicly spam channels. Users get annoyed when `/weather` broadcasts "72°F in SF" to 50 people instead of just the person who asked. Ephemeral responses (only visible to the command user) keep channels clean.

## Outcome

Build an `/echo` command with input validation and ephemeral responses.

## Fast Track

1. Add `/echo` to `manifest.json` slash\_commands
2. Create `server/listeners/commands/echo.ts` with ack-first pattern
3. Run `slack run` to reinstall app (manifest changes require reinstall)
4. Test `/echo hello` and verify only you see the response

\*\*Warning: Manifest Changes Require Reinstall\*\*

Adding a new slash command changes `manifest.json`. You must run `slack run` and approve the manifest update for the command to appear in Slack. Just restarting your server won't register the new command.

## Building on Previous Lessons

- **From [Bolt Middleware](./bolt-nitro-middleware-and-logging)**: Use `context.correlation` in logs
- **From [Ack Semantics](./acknowledgment-and-latency)**: Call `ack()` first, then `respond()`

## Ephemeral vs Public Responses

Commands can respond privately (ephemeral) or publicly (in\_channel):

```typescript
// Ephemeral (only command user sees - default for most commands)
await respond({
  text: "Only you can see this",
  response_type: "ephemeral",
});

// Public (whole channel sees)
await respond({
  text: "Everyone sees this",
  response_type: "in_channel",
});
```

**When to use ephemeral:** Personal queries, errors, usage help\
**When to use public:** Announcements, shared info, team updates

## Hands-On Exercise 3.1

Create an `/echo` command that repeats user input:

**Requirements:**

1. Add `/echo` to `manifest.json` slash\_commands
2. Create `server/listeners/commands/echo.ts` with ack-first pattern
3. Validate input with Zod—reusing the pattern from lesson 1.3 (Boot Checks)
4. Use AI to detect sentiment and append an emoji to the echo response
5. Use ephemeral responses (`response_type: "ephemeral"`)
6. Register in `server/listeners/commands/index.ts`
7. Add correlation logging (continuing lesson 2.2 pattern)

**Implementation hints:**

- Extract `text` from the `command` object for user input
- Create Zod schema for input: `z.object({ text: z.string().min(1) })`
- After validation, use `generateObject` with sentiment schema: `z.object({ sentiment: z.enum(["positive", "negative", "neutral"]) })`
- Map sentiment to emoji: positive → 😊, negative → 😞, neutral → 😐
- Respond with: `You said: ${text} ${emoji}`
- Use `respond()` for all messages after `ack()` (commands can't use `say()`)
- Add `context` parameter to access correlation fields from middleware

**Manifest update** (add to existing slash\_commands array):

```json
{
  "command": "/echo",
  "url": "https://your-app-domain.com/api/slack/events",
  "description": "Echo back your text",
  "should_escape": false
}
```

Note: The URL will be automatically updated by the tunnel script when you run `slack run`.

## Try It

1. **Test AI sentiment detection**:
   - Type `/echo I love this!`
   - Verify response: "You said: I love this! 😊" (positive sentiment)
   - Type `/echo this sucks`
   - Verify response: "You said: this sucks 😞" (negative sentiment)
   - Type `/echo the weather is nice`
   - Verify response: "You said: the weather is nice 😐" (neutral sentiment)

2. **Test empty input**:
   - Type `/echo` with no arguments
   - Should show usage instructions

3. **Check logs for correlation**:
   ```
   [INFO] bolt-app {
     event_id: '...',
     command: '/echo',
     user: 'U09D6B53WP4'
   } Processing echo command
   ```

## Commit

```bash
git add -A
git commit -m "feat(commands): add /echo command with input validation

- Create echo command handler with ack-first pattern
- Add Zod validation for empty input
- Use ephemeral responses to keep channels clean
- Include correlation logging from lesson 2.2
- Register command in manifest and handler index"
```

## Done-When

- [x] Added `/echo` to `manifest.json` slash\_commands
- [x] Created `server/listeners/commands/echo.ts` with ack-first pattern
- [x] Used Zod to validate input (show usage if empty)
- [x] Integrated AI sentiment analysis with `generateObject`
- [x] Response includes emoji based on detected sentiment
- [x] All responses are ephemeral
- [x] Registered in `server/listeners/commands/index.ts`
- [x] Included correlation logging in handler
- [x] Tested positive, negative, and neutral sentiment detection

## Step-by-Step Solution

### Step 1: Create a basic echo command

First, get the command working with just validation and echoing:

```typescript title="/slack-agent/server/listeners/commands/echo.ts"
import type {
  AllMiddlewareArgs,
  SlackCommandMiddlewareArgs,
} from "@slack/bolt";
import { z } from "zod";

const EchoInputSchema = z.object({
  text: z.string().min(1, "Command requires input"),
});

export const echoCallback = async ({
  ack,
  command,
  respond,
  logger,
  context,
}: AllMiddlewareArgs & SlackCommandMiddlewareArgs) => {
  try {
    await ack();
    
    logger.info({
      ...context.correlation,
      command: command.command,
      user: command.user_id,
    }, "Processing echo command");

    const { text, command: cmd } = command;

    const validated = EchoInputSchema.safeParse({ text: text?.trim() || "" });
    if (!validated.success) {
      await respond({
        text: `${validated.error.issues[0].message}\nUsage: ${cmd} <message>`,
        response_type: "ephemeral",
      });
      return;
    }

    // Simple echo (we'll add AI next)
    await respond({
      text: `You said: ${validated.data.text}`,
      response_type: "ephemeral",
    });

  } catch (error) {
    logger.error({
      ...context.correlation,
      error: error instanceof Error ? error.message : String(error),
    }, "Echo command failed");
    try {
      await respond({
        text: "Sorry, something went wrong with the echo command.",
        response_type: "ephemeral",
      });
    } catch (respondError) {
      logger.error({
        ...context.correlation,
        error: respondError instanceof Error ? respondError.message : String(respondError),
      }, "Failed to send error response");
    }
  }
};
```

### Step 2: Register the command

Add to `server/listeners/commands/index.ts`:

```typescript
import type { App } from "@slack/bolt";
import { sampleCommandCallback } from "./sample-command";
import { echoCallback } from "./echo"; // Add import

const register = (app: App) => {
  app.command("/sample-command", sampleCommandCallback);
  app.command("/echo", echoCallback); // Add registration
};

export default { register };
```

### Step 3: Add to manifest

Add to the `slash_commands` array in `manifest.json`:

```json
{
  "command": "/echo",
  "url": "https://your-app-domain.com/api/slack/events",
  "description": "Echo back your text with AI sentiment",
  "should_escape": false
}
```

Note: The URL will be automatically updated by the tunnel script when you run `slack run`.

**Test end-to-end:** Run `slack run` (reinstalls manifest), use `/echo hello world`, verify "You said: hello world" appears (ephemeral).

### Step 4: Add AI sentiment analysis

Now enhance with AI. Update the imports in `echo.ts`:

```typescript
import { generateObject } from "ai";  // Add this

// Add after EchoInputSchema:
const SentimentSchema = z.object({
  sentiment: z.enum(["positive", "negative", "neutral"]),
});
```

**Replace the simple respond()** with AI-enhanced version:

```typescript
// Replace this:
await respond({
  text: `You said: ${validated.data.text}`,
  response_type: "ephemeral",
});

// With this:
const analysis = await generateObject({
  model: "openai/gpt-4o-mini",
  schema: SentimentSchema,
  prompt: `Analyze the sentiment of this text: "${validated.data.text}"`,
});

const emojiMap = {
  positive: "😊",
  negative: "😞",
  neutral: "😐",
};
const emoji = emojiMap[analysis.object.sentiment];

await respond({
  text: `You said: ${validated.data.text} ${emoji}`,
  response_type: "ephemeral",
});
```

**Test AI sentiment:**

- `/echo I love this!` → 😊
- `/echo this sucks` → 😞
- `/echo the weather is nice` → 😐

## Troubleshooting

**Command not appearing in autocomplete:**

- Verify `manifest.json` includes the `/echo` command
- Run `slack run` to reinstall the app (manifest changes require reinstall)

**"Unknown command" error:**

- Command name in handler registration (`app.command("/echo", ...)`) must match manifest exactly
- Check for typos—`/echo` vs `/Echo` are different

**Timeout errors:**

- Ensure `ack()` is first line in try block
- No database queries or API calls before `ack()`

\*\*Side Quest: Build /analyze Command with Thread Context\*\*


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
