← Blog

How to Build a Claude-Powered Slack Summarizer

2026-10-31 · 5 min read · SubToAPI Team

Building a Claude-powered Slack summarizer means connecting three pieces: a Slack app that can read channel or thread history, a call to Claude that turns that history into a concise summary, and a way to post the result back into Slack — either on demand via a slash command or automatically on a schedule. The core loop is small: pull messages, format them into a prompt, call the Claude API, post the response.

This guide walks through that loop end to end, including the Slack app setup, the prompt structure that produces reliable summaries instead of rambling recaps, and two delivery patterns (slash command and scheduled digest) you can ship in an afternoon.

What You're Actually Building

A Slack summarizer has four moving parts:

  1. A Slack app with permission to read channel/thread history and post messages
  2. A trigger — a slash command (/summarize), a mention, or a cron job
  3. A fetch step that pulls the last N messages or a thread's full history
  4. A Claude call that condenses those messages into a readable summary, posted back to the channel

The complexity lives almost entirely in steps 1 and 3 — Slack's API and OAuth scopes. The Claude integration itself is a single HTTP call once you have the text.

Step 1: Set Up the Slack App

Create a Slack app at api.slack.com/apps and add these OAuth scopes:

Install the app to your workspace and grab the bot token (xoxb-...). Add a slash command, e.g. /summarize, pointing to your backend endpoint.

Step 2: Fetch the Messages

For a slash command triggered inside a channel, use conversations.history to pull recent messages:

async function fetchChannelMessages(channelId, limit = 50) {
  const res = await fetch(
    `https://slack.com/api/conversations.history?channel=${channelId}&limit=${limit}`,
    { headers: { Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}` } }
  );
  const data = await res.json();
  return data.messages
    .reverse()
    .map((m) => `${m.user}: ${m.text}`)
    .join("\n");
}

For thread summaries, swap in conversations.replies and pass the thread's ts. Either way, you end up with a flat block of text — that's what gets handed to Claude.

Step 3: Call Claude to Summarize

The prompt matters more than the plumbing. A generic "summarize this" prompt produces generic output. Be explicit about format and length:

async function summarizeWithClaude(transcript) {
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 500,
      system:
        "You summarize Slack conversations for busy teammates. " +
        "Output 3-6 bullet points covering decisions made, open questions, " +
        "and action items with owners if mentioned. No preamble, no closing remarks.",
      messages: [
        {
          role: "user",
          content: `Summarize this Slack conversation:\n\n${transcript}`,
        },
      ],
    }),
  });

  const data = await res.json();
  return data.content[0].text;
}

This example calls Claude through SubToAPI, which exposes the same Messages-style API as a straightforward HTTPS endpoint with an application API key — useful if you don't want to manage Anthropic console access, billing, or key rotation for a side project or internal tool. The request/response shape is documented at /docs/messages, and /docs/quickstart covers getting a key in a couple of minutes.

The system prompt is doing the real work here: it forces bullets, caps the format, and tells Claude to skip filler like "Here's a summary of the conversation." Without that instruction, Claude (like most models) tends to add conversational wrapping you'll have to strip out before posting to Slack.

Step 4: Post the Summary Back

async function postSummary(channelId, summary) {
  await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      channel: channelId,
      text: `*Summary:*\n${summary}`,
    }),
  });
}

Wire the three functions together in your slash command handler, and /summarize now returns a bulleted recap of the channel directly where it was called.

Handling Long Channels

Slack channels can run well past what fits in a single context window, especially if you're summarizing a full day or week. Two practical approaches:

If you want summaries to stream into a "thinking" indicator rather than appear all at once (useful for longer digests posted in a thread), SubToAPI supports streaming responses — see /docs/streaming. For summarizers that also need to pull structured data (e.g., extracting action items as JSON instead of prose), tool use lets Claude return a typed object instead of free text — covered in /docs/tools.

Scheduling Automatic Digests

For a daily digest instead of an on-demand command, skip the slash command trigger and run the same fetch → summarize → post pipeline on a cron schedule (node-cron, a scheduled Lambda, or a GitHub Actions workflow). Pull the last 24 hours of messages per channel at 9am, summarize, and post into a dedicated #daily-digest channel.

Getting Started with SubToAPI

If you're prototyping this as a side project or an internal team tool, you don't need to set up Anthropic billing and manage a raw API key just to run a Slack bot. /signup gives you a free trial and an application key (sub_live_...) that works with the same request format shown above. Plans start at Solo for individual projects, with Team and Scale tiers for shared usage across a workspace — details at /pricing.

Questions

Does this work for private channels, not just public ones? Yes — add the groups:history OAuth scope when you create the Slack app, and the bot must be invited to the private channel before it can read its history.

How do I keep the summarizer from hitting Claude's context window on long threads? Cap the number of messages fetched per call, or chunk long transcripts into batches, summarize each batch, and run a final pass that condenses the batch summaries into one.

Can the summarizer extract action items as structured data instead of plain text? Yes — use Claude's tool-use feature to define a schema (e.g., {task, owner, due_date}) and have Claude return structured JSON instead of bullet-point prose; see /docs/tools.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →