Use Cases (Diperbarui: 3/6/2026)

Membuat Slack Bot dengan Claude Code: triase, insiden, dan laporan harian

Panduan Bolt JS dengan Socket Mode, slash command, keamanan, test, dan checklist produksi.

Membuat Slack Bot dengan Claude Code: triase, insiden, dan laporan harian

Jangan berhenti di bot notifikasi

Slack Bot adalah aplikasi yang merespons pesan, slash command, tombol, dan pengiriman modal di dalam Slack. Bolt for JavaScript adalah framework resmi Slack untuk Node.js yang mengarahkan event tersebut ke handler yang tepat. Untuk pemula, Bolt bisa dianggap sebagai kerangka yang membuat aturan “saat event Slack ini datang, jalankan fungsi ini”.

Kesalahan umum saat memakai Claude Code adalah berhenti pada bot yang hanya mengirim notifikasi. Bot yang berguna mengubah percakapan kanal menjadi pekerjaan terstruktur: triase support, respons pertama insiden, laporan harian, approval, dan pemeriksaan sebelum publikasi. Dalam alur kerja Masa, bot notifikasi pertama memang membantu beberapa hari, tetapi tidak menjawab siapa pemilik tugas, seberapa urgent, dan apakah sudah selesai.

Panduan ini diverifikasi dengan dokumentasi resmi Slack pada 3 Juni 2026: Bolt for JavaScript, listener command Bolt, Socket Mode, slash command, Events API, chat.postMessage, verifikasi request, dan tokens. Untuk konteks ClaudeCodeLab, baca juga implementasi webhook, pengembangan API, manajemen secrets, dan otomasi workflow.

Tentukan use case terlebih dahulu

Jika Anda hanya meminta Claude Code “buat Slack bot”, hasilnya biasanya demo tipis. Tentukan dulu titik masuk di Slack, field yang dikumpulkan, pesan balasan, dan perilaku saat gagal.

Use caseTitik masuk SlackYang dilakukan botRisiko yang dikendalikan
Triase support/triage add, modalMenormalkan judul, severity, pemohon, dan notifikasi kanalPengguna menempel nama pelanggan, secret, atau URL privat
Respons pertama insidenMention @bot, tombolMengirim checklist awal dan menyimpan konteks di threadBot terdengar terlalu yakin dan tidak mengeskalasi
Laporan harian/triage list, job terjadwalMerangkum item terbuka untuk daily atau laporanPesan terlalu panjang dan sulit dibaca di Slack
Cek artikel atau landing pageSlash CommandMengecek CTA, internal link, pemilik, dan URL publikasiURL draft dan produksi tercampur

Arsitekturnya sengaja kecil.

flowchart LR
  A["Slack user"] --> B["/triage or @mention"]
  B --> C["Bolt listener"]
  C --> D["Triage logic"]
  D --> E["chat.postMessage"]
  D --> F["Modal and button"]

Prompt yang konkret untuk Claude Code:

Implementasikan Slack Bot dengan Bolt for JavaScript.
Tujuannya adalah triase support.
Sertakan:
- Beralih antara Socket Mode dan Request URL dengan environment variable
- /triage add, /triage list, /triage modal
- Input modal dan handling view_submission
- Tombol Mark done
- Balasan bantuan untuk app_mention
- Penjelasan scopes, secrets, dan verifikasi request
- Unit test untuk triage.ts
Jangan gunakan pseudo API. Tulis TypeScript yang bisa dicopy dan dijalankan.

Socket Mode atau Request URL

Socket Mode menerima event melalui koneksi WebSocket yang dibuka oleh aplikasi Anda, sehingga pengembangan lokal tidak membutuhkan endpoint HTTPS publik. Ini cocok untuk prototype, tool internal, dan lingkungan di balik firewall. Dokumentasi Slack menjelaskan penggunaan app-level token yang diawali xapp-.

Request URL menerima HTTP POST dari Slack ke endpoint HTTPS Anda. Ini pola umum untuk produksi. Saat memakai HTTP, verifikasi signature dengan Signing Secret. Bolt bisa melakukan verifikasi jika dikonfigurasi, tetapi catatan desain tetap harus mengatakan bahwa verification token lama tidak menjadi dasar keamanan.

