[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
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.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.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.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
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

Related use cases