appscript.dev
Automation Intermediate Docs Sheets

Extract follow-ups from call transcripts

Turn Northwind sales calls into actionable tasks — owner + task + due date per extracted item.

Published Jan 10, 2026

A Northwind sales call ends with a handful of commitments — “I’ll send the pricing breakdown”, “let’s reconnect after your board meeting”, “can you loop in your IT lead”. Those follow-ups are the call’s whole value, and they live in a transcript nobody re-reads. By the next morning half of them are forgotten.

This script reads a call transcript from a Doc, asks Claude to pull out every follow-up as a structured task — what, who, and when — and appends each one as a row in a tasks spreadsheet. The commitments become a list you can actually work through.

What you’ll need

  • A Google Doc containing the call transcript — most meeting recorders can export one.
  • A Google Sheet of tasks with columns for task, owner, due date, and status — the script appends to the first tab.
  • 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 transcript and the Sheet that collects tasks.
const TRANSCRIPT_DOC_ID = '1abcTranscriptDocId';
const TASKS_SHEET_ID = '1abcTasksId';

// How much of the transcript to send in one pass — keeps the prompt
// within a sensible token budget. See "Watch out for".
const CONTEXT_CHARS = 12000;

/**
 * Reads a call transcript, asks Claude to extract every follow-up as a
 * structured task, and appends each one to the tasks spreadsheet.
 */
function extractFollowUps() {
  // 1. Read the transcript text.
  const text = DocumentApp.openById(TRANSCRIPT_DOC_ID)
    .getBody().getText().slice(0, CONTEXT_CHARS);
  if (!text.trim()) {
    Logger.log('Transcript Doc is empty — nothing to extract.');
    return;
  }

  // 2. Ask Claude for follow-ups as strict JSON. A fixed schema is the
  //    difference between a parseable result and a parsing nightmare.
  const prompt =
    'Extract follow-ups from this Northwind sales-call transcript. ' +
    'Return ONLY a JSON array — no prose, no markdown — in this shape: ' +
    '[{"task": string, "owner": string, "dueDate": "YYYY-MM-DD or null"}]. ' +
    'Only include real commitments, not general discussion.\n\n' + text;
  const items = JSON.parse(stripFences(
    callClaude(prompt, 'claude-sonnet-4-6', 1000)));

  if (!items.length) {
    Logger.log('No follow-ups found in the transcript.');
    return;
  }

  // 3. Append one row per follow-up, all marked "open".
  const sheet = SpreadsheetApp.openById(TASKS_SHEET_ID).getSheets()[0];
  for (const it of items) {
    sheet.appendRow([it.task, it.owner, it.dueDate || '', 'open']);
  }
  Logger.log('Appended ' + items.length + ' follow-ups to the tasks sheet.');
}

/**
 * Claude occasionally wraps JSON in a ```json code fence. Strip it so
 * JSON.parse never chokes on the markdown.
 */
function stripFences(textValue) {
  return textValue.replace(/```(?:json)?/g, '').trim();
}

/**
 * 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. extractFollowUps opens the transcript Doc and reads the first CONTEXT_CHARS characters of its text.
  2. If the transcript is empty, it logs a message and stops — no wasted API call.
  3. It builds a prompt that pins the output to a strict JSON schema: an array of objects, each with a task, an owner, and a dueDate that can be null.
  4. It calls Claude Sonnet, which is worth the extra cost because telling a real commitment apart from passing discussion needs reasoning. stripFences removes any code fence, then JSON.parse turns the reply into objects.
  5. If no follow-ups are found, it logs a message and stops.
  6. It appends one row per follow-up to the first tab of the tasks sheet, each marked open, ready to be worked through.

Example run

Say the transcript Doc ends with:

Rep: Great — I’ll get the enterprise pricing over to you by Friday. Client: Perfect. I’ll need to run it past our IT lead, Sam, first. Rep: Understood. Shall we reconnect the week of the 20th? Client: Yes, book something in.

After a run, the tasks sheet gains these rows:

TaskOwnerDue dateStatus
Send enterprise pricing breakdownSales rep2026-01-16open
Review pricing with IT lead SamClientopen
Book a reconnect call for week of the 20thSales rep2026-01-20open

Three rows you can assign and track, instead of a transcript nobody reopens.

Trigger it

Run this after each call. The simplest setup is a custom menu on the tasks sheet, so a rep can trigger it the moment a transcript is ready:

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Call tools')
    .addItem('Extract follow-ups', 'extractFollowUps')
    .addToUi();
}

If your recorder drops transcripts into a fixed Drive folder, you can instead run extractFollowUps on a time-driven trigger that processes new Docs — but have it skip Docs it has already handled so rows are not duplicated.

Watch out for

  • It always reads the same TRANSCRIPT_DOC_ID. To process many calls, pass the Doc ID in as an argument or read it from the active sheet — otherwise every run re-extracts the same transcript.
  • Run it twice on one transcript and you get duplicate rows. appendRow never de-duplicates — extract once per call, or check for existing rows first.
  • Due dates are inferred. “By Friday” or “week of the 20th” is resolved against the call date as Claude reads it; always sanity-check the dates before relying on them.
  • Only the first CONTEXT_CHARS characters are read. A very long call is truncated, so late commitments can be missed — raise the cap and max_tokens together for marathon calls.
  • Transcripts can name clients and deals. The text is sent to the API, so keep anything genuinely confidential out of the transcript Doc first.

Related