ModeCocok untukKonfigurasi yang dibutuhkanJebakan
Socket ModeDev lokal, PoC internalSLACK_APP_TOKEN, connections:writeJika proses mati, event tidak masuk; kurang cocok untuk Marketplace
Request URLDeploy HTTP produksiURL HTTPS, SLACK_SIGNING_SECRETack() yang lambat menjadi timeout di Slack

Mulai dengan Socket Mode, lalu pindah ke Request URL ketika bot menyentuh kanal produksi atau pengguna eksternal. Kode di bawah berpindah dengan SLACK_SOCKET_MODE=true.

Manifest dan scopes Slack

Simpan manifest di repository agar konfigurasi dev dan produksi tidak berbeda diam-diam. Scope di sini minimal: commands untuk menerima slash command, chat:write untuk mengirim pesan, dan app_mentions:read untuk menerima mention.

display_information:
  name: Claude Triage Bot
  description: Collect triage requests from Slack
  background_color: "#2E2A24"
features:
  bot_user:
    display_name: Claude Triage
    always_online: false
  slash_commands:
    - command: /triage
      description: Add or list triage items
      usage_hint: "add Fix login | list | modal"
      should_escape: true
oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - chat:write
      - commands
settings:
  event_subscriptions:
    bot_events:
      - app_mention
  interactivity:
    is_enabled: true
  socket_mode_enabled: true
  org_deploy_enabled: false
  token_rotation_enabled: false

Jangan menambahkan channels:history atau groups:history hanya karena terlihat berguna. Scope baca histori baru masuk jika bot benar-benar membaca histori dan dampak privasinya sudah direview.

Membuat proyek lokal

Gunakan Node.js 20 atau lebih baru.

mkdir claude-slack-triage-bot
cd claude-slack-triage-bot
npm init -y
npm install @slack/bolt @slack/types dotenv
npm install -D typescript tsx vitest @types/node
npm pkg set type=module
npm pkg set scripts.dev="tsx watch src/app.ts"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/app.js"
npm pkg set scripts.test="vitest run"
mkdir src tests
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

Buat .env.example. Nilai asli disimpan di .env atau secret manager hosting, bukan di Git.

SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_SOCKET_MODE=true
SLACK_APP_TOKEN=xapp-your-app-level-token
TRIAGE_CHANNEL_ID=C0123456789
PORT=3000

xoxb- adalah bot token. xapp- adalah app-level token untuk Socket Mode. Signing Secret membuktikan request HTTP berasal dari Slack. Claude Code tidak membutuhkan nilai asli, hanya nama variabel, perilaku yang diharapkan, dan aturan logging.

Implementasi Bolt yang bisa dicopy

Pisahkan dulu logika yang tidak bergantung pada Slack ke src/triage.ts.

// src/triage.ts
import type { KnownBlock, View } from "@slack/types";

export type Severity = "low" | "normal" | "high";

export interface Ticket {
  id: string;
  channelId: string;
  title: string;
  createdBy: string;
  severity: Severity;
  status: "open" | "done";
  createdAt: string;
}

const tickets = new Map<string, Ticket>();

export function resetForTest() {
  tickets.clear();
}

export function parseTriageText(text: string) {
  const [actionRaw, ...rest] = text.trim().split(/\s+/);
  return { action: actionRaw || "help", title: rest.join(" ").trim() };
}

export function addTicket(input: {
  channelId: string;
  title: string;
  createdBy: string;
  severity?: Severity;
}) {
  const ticket: Ticket = {
    id: `triage_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    channelId: input.channelId,
    title: input.title,
    createdBy: input.createdBy,
    severity: input.severity ?? "normal",
    status: "open",
    createdAt: new Date().toISOString(),
  };
  tickets.set(ticket.id, ticket);
  return ticket;
}

export function completeTicket(id: string) {
  const ticket = tickets.get(id);
  if (!ticket) return undefined;
  const updated: Ticket = { ...ticket, status: "done" };
  tickets.set(id, updated);
  return updated;
}

export function formatTicketList(channelId: string) {
  const open = [...tickets.values()].filter((ticket) => {
    return ticket.channelId === channelId && ticket.status === "open";
  });

  if (open.length === 0) return "No open triage items.";

  return open
    .map((ticket, index) => {
      return `${index + 1}. [${ticket.severity}] ${ticket.title} by <@${ticket.createdBy}>`;
    })
    .join("\n");
}

export function ticketBlocks(ticket: Ticket): KnownBlock[] {
  return [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*${ticket.title}*\nSeverity: ${ticket.severity}\nOwner: <@${ticket.createdBy}>`,
      },
    },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "Mark done" },
          action_id: "triage_done",
          value: ticket.id,
        },
      ],
    },
  ];
}

