[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Slack Bot for Team Automation

Build a Slack Bot for Team Automation

Build a Slack bot that automates team workflows — standup collection, incident alerts, deployment notifications, on-call rotation, and AI-powered answers from company knowledge base.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Rosa leads engineering ops at a 45-person company. Daily standups take 30 minutes in video calls — half the team zones out. Deployment notifications go to a channel nobody reads. Incident alerts rely on someone remembering to post in Slack. The on-call schedule lives in a Google Sheet that's always outdated. New hires ask the same 10 questions weekly ("Where's the staging URL?", "How do I get VPN access?"). They need a Slack bot that handles standups asynchronously, alerts on incidents, tracks deployments, manages on-call, and answers common questions from the knowledge base.

Step 1: Build the Slack Bot Framework

typescript
// src/slack/bot.ts — Slack bot with event handling and interactive components
import { Hono } from "hono";
import { createHmac } from "node:crypto";
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN!;
const SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET!;

const app = new Hono();

// Verify Slack request signatures
async function verifySlackSignature(c: any): Promise<boolean> {
  const timestamp = c.req.header("X-Slack-Request-Timestamp");
  const signature = c.req.header("X-Slack-Signature");
  const body = await c.req.text();

  const sigBasestring = `v0:${timestamp}:${body}`;
  const mySignature = "v0=" + createHmac("sha256", SIGNING_SECRET).update(sigBasestring).digest("hex");
  return mySignature === signature;
}

// Handle Slack events
app.post("/slack/events", async (c) => {
  const body = await c.req.json();

  // URL verification challenge
  if (body.type === "url_verification") {
    return c.json({ challenge: body.challenge });
  }

  if (body.event) {
    await handleEvent(body.event);
  }

  return c.json({ ok: true });
});

// Handle slash commands
app.post("/slack/commands", async (c) => {
  const form = await c.req.parseBody();
  const command = form.command as string;
  const text = (form.text as string || "").trim();
  const userId = form.user_id as string;
  const channelId = form.channel_id as string;

  switch (command) {
    case "/standup":
      return c.json(await handleStandup(userId, text));

    case "/oncall":
      return c.json(await handleOnCall(text));

    case "/deploy":
      return c.json(await handleDeployNotification(userId, text, channelId));

    case "/ask":
      // AI-powered knowledge base search
      return c.json(await handleAskBot(text, userId));

    default:
      return c.json({ text: `Unknown command: ${command}` });
  }
});

// Handle interactive components (buttons, modals)
app.post("/slack/interactions", async (c) => {
  const payload = JSON.parse((await c.req.parseBody()).payload as string);

  if (payload.type === "block_actions") {
    for (const action of payload.actions) {
      if (action.action_id === "acknowledge_incident") {
        await acknowledgeIncident(action.value, payload.user.id);
      }
      if (action.action_id === "resolve_incident") {
        await resolveIncident(action.value, payload.user.id);
      }
    }
  }

  return c.json({ ok: true });
});

// Event handlers
async function handleEvent(event: any): Promise<void> {
  switch (event.type) {
    case "app_mention":
      // Bot was @mentioned — respond with AI
      const answer = await searchKnowledgeBase(event.text);
      await postMessage(event.channel, answer);
      break;

    case "team_join":
      // New team member — send welcome DM
      await sendWelcomeDM(event.user.id);
      break;
  }
}

// Standup collection
async function handleStandup(userId: string, text: string): Promise<any> {
  if (!text) {
    return {
      response_type: "ephemeral",
      text: "📋 *Daily Standup*\nUsage: `/standup yesterday: ..., today: ..., blockers: ...`",
    };
  }

  // Parse standup
  const sections = text.split(",").reduce((acc: any, part: string) => {
    const [key, ...value] = part.split(":");
    acc[key.trim().toLowerCase()] = value.join(":").trim();
    return acc;
  }, {});

  await pool.query(
    `INSERT INTO standups (user_id, yesterday, today, blockers, submitted_at)
     VALUES ($1, $2, $3, $4, NOW())`,
    [userId, sections.yesterday || "", sections.today || "", sections.blockers || ""]
  );

  // Post to standup channel
  await postMessage(process.env.STANDUP_CHANNEL!, {
    blocks: [
      { type: "section", text: { type: "mrkdwn", text: `*<@${userId}>*'s standup:` } },
      { type: "section", fields: [
        { type: "mrkdwn", text: `*Yesterday:*\n${sections.yesterday || "_nothing_"}` },
        { type: "mrkdwn", text: `*Today:*\n${sections.today || "_nothing_"}` },
      ]},
      ...(sections.blockers ? [{ type: "section", text: { type: "mrkdwn", text: `🚧 *Blockers:* ${sections.blockers}` } }] : []),
    ],
  });

  return { response_type: "ephemeral", text: "✅ Standup submitted!" };
}

// On-call management
async function handleOnCall(text: string): Promise<any> {
  if (text === "who") {
    const { rows: [current] } = await pool.query(
      "SELECT user_id, started_at, ends_at FROM oncall_schedule WHERE NOW() BETWEEN started_at AND ends_at LIMIT 1"
    );
    if (!current) return { text: "Nobody is on-call right now 😱" };
    return { text: `🔔 On-call: <@${current.user_id}> (until ${new Date(current.ends_at).toLocaleDateString()})` };
  }

  // Show schedule
  const { rows } = await pool.query(
    "SELECT user_id, started_at, ends_at FROM oncall_schedule WHERE ends_at > NOW() ORDER BY started_at LIMIT 8"
  );

  const schedule = rows.map((r) =>
    `• <@${r.user_id}>: ${new Date(r.started_at).toLocaleDateString()}${new Date(r.ends_at).toLocaleDateString()}`
  ).join("\n");

  return { text: `📅 *On-Call Schedule*\n${schedule}` };
}

// Incident alerts with action buttons
export async function sendIncidentAlert(
  channelId: string,
  title: string,
  severity: string,
  service: string,
  incidentId: string
): Promise<void> {
  const severityEmoji = severity === "critical" ? "🔴" : severity === "high" ? "🟠" : "🟡";

  await postMessage(channelId, {
    blocks: [
      { type: "header", text: { type: "plain_text", text: `${severityEmoji} Incident: ${title}` } },
      { type: "section", fields: [
        { type: "mrkdwn", text: `*Severity:* ${severity}` },
        { type: "mrkdwn", text: `*Service:* ${service}` },
      ]},
      { type: "actions", elements: [
        { type: "button", text: { type: "plain_text", text: "👋 Acknowledge" }, action_id: "acknowledge_incident", value: incidentId, style: "primary" },
        { type: "button", text: { type: "plain_text", text: "✅ Resolve" }, action_id: "resolve_incident", value: incidentId, style: "danger" },
      ]},
    ],
  });
}

// AI knowledge base search
async function handleAskBot(question: string, userId: string): Promise<any> {
  const answer = await searchKnowledgeBase(question);
  return {
    response_type: "ephemeral",
    text: `💡 ${answer}\n\n_Didn't find what you need? Ask in #help_`,
  };
}

async function searchKnowledgeBase(query: string): Promise<string> {
  // Search FAQ/docs database
  const { rows } = await pool.query(
    `SELECT question, answer FROM knowledge_base
     WHERE to_tsvector('english', question || ' ' || answer) @@ plainto_tsquery('english', $1)
     ORDER BY ts_rank(to_tsvector('english', question || ' ' || answer), plainto_tsquery('english', $1)) DESC
     LIMIT 1`,
    [query]
  );

  if (rows.length > 0) return rows[0].answer;
  return "I couldn't find an answer to that. Try asking in #help or checking the wiki.";
}

async function postMessage(channel: string, content: any): Promise<void> {
  const body = typeof content === "string" ? { channel, text: content } : { channel, ...content };
  await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: { Authorization: `Bearer ${SLACK_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

async function sendWelcomeDM(userId: string): Promise<void> {
  // Open DM channel
  const res = await fetch("https://slack.com/api/conversations.open", {
    method: "POST",
    headers: { Authorization: `Bearer ${SLACK_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ users: userId }),
  });
  const { channel } = await res.json();

  await postMessage(channel.id,
    `👋 Welcome to the team! Here's what you need to know:\n\n` +
    `• Staging URL: https://staging.example.com\n` +
    `• VPN setup: https://wiki.example.com/vpn\n` +
    `• Use \`/ask <question>\` to search our knowledge base\n` +
    `• Submit standups with \`/standup yesterday: ..., today: ...\`\n\n` +
    `Questions? Ask me or post in #help!`
  );
}

async function acknowledgeIncident(incidentId: string, userId: string) {
  await pool.query("UPDATE incidents SET acknowledged_by = $2, acknowledged_at = NOW() WHERE id = $1", [incidentId, userId]);
}

async function resolveIncident(incidentId: string, userId: string) {
  await pool.query("UPDATE incidents SET status = 'resolved', resolved_by = $2, resolved_at = NOW() WHERE id = $1", [incidentId, userId]);
}

export default app;

Results

  • Standup meetings eliminated — async standups via /standup take 2 minutes to write; the team reads them at their own pace; 30-minute meetings → 0
  • Incident response time: 15 min → 2 min — bot posts alert with Acknowledge/Resolve buttons; on-call clicks Acknowledge from their phone immediately
  • New hire questions answered instantly/ask how do I get VPN access? returns the answer from the knowledge base; senior engineers aren't interrupted
  • On-call schedule always current/oncall who shows the current on-call; no more checking an outdated Google Sheet
  • Deployment visibility — every deploy posts to #deployments with version, deployer, and changelog; rollbacks are faster because the team knows what changed