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_KEYin 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
extractFollowUpsopens the transcript Doc and reads the firstCONTEXT_CHARScharacters of its text.- If the transcript is empty, it logs a message and stops — no wasted API call.
- It builds a prompt that pins the output to a strict JSON schema: an array of
objects, each with a
task, anowner, and adueDatethat can benull. - It calls Claude Sonnet, which is worth the extra cost because telling a real
commitment apart from passing discussion needs reasoning.
stripFencesremoves any code fence, thenJSON.parseturns the reply into objects. - If no follow-ups are found, it logs a message and stops.
- 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:
| Task | Owner | Due date | Status |
|---|---|---|---|
| Send enterprise pricing breakdown | Sales rep | 2026-01-16 | open |
| Review pricing with IT lead Sam | Client | open | |
| Book a reconnect call for week of the 20th | Sales rep | 2026-01-20 | open |
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.
appendRownever 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_CHARScharacters are read. A very long call is truncated, so late commitments can be missed — raise the cap andmax_tokenstogether 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
Parse semi-structured listings into tables
Extract recipes, specs, or ads from Northwind content into clean spreadsheet rows.
Updated Mar 7, 2026
Build an AI data-enrichment pipeline
Fill missing company and contact fields on Northwind's prospect list — Claude infers from the row.
Updated Nov 23, 2025
Build an AI invoice and receipt parser
Read Northwind documents into structured ledger rows — vendor, amount, line items.
Updated Oct 2, 2025
Extract entities and relationships from text
Build a structured graph from Northwind prose — people, companies, and how they connect.
Updated Sep 24, 2025
Build an AI data-cleaning assistant
Standardise messy Northwind names, addresses, and categories into clean canonical values.
Updated Sep 4, 2025