export function modalView(): View {
  return {
    type: "modal",
    callback_id: "triage_modal_submit",
    title: { type: "plain_text", text: "New triage" },
    submit: { type: "plain_text", text: "Create" },
    close: { type: "plain_text", text: "Cancel" },
    blocks: [
      {
        type: "input",
        block_id: "title_block",
        label: { type: "plain_text", text: "What needs attention?" },
        element: {
          type: "plain_text_input",
          action_id: "title_input",
          min_length: 3,
          max_length: 120,
        },
      },
      {
        type: "input",
        block_id: "severity_block",
        label: { type: "plain_text", text: "Severity" },
        element: {
          type: "static_select",
          action_id: "severity_input",
          initial_option: {
            text: { type: "plain_text", text: "Normal" },
            value: "normal",
          },
          options: [
            { text: { type: "plain_text", text: "High" }, value: "high" },
            { text: { type: "plain_text", text: "Normal" }, value: "normal" },
            { text: { type: "plain_text", text: "Low" }, value: "low" },
          ],
        },
      },
    ],
  };
}

Lalu hubungkan listener Bolt.

// src/app.ts
import "dotenv/config";
import { App, LogLevel } from "@slack/bolt";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  modalView,
  parseTriageText,
  ticketBlocks,
  type Severity,
} from "./triage.js";

const socketMode = process.env.SLACK_SOCKET_MODE === "true";
const required = ["SLACK_BOT_TOKEN", socketMode ? "SLACK_APP_TOKEN" : "SLACK_SIGNING_SECRET"];

for (const key of required) {
  if (!process.env[key]) throw new Error(`Missing environment variable: ${key}`);
}

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode,
  appToken: process.env.SLACK_APP_TOKEN,
  logLevel: LogLevel.INFO,
});

