Build a Claude-Powered Meeting Summarizer Slack App
How to Build a Claude-Powered Meeting Summarizer Slack App
A meeting summarizer Slack app takes a raw transcript (from Zoom, Google Meet, Otter, or a manual paste) and turns it into a structured summary — decisions made, action items, owners, deadlines — posted directly into a Slack channel. The core pieces are a Slack app with a slash command or workflow trigger, a backend endpoint that receives the transcript, a call to Claude to do the summarization, and a message posted back to Slack using the Block Kit format.
This guide walks through all four pieces, with working code, and points out the decisions that actually matter: how you structure the prompt, how you handle long transcripts, and how you avoid blocking Slack's 3-second response window.
Architecture Overview
The flow looks like this:
- User triggers the app — via
/summarizeslash command, a Slack workflow step, or a bot that watches for a pasted transcript. - Slack sends an HTTP POST to your backend.
- Your backend acknowledges immediately (Slack requires a response within 3 seconds).
- In the background, your backend sends the transcript to Claude with a summarization prompt.
- The result is formatted as Slack Block Kit and posted back via
chat.postMessageor a response URL.
This is a classic "ack fast, respond async" pattern that every Slack app with LLM calls needs, since summarization of a 45-minute meeting can easily take 5-15 seconds.
Step 1: Set Up the Slack App
Create a Slack app at api.slack.com/apps with:
- A slash command, e.g.
/summarize - Bot token scopes:
chat:write,commands - Request URL pointing to your backend's
/slack/summarizeendpoint
Slack will POST form-encoded data including response_url, which you'll use to send the final message once summarization finishes.
Step 2: Acknowledge Immediately
app.post("/slack/summarize", async (req, res) => {
// Respond within 3 seconds so Slack doesn't retry the request
res.status(200).send("Summarizing meeting notes, one moment...");
const { text, response_url, channel_id } = req.body;
await summarizeAndPost(text, response_url, channel_id);
});
Everything after res.send runs asynchronously, so the user sees instant feedback while Claude does the work.
Step 3: Call Claude for Summarization
A good summarization prompt for meetings is explicit about output structure — don't let the model freestyle, or you'll get inconsistent formatting across meetings. Ask for a fixed schema: summary, decisions, action items with owners, and open questions.
async function summarizeTranscript(transcript) {
const response = 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: 1024,
messages: [
{
role: "user",
content: `Summarize this meeting transcript into four sections:
1. **Summary** (2-3 sentences)
2. **Decisions made**
3. **Action items** (format: - [owner] task — due date if mentioned)
4. **Open questions**
Keep it concise. If a section has nothing relevant, write "None".
Transcript:
${transcript}`
}
]
})
});
const data = await response.json();
return data.content[0].text;
}
Using SubToAPI here gives you a single sub_live_... key for this app instead of juggling raw provider credentials, plus usage metadata per request so you can track exactly how much each meeting summary costs — useful if you're running this across multiple teams. See /docs/quickstart to get a key and make your first call, and /docs/messages for the full request/response shape.
Step 4: Handle Long Transcripts
A one-hour meeting transcript can run 8,000-15,000 words. Two practical approaches:
Chunk and merge. Split the transcript into ~3,000-word chunks, summarize each with a lighter prompt ("extract key points and action items only"), then feed those partial summaries into a final Claude call that produces the structured output.
async function summarizeLongTranscript(transcript) {
const chunks = splitIntoChunks(transcript, 3000); // words
const partials = [];
for (const chunk of chunks) {
const partial = await summarizeTranscript(chunk);
partials.push(partial);
}
return summarizeTranscript(partials.join("\n\n---\n\n"));
}
Rely on context window. Claude's context window often handles a full transcript without chunking — try the single-call approach first, and only add chunking if you see truncated or degraded output on your longest meetings.
Step 5: Post the Summary Back to Slack
Use response_url with Block Kit so the message is readable, not a wall of markdown.
async function postToSlack(responseUrl, summary) {
await fetch(responseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
response_type: "in_channel",
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: `*Meeting Summary*\n${summary}` }
}
]
})
});
}
Step 6: Stream for Faster Perceived Response (Optional)
For longer transcripts, you can stream the Claude response and post incremental updates to Slack (edit the message as chunks arrive) instead of waiting for the full completion. This matters more for live transcription use cases than post-meeting batch summaries. See /docs/streaming for how streaming responses work.
Production Considerations
- Transcript source. If you're pulling from Zoom or Google Meet, you'll need their respective recording/transcript APIs or a bot that joins the call — that's a separate integration, not covered by Claude directly.
- Rate limits and cost tracking. If multiple teams in your company use the app, give each team its own API key so you can see usage per team in one dashboard rather than one shared bill. SubToAPI supports Team (€19/seat) and Scale (€49/seat) plans for this, or Solo (€9) if it's just you — check /pricing.
- PII handling. Meeting transcripts often contain sensitive info. Don't log raw transcripts longer than necessary, and be clear in your Slack app's description about what's sent to the model.
- Retry logic. Claude calls can occasionally fail or time out — wrap the summarization call in a retry with backoff, and post a fallback message to Slack if it ultimately fails, so the user isn't left wondering.
Questions
Does Slack require a response within 3 seconds for slash commands? Yes. You must return an HTTP 200 immediately, then use the response_url Slack provides to send the actual summary once it's ready, since Claude's summarization call will usually take longer than 3 seconds.
Can Claude summarize a transcript without timestamps or speaker labels? Yes, but quality improves significantly if speaker labels are present, since the model can then attribute action items to specific people. If your transcript source doesn't include labels, ask Claude to flag action items as "unassigned" rather than guessing.
What's the fastest way to get started without managing provider API keys directly? Sign up at /signup, grab a sub_live_... key, and point your backend at https://api.subtoapi.app/v1/messages as shown above — you get streaming, usage metadata, and team seats without separate billing setup.