Slack

Escopo: JavaScript/Node.js. Guia baseado em @slack/bolt v4.7.1 com Node.js 22. Conceitos fundamentais (webhook, polling, command routing, rate limit): shared/platform/bots.md. Primitivas Slack (tokens, Socket Mode, Block Kit, scopes): shared/platform/bots-advanced.md.

O Slack impõe um prazo de resposta ao seu bot, e esse é o fato que organiza este guia. Toda interação que ele envia, seja um comando digitado, um clique de botão ou uma menção, precisa de um ack() (de acknowledge, o aviso de "recebi") em até três segundos, senão o usuário vê um erro na tela mesmo que o trabalho esteja em andamento. O Bolt for JavaScript é o framework oficial da Slack para lidar com isso: você cria um objeto App e registra nele os listeners de comando, evento e botão, cada um começando pelo ack() e só depois indo trabalhar.

Conceitos fundamentais

ConceitoO que é
App (aplicativo Bolt)Instância principal do bot; registra listeners e gerencia a conexão com o Slack
ack() (reconhecer recebimento)Função de reconhecimento obrigatória; confirma ao Slack que o evento foi recebido em até 3 segundos
say() (dizer no canal)Envia uma mensagem ao canal onde o evento ocorreu; aceita string ou objeto com blocks
respond() (responder via URL)Responde via response_url; funciona fora do canal original e aceita thread_ts para respostas em thread
Block Kit (kit de blocos de UI)Sistema de UI do Slack; mensagens compostas por blocos (section, actions, image) com elementos interativos
Socket Mode (modo socket)Conexão via WebSocket usando um App-Level Token; dispensa URL pública; indicado para desenvolvimento e bots internos
Bot Token (token do bot)Credencial xoxb-... emitida pela Slack para chamadas à API; obtida em OAuth & Permissions
Signing Secret (segredo de assinatura)Chave usada para validar que as requisições HTTP vêm do Slack; obtida em Basic Information
App-Level Token (token de nível de aplicativo)Credencial xapp-... usada exclusivamente no Socket Mode; requer scope connections:write

Instalação

npm install @slack/bolt

Criar o app e subir com as credenciais certas

Duas credenciais aparecem já na criação do App. O Bot Token (xoxb-...) autoriza o bot a chamar a API do Slack. O Signing Secret é o que permite ao Bolt provar que uma requisição veio mesmo do Slack, e não de alguém que descobriu sua URL. Nenhuma das duas entra no código: leia de variável de ambiente. E use await no app.start(), porque sem ele uma falha de inicialização vira uma promise rejeitada que ninguém observa, e o processo segue como se estivesse no ar.

❌ Ruim: credenciais hardcoded; sem await no start
import { App } from '@slack/bolt';

const app = new App({
  token: 'xoxb-1234-...',
  signingSecret: 'abc123',
});

app.start(3000);
✅ Bom: credenciais via env; await no start; port via env
import { App } from '@slack/bolt';

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});

await app.start(process.env.PORT ?? 3000);

Comandos digitados com barra

O ack() é a primeira linha do handler, antes de qualquer await de trabalho. A ordem importa: se você buscar o pedido primeiro e reconhecer depois, a janela de três segundos pode fechar durante a consulta e o Slack já terá mostrado o erro. Reconheça, depois trabalhe, depois responda com say().

❌ Ruim: sem ack(); destructuring no parâmetro; lógica de negócio no handler; format inline no say()
app.command('/order', async ({ ack, say, command }) => {
  const orderId = command.text.trim();
  const order = await db.findOrder(orderId);
  await say(`Pedido #${order.id}: ${order.status}, Cliente: ${order.customerName}`);
});
✅ Bom: ack() primeiro; destructuring no corpo; orquestrador + helpers abaixo
app.command('/order', async (commandPayload) => {
  const { ack, say, command } = commandPayload;
  await ack();
  const orderId = command.text.trim();

  if (!orderId) {
    await say('Informe o ID do pedido. Exemplo: /order 12345');
    return;
  }

  const order = await fetchOrder(orderId);
  const reply = buildOrderReply(order);
  await say(reply);
});

async function fetchOrder(orderId) {
  const order = await orderRepository.findById(orderId);
  return order;
}

function buildOrderReply(order) {
  const reply = `Pedido #${order.id}\nStatus: ${order.status}\nCliente: ${order.customerName}`;
  return reply;
}

Reagir ao que acontece no workspace

Além dos comandos, o Slack empurra eventos: alguém mencionou o bot, alguém escreveu no canal. Você os captura com app.event(). Em listener de mensagem, o guard de event.bot_id não é opcional: as mensagens que o próprio bot envia também chegam como evento, então sem o guard ele responde a si mesmo e o loop não para.

❌ Ruim: destructuring no parâmetro; sem guard para bot messages; format inline no say()
app.event('app_mention', async ({ event, say }) => {
  await say(`Olá, <@${event.user}>! Como posso ajudar?`);
});

app.event('message', async ({ event, say }) => {
  await say(`Recebi: ${event.text}`);
});
✅ Bom: destructuring no corpo; guard para bot messages; compute extraído antes do say()
app.event('app_mention', async (eventPayload) => {
  const { event, say } = eventPayload;
  const mentionReply = buildMentionReply(event.user);
  await say(mentionReply);
});

app.event('message', async (eventPayload) => {
  const { event, say } = eventPayload;
  if (event.bot_id) return;

  const echoReply = buildEchoReply(event.text);
  await say(echoReply);
});

function buildMentionReply(userId) {
  const reply = `Olá, <@${userId}>! Use /order <id> para consultar um pedido.`;
  return reply;
}

function buildEchoReply(messageText) {
  const reply = `Recebi: ${messageText}`;
  return reply;
}

Botões e blocos interativos

O Block Kit é o sistema de interface do Slack: em vez de texto puro, a mensagem vira uma lista de blocos (um parágrafo, uma linha de botões, uma imagem). Cada botão carrega um action_id, e é por ele que o Slack encontra o listener registrado em app.action() quando alguém clica. O clique é uma interação como qualquer outra, então o ack() vale aqui também: enquanto ele não chega, o botão fica girando o indicador de carregamento.

Duas coisas saem do meio do código na versão boa: os blocos viram uma constante nomeada, e o action_id vira uma entrada de ACTIONS. Assim o nome da ação é escrito uma vez e usado nos dois lugares, em vez de repetido como string solta no botão e no listener, onde um erro de digitação não avisa ninguém.

❌ Ruim: blocks montados inline; action_id como string solta; sem ack() na ação
app.command('/menu', async ({ ack, say }) => {
  await ack();
  await say({
    blocks: [
      { type: 'section', text: { type: 'mrkdwn', text: 'Escolha uma opção:' } },
      {
        type: 'actions',
        elements: [
          { type: 'button', text: { type: 'plain_text', text: 'Ver pedidos' }, action_id: 'view_orders' },
        ],
      },
    ],
  });
});

app.action('view_orders', async ({ say }) => {
  await say('Mostrando seus pedidos...');
});
✅ Bom: blocks extraídos; action_id como constante; ack() antes do processamento
const ACTIONS = {
  VIEW_ORDERS: 'view_orders',
};

const MENU_BLOCKS = [
  {
    type: 'section',
    text: { type: 'mrkdwn', text: 'Escolha uma opção:' },
  },
  {
    type: 'actions',
    elements: [
      {
        type: 'button',
        text: { type: 'plain_text', text: 'Ver pedidos' },
        action_id: ACTIONS.VIEW_ORDERS,
      },
    ],
  },
];

app.command('/menu', async (commandPayload) => {
  const { ack, say } = commandPayload;
  await ack();

  const menuPayload = { blocks: MENU_BLOCKS };
  await say(menuPayload);
});

app.action(ACTIONS.VIEW_ORDERS, async (actionPayload) => {
  const { ack, say } = actionPayload;
  await ack();

  const orders = await fetchUserOrders();
  const reply = buildOrdersReply(orders);
  await say(reply);
});

async function fetchUserOrders() {
  const orders = await orderRepository.findRecent();
  return orders;
}

function buildOrdersReply(orders) {
  const lines = orders.map((order) => `#${order.id}: ${order.status}`);
  const reply = lines.join('\n');
  return reply;
}

Conexão de saída em vez de webhook

O modo padrão exige que o Slack alcance uma URL pública sua, o que é um problema no notebook do desenvolvedor e um risco a mais em bot interno. O Socket Mode inverte a direção: o bot abre uma conexão WebSocket de saída e recebe os eventos por ela. Nada precisa entrar pela sua rede, e não há porta para escutar. O preço é uma credencial extra, o App-Level Token (xapp-...), que precisa do scope connections:write.

❌ Ruim: tokens hardcoded; sem await no start
import { App } from '@slack/bolt';

const app = new App({
  token: 'xoxb-...',
  socketMode: true,
  appToken: 'xapp-...',
});

app.start();
✅ Bom: tokens via env; await no start; sem port (Socket Mode não usa porta)
import { App } from '@slack/bolt';

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
});

await app.start();

Repare que o app.start() não recebe porta aqui. Quem abriu a conexão foi o bot, então não há servidor esperando ninguém.

Veja também

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.