app.command("/triage", async ({ ack, command, respond, client }) => {
  await ack();
  const parsed = parseTriageText(command.text);

  if (parsed.action === "add") {
    if (!parsed.title) {
      await respond("Usage: `/triage add Fix login redirect`");
      return;
    }

    const ticket = addTicket({
      channelId: command.channel_id,
      title: parsed.title,
      createdBy: command.user_id,
      severity: "normal",
    });

    await respond({
      response_type: "in_channel",
      text: `Triage item added: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
    return;
  }

  if (parsed.action === "list") {
    await respond({ response_type: "ephemeral", text: formatTicketList(command.channel_id) });
    return;
  }

  if (parsed.action === "modal") {
    await client.views.open({ trigger_id: command.trigger_id, view: modalView() });
    return;
  }

  await respond("Usage: `/triage add ...`, `/triage list`, or `/triage modal`");
});

app.view("triage_modal_submit", async ({ ack, view, body, client }) => {
  const titleState = view.state.values.title_block.title_input;
  const severityState = view.state.values.severity_block.severity_input;
  const title = titleState.type === "plain_text_input" ? titleState.value?.trim() : "";
  const severity =
    severityState.type === "static_select"
      ? severityState.selected_option?.value ?? "normal"
      : "normal";

  if (!title) {
    await ack({ response_action: "errors", errors: { title_block: "Please enter a title." } });
    return;
  }

  await ack();

  const channelId = process.env.TRIAGE_CHANNEL_ID ?? "modal-only";
  const ticket = addTicket({
    channelId,
    title,
    createdBy: body.user.id,
    severity: severity as Severity,
  });

  if (process.env.TRIAGE_CHANNEL_ID) {
    await client.chat.postMessage({
      channel: process.env.TRIAGE_CHANNEL_ID,
      text: `New triage item: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
  }
});

app.action("triage_done", async ({ ack, action, respond }) => {
  await ack();
  const value = action.type === "button" ? action.value : undefined;
  if (!value) return;

  const ticket = completeTicket(value);
  await respond(ticket ? `Closed: ${ticket.title}` : "Ticket not found.");
});

app.event("app_mention", async ({ event, say }) => {
  await say({
    thread_ts: event.ts,
    text: "Use `/triage add ...`, `/triage list`, or `/triage modal`.",
  });
});

const port = Number(process.env.PORT ?? 3000);
if (socketMode) {
  await app.start();
} else {
  await app.start(port);
}

app.logger.info(`Slack bot started in ${socketMode ? "Socket Mode" : `HTTP mode on ${port}`}`);

Tambahkan unit test tanpa Slack.

// tests/triage.test.ts
import { beforeEach, describe, expect, it } from "vitest";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  parseTriageText,
  resetForTest,
} from "../src/triage";

describe("triage helpers", () => {
  beforeEach(() => resetForTest());

  it("parses slash command text", () => {
    expect(parseTriageText("add Fix login")).toEqual({
      action: "add",
      title: "Fix login",
    });
  });

  it("lists only open tickets", () => {
    const ticket = addTicket({
      channelId: "C123",
      title: "Review pricing CTA",
      createdBy: "U123",
      severity: "high",
    });

    expect(formatTicketList("C123")).toContain("[high] Review pricing CTA");
    completeTicket(ticket.id);
    expect(formatTicketList("C123")).toBe("No open triage items.");
  });
});

Jalankan:

npm run test
npm run build
npm run dev

Dengan Socket Mode, biarkan npm run dev berjalan dan ketik /triage add Test from Slack. Dengan Request URL, deploy app lalu set https://example.com/slack/events untuk slash commands, interactivity, dan event subscriptions.

Jebakan dan keamanan

Panggil ack() sebelum pekerjaan lambat. Command, tombol, dan modal harus mengonfirmasi penerimaan sebelum update database atau call API eksternal.

Anggap trigger_id berumur pendek. Buka modal dulu, lalu validasi detail di view_submission.

Jangan debug masalah izin hanya dari kode. chat:write yang kurang, bot belum diundang, atau subscription app_mention yang belum ada harus diperbaiki di Slack settings.

Jangan mencampur mode. Socket Mode butuh SLACK_APP_TOKEN; Request URL butuh HTTPS dan SLACK_SIGNING_SECRET. Tampilkan mode yang dipakai saat startup.

Jangan pernah membocorkan secret. Jangan tempel xoxb-, xapp-, atau Signing Secret ke prompt Claude Code, screenshot, log, fixture, atau artikel. Jika bocor, rotasi segera.

Terakhir, jangan memberi bot terlalu banyak wewenang mengambil kesimpulan. Untuk support dan insiden, bot sebaiknya memberi langkah cek berikutnya dan aturan eskalasi, bukan mengarang root cause.

Checklist produksi

  • Scope manifest cocok dengan API yang dipakai.
  • /triage tidak bentrok dengan app lain.
  • Interactivity aktif untuk modal dan tombol.
  • Bot sudah diundang ke kanal tujuan.
  • SLACK_BOT_TOKEN, SLACK_APP_TOKEN, dan SLACK_SIGNING_SECRET tersimpan sebagai secrets.
  • npm run test dan npm run build lulus.
  • Request URL memakai HTTPS dan verifikasi signature Slack.
  • Socket Mode memiliki process monitoring dan restart.
  • Log tidak berisi token, data personal tanpa masking, nama pelanggan, atau URL privat.
  • Topik kanal menjelaskan siapa yang menerima eskalasi saat bot tidak bisa membantu.

Pisahkan pekerjaan Claude Code: usulan manifest dan scopes, logika tanpa Slack, listener Bolt, unit test, dan checklist deploy. Dengan begitu, kesalahan konfigurasi Slack tidak bercampur dengan bug kode.

ClaudeCodeLab membahas bot internal, webhook, API, dan secrets dalam training dan konsultasi. Jika Anda butuh aturan CLAUDE.md, template review sebelum publikasi, dan checklist tim, gabungkan pola ini dengan template dan produk agar bot membantu operasi dan monetisasi, bukan hanya demo.

Hasil saat diuji

Jalur tercepat bukan membuat bot Slack besar dalam sekali generate. Cara yang paling stabil adalah mengunci manifest dan scopes terlebih dahulu, menulis logika murni seperti triage.ts, lalu menghubungkan listener Bolt dengan konfigurasi admin Slack. Claude Code paling berguna ketika kode, izin, secrets, test, dan checklist produksi diperlakukan sebagai satu unit kerja yang bisa direview.

#Claude Code #Slack Bot #Bolt SDK #chatbot #automation
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.