Use Cases (Atualizado: 03/06/2026)

Criar um bot do Slack com Claude Code: triagem, incidentes e relatórios diários

Guia com Bolt JS, Socket Mode, comandos slash, segurança, testes e checklist de produção.

Criar um bot do Slack com Claude Code: triagem, incidentes e relatórios diários

Não pare em um bot de notificações

Um Slack Bot é um app que reage a mensagens, comandos slash, botões e envios de modal dentro do Slack. Bolt for JavaScript é o framework oficial da Slack para Node.js que envia cada evento ao handler correto. Em termos simples, Bolt é a estrutura que permite dizer: “quando este evento do Slack chegar, execute esta função”.

O erro comum com Claude Code é parar em um bot que apenas envia alertas. Um bot útil transforma ruído de canal em trabalho estruturado: triagem de suporte, primeira resposta a incidentes, relatórios diários, aprovações e checagens antes de publicar. Nos fluxos de Masa, o primeiro bot de notificação ajudou por alguns dias, mas não registrava quem era o responsável, qual era a urgência nem se o item já tinha sido encerrado.

Este guia foi conferido com a documentação oficial da Slack em 3 de junho de 2026: Bolt for JavaScript, listeners de comandos no Bolt, Socket Mode, comandos slash, Events API, chat.postMessage, verificação de requests e tokens. Para contexto interno, veja também webhooks com Claude Code, desenvolvimento de APIs, gestão de secrets e automação de workflows.

Defina os casos de uso antes

Se você pedir a Claude Code “crie um Slack bot” sem um fluxo concreto, o resultado tende a ser uma demo rasa. Defina antes a entrada no Slack, os campos coletados, a resposta e o comportamento de falha.

Caso de usoEntrada no SlackO que o bot fazRisco a controlar
Triagem de suporte/triage add, modalNormaliza título, severidade, solicitante e notificação de canalUsuários colam nomes de clientes, secrets ou URLs privadas
Primeira resposta a incidenteMenção @bot, botãoRetorna checklist inicial e mantém contexto em threadO bot parece confiante demais em vez de escalar
Relatório diário/triage list, job agendadoResume itens abertos para daily ou relatórioMensagens longas ficam pouco legíveis no Slack
Checagem de artigo ou landing pageSlash CommandVerifica CTA, links internos, responsável e URL de publicaçãoURLs de rascunho e produção se misturam

A arquitetura é propositalmente pequena.

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"]

Um prompt bom para Claude Code:

Implemente um Slack Bot com Bolt for JavaScript.
O objetivo é triagem de suporte.
Inclua:
- Alternar entre Socket Mode e Request URL por variáveis de ambiente
- /triage add, /triage list, /triage modal
- Entrada de modal e tratamento de view_submission
- Botão Mark done
- Resposta de ajuda para app_mention
- Explicação de scopes, secrets e verificação de requests
- Testes unitários para triage.ts
Não use APIs fictícias. Escreva TypeScript copiável e executável.

Socket Mode ou Request URL

Socket Mode recebe eventos por uma conexão WebSocket iniciada pelo seu app, então você não precisa expor um endpoint HTTPS público no desenvolvimento local. É útil para protótipos, ferramentas internas e ambientes atrás de firewall. A documentação da Slack explica o uso de um app-level token que começa com xapp-.

Request URL recebe POSTs HTTP da Slack no seu endpoint HTTPS. Esse é o padrão comum em produção. Ao receber HTTP, verifique a assinatura com o Signing Secret. O Bolt pode fazer isso quando configurado, mas o design deve registrar que verification tokens antigos não são a base.

ModoMelhor paraConfiguração necessáriaArmadilha
Socket ModeDesenvolvimento local, PoC internoSLACK_APP_TOKEN, connections:writeSe o processo cair, não há eventos; não é ideal para Marketplace
Request URLDeploy HTTP de produçãoURL HTTPS, SLACK_SIGNING_SECRETack() lento vira timeout no Slack

Comece com Socket Mode e migre para Request URL quando o bot tocar canais de produção ou usuários externos. O código muda com SLACK_SOCKET_MODE=true.

Manifest e scopes do Slack

Mantenha o manifest no repositório para evitar divergência entre dev e produção. Os scopes são mínimos: commands recebe o slash command, chat:write publica mensagens e app_mentions:read recebe menções.

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

Não adicione channels:history ou groups:history só porque parecem úteis. Scopes de leitura de histórico só entram quando o bot realmente lê histórico e alguém revisou privacidade.

Criar o projeto local

Use Node.js 20 ou mais novo.

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"]
}

Crie .env.example. Valores reais ficam em .env ou no secret manager da hospedagem, nunca no 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- é o token do bot. xapp- é o app-level token usado pelo Socket Mode. O Signing Secret prova que requests HTTP vieram da Slack. Claude Code não precisa desses valores reais, apenas dos nomes das variáveis, comportamento esperado e regras de log.

Implementação Bolt copiável

Primeiro, isole a lógica sem dependência da Slack em 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" },
          ],
        },
      },
    ],
  };
}

Agora conecte os listeners do 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}`}`);

Adicione testes unitários sem 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.");
  });
});

Execute:

npm run test
npm run build
npm run dev

Com Socket Mode, deixe npm run dev rodando e digite /triage add Test from Slack. Com Request URL, faça deploy e configure https://example.com/slack/events em slash commands, interactivity e event subscriptions.

Armadilhas e segurança

Chame ack() antes de trabalho lento. Comandos, botões e modais devem confirmar recebimento antes de banco de dados ou APIs externas.

Trate trigger_id como curto. Abra o modal primeiro e valide os detalhes em view_submission.

Não depure permissões apenas no código. Falta de chat:write, bot fora do canal ou ausência de app_mention são problemas de configuração na Slack.

Não misture modos. Socket Mode precisa de SLACK_APP_TOKEN; Request URL precisa de HTTPS e SLACK_SIGNING_SECRET. Registre o modo no startup.

Nunca exponha secrets. Não cole xoxb-, xapp- ou Signing Secret em prompts do Claude Code, screenshots, logs, fixtures ou artigos. Se vazar, rotacione imediatamente.

Por fim, não dê julgamento demais ao bot. Em suporte e incidentes, ele deve mostrar próximos passos e regra de escalonamento, não inventar causa raiz.

Checklist de produção

  • Scopes do manifest batem com as APIs usadas.
  • /triage não conflita com outro app instalado.
  • Interactivity está ativa para modais e botões.
  • O bot foi convidado para o canal de destino.
  • SLACK_BOT_TOKEN, SLACK_APP_TOKEN e SLACK_SIGNING_SECRET estão como secrets.
  • npm run test e npm run build passam.
  • Request URL usa HTTPS e verificação de assinatura Slack.
  • Socket Mode tem monitoramento de processo e restart.
  • Logs não incluem tokens, dados pessoais sem máscara, clientes ou URLs privadas.
  • O tópico do canal explica quem recebe escalonamento quando o bot não consegue ajudar.

Separe o que Claude Code deve produzir: proposta de manifest e scopes, lógica sem Slack, listeners Bolt, testes unitários e checklist de deploy. Assim erros de configuração não se misturam com bugs de código.

ClaudeCodeLab cobre bots internos, webhooks, APIs e secrets em treinamento e consultoria. Para regras CLAUDE.md reutilizáveis, templates de revisão antes de publicar e checklists de equipe, combine este padrão com templates e produtos para apoiar operação e receita, não apenas uma demo.

Resultado ao testar

O caminho mais rápido não foi gerar um bot grande de uma vez. O fluxo mais estável foi travar manifest e scopes primeiro, escrever lógica pura como triage.ts depois, e só então conectar listeners Bolt com as configurações da Slack. Claude Code funciona melhor quando também revisa permissões, secrets, testes e checklist de produção como uma unidade de trabalho.

#Claude Code #Slack Bot #Bolt SDK #chatbot #automation
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.