appscript.dev
Automation Intermediate Docs

Summarize chat and Slack exports

Digest Northwind's long Slack conversations into recaps — for catch-up after PTO.

Published Dec 5, 2025

Come back from a week off and the Northwind channels have a thousand unread messages. Most of it is noise — lunch plans, emoji reactions, threads that went nowhere — but somewhere in there are the three decisions that actually affect your week. Scrolling to find them takes the whole first morning back.

This script reads an exported chat log from a Doc, breaks it into chunks small enough to summarise, recaps each chunk, then stitches the chunk summaries into one coherent recap at the bottom of the same Doc. You skim ten bullets instead of a thousand messages.

What you’ll need

  • A Google Doc holding the exported chat log — paste in a Slack channel export, a Teams transcript, or any plain-text chat history. The script reads the Doc and appends the recap to the end.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.

The script

// The Doc holding the exported chat log; the recap is appended to it.
const EXPORT_DOC_ID = '1abcExportDocId';

// Characters per chunk. Small enough that each chunk summary is cheap
// and reliable — see "Watch out for".
const CHUNK_SIZE = 8000;

/**
 * Reads the chat-export Doc, summarises it in chunks, stitches the
 * chunk summaries into one recap, and appends that recap to the Doc.
 */
function appendChatRecap() {
  const doc = DocumentApp.openById(EXPORT_DOC_ID);
  const exportText = doc.getBody().getText();

  if (!exportText.trim()) {
    Logger.log('Export Doc is empty — nothing to summarise.');
    return;
  }

  // 1. Produce a single recap from the whole export.
  const recap = summariseSlackExport(exportText);

  // 2. Append it under a dated heading so it is easy to find later.
  const today = Utilities.formatDate(
    new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd');
  const body = doc.getBody();
  body.appendParagraph('Recap — ' + today)
    .setHeading(DocumentApp.ParagraphHeading.HEADING2);
  body.appendParagraph(recap);
  Logger.log('Appended a recap dated ' + today + '.');
}

/**
 * Summarises a long chat export with a map-reduce approach: summarise
 * each chunk, then summarise the summaries into one recap.
 */
function summariseSlackExport(exportText) {
  // Map: split the export into chunks and summarise each one.
  const chunks = [];
  for (let i = 0; i < exportText.length; i += CHUNK_SIZE) {
    chunks.push(exportText.slice(i, i + CHUNK_SIZE));
  }
  const summaries = chunks.map((chunk) => callClaude(
    'Summarise this Northwind chat snippet in 3 bullets. ' +
    'Focus on decisions and action items, skip small talk.\n\n' + chunk,
    'claude-sonnet-4-6', 500));

  // Reduce: stitch the chunk summaries into one coherent recap.
  return callClaude(
    'Stitch these summaries into one coherent recap (max 10 bullets). ' +
    'Lead with decisions and anything needing follow-up:\n\n' +
    summaries.join('\n\n'),
    'claude-sonnet-4-6', 800);
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');
  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. appendChatRecap opens the export Doc and reads its full text.
  2. If the Doc is empty, it logs a message and stops — no wasted API calls.
  3. It passes the text to summariseSlackExport, which splits it into chunks of CHUNK_SIZE characters — small enough that each chunk fits comfortably in a prompt.
  4. The map step summarises each chunk into three bullets, focused on decisions and action items rather than chatter.
  5. The reduce step sends all the chunk summaries back to Claude in one prompt, asking for a single coherent recap of at most ten bullets.
  6. appendChatRecap appends that recap under a dated Heading 2, so the recap and its source export stay in one Doc.

Example run

Say the export Doc contains 1,200 messages across a fortnight. After a run, the Doc gets a new section:

Recap — 2025-12-05

  • Decision: the Q1 pricing change is approved, rolling out 6 January.
  • Maria is owning the migration runbook; draft due Friday.
  • The staging outage on the 2nd was a config issue, now fixed.
  • Open question: who covers support over the holiday week? Unresolved.
  • New hire (designer) starts 12 January — onboarding doc needed.

Five bullets that tell you what changed and what still needs you, instead of a fortnight of scrollback.

Run it

This is an on-demand job — run it whenever you need to catch up:

  1. Paste the chat export into the Doc and set EXPORT_DOC_ID to it.
  2. In the Apps Script editor, select appendChatRecap and click Run.
  3. Approve the authorisation prompt the first time.
  4. Open the Doc — the recap is at the bottom under a dated heading.

A long export means several API calls in sequence, so give the run a moment to finish before checking the Doc.

Watch out for

  • It is a map-reduce, so cost scales with length: one call per chunk plus one final call. A very long export is several calls — fine occasionally, worth watching if you run it daily.
  • Detail is lost on purpose. Each chunk is squeezed to three bullets before the final pass, so a minor point in one chunk may not survive. For the full thread, read the source.
  • Chat exports are sensitive. The whole log is sent to the API — strip private DMs or HR threads before pasting, or only export the channels you need.
  • Threaded replies often export out of order. The recap can read slightly jumbled if the export interleaves threads — that is the export’s doing, not the script’s.

Related