Auto-write CRM notes from call summaries
Generate Northwind account updates after each client call — pulled from the transcript.
Published Jan 26, 2026
After a client call, the hard part is not the conversation — it is writing it up. Northwind account managers run back-to-back calls all day, and the CRM note that should follow each one gets pushed to “later”. By the time later arrives, the detail has faded and the note is a vague one-liner that helps nobody.
This script turns a call transcript into a tidy CRM update automatically. Point it at a transcript Doc and an account name, and it asks Claude for a three-sentence summary covering status, next step, and risk, then appends that note to a tracking sheet alongside the date and a link back to the transcript. The write-up happens while the call is still fresh — no spare afternoon required.
What you’ll need
- A Google Doc per call containing the transcript — anything that produces a text transcript (Meet, Otter, a manual paste) works, as long as it lands in a Doc you can open by ID.
- A Google Sheet to collect the notes. The first tab should have headers in
row 1:
Date,Account,Note,Transcript. - An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely.
The script
// The Sheet that collects every CRM note this script writes.
const CRM_NOTES_SHEET_ID = '1abcCrmNotesId';
// How much of the transcript to send to Claude. Long calls run past the
// token budget, so we cap the input — see "Watch out for".
const MAX_TRANSCRIPT_CHARS = 8000;
// Model and token budget for the summary. Sonnet handles the reasoning;
// 300 tokens is plenty for three sentences.
const NOTE_MODEL = 'claude-sonnet-4-6';
const NOTE_MAX_TOKENS = 300;
/**
* Reads a call transcript, asks Claude for a short CRM update, and
* appends it to the CRM notes Sheet.
*
* @param {string} transcriptDocId ID of the Doc holding the transcript.
* @param {string} account Name of the Northwind account.
*/
function writeCrmNote(transcriptDocId, account) {
// 1. Open the transcript Doc and read its text, trimmed to the cap.
const text = DocumentApp.openById(transcriptDocId)
.getBody()
.getText()
.slice(0, MAX_TRANSCRIPT_CHARS);
// 2. Bail out early if the Doc is empty — no point calling the API.
if (!text.trim()) {
Logger.log('Transcript ' + transcriptDocId + ' is empty — skipping.');
return;
}
// 3. Ask Claude for a fixed-shape, three-sentence update.
const prompt =
'Write a 3-sentence CRM update for the Northwind account "' + account +
'" from this call transcript. Cover: status, next step, risk.\n\n' + text;
// 4. Get the note back from the model.
const note = callClaude(prompt, NOTE_MODEL, NOTE_MAX_TOKENS);
// 5. Append a row: date, account, the note, and a link to the transcript.
SpreadsheetApp.openById(CRM_NOTES_SHEET_ID).getSheets()[0].appendRow([
new Date(),
account,
note,
'https://docs.google.com/document/d/' + transcriptDocId,
]);
Logger.log('Wrote CRM note for ' + account + '.');
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it
* is never pasted into the code.
*
* @param {string} prompt The prompt text.
* @param {string} model Claude model ID.
* @param {number} maxTokens Token budget for the reply.
* @return {string} The model's reply text.
*/
function callClaude(prompt, model, maxTokens) {
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 }],
}),
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
writeCrmNotetakes a transcript Doc ID and an account name, opens the Doc, and reads its body text, trimmed toMAX_TRANSCRIPT_CHARSso a long call cannot blow the token budget.- If the Doc is empty, it logs a message and stops — no wasted API call.
- It builds a prompt that pins the output to three sentences and three topics: status, next step, and risk. A fixed shape keeps every note consistent.
callClaudesends the prompt to Claude Sonnet, which is worth the extra cost here because summarising a call needs real comprehension of the transcript.- It appends one row to the CRM notes Sheet — the timestamp, the account name, the generated note, and a link back to the transcript so anyone reading the note can check the source.
Example run
Given a transcript Doc for the Harbour Logistics account containing a call about a delayed rollout, a run appends a row like this:
| Date | Account | Note | Transcript |
|---|---|---|---|
| 2026-01-26 | Harbour Logistics | The rollout is on hold pending sign-off from their IT team, who flagged a single-sign-on concern. Next step is a technical call with their security lead, booked for Thursday. Risk: the delay pushes the go-live past quarter-end, so renewal timing may need revisiting. | Open Doc |
That is a CRM entry someone can act on — three sentences, written while the call was still fresh, instead of a blank field nobody got round to filling.
Run it
This is a per-call job, so run it once after each call:
- In the Apps Script editor, open
writeCrmNoteand call it from a small wrapper that passes the transcript ID and account name, for example:
function logHarbourCall() {
writeCrmNote('1xyzTranscriptDocId', 'Harbour Logistics');
}
- Select the wrapper and click Run, approving the authorisation prompt the first time.
- Check the CRM notes Sheet for the new row.
To make it hands-off, have whatever produces the transcript Doc call
writeCrmNote directly, or run a daily trigger that scans a “transcripts”
folder for new Docs.
Watch out for
- The transcript is capped at
MAX_TRANSCRIPT_CHARS(8000). A long call is truncated, so the note reflects only the first part. For long calls, raise the cap andNOTE_MAX_TOKENStogether, or summarise in sections. - The note is only as good as the transcript. Garbled speech-to-text produces a garbled summary — skim each note before it goes to a client-facing record.
- There is no retry. A transient API failure throws and the row is skipped.
Wrap the call in a
try/catchif you need it to keep going across a batch. - The script trusts the account name you pass in. A typo writes the note against the wrong account, so pull the name from a reliable source rather than typing it each time.
Related
Generate and test email subject lines
A/B test AI-written Northwind subject lines for open rate — outputs ranked by past performance.
Updated Mar 3, 2026
Build retrieval-augmented Q&A over your data
Answer Northwind questions grounded in your own Sheet data — pass relevant rows as context.
Updated Feb 27, 2026
Build an AI weekly-report narrator
Turn Northwind metrics into a written executive summary — numbers in, prose out.
Updated Feb 23, 2026
Build a multi-step AI agent workflow
Chain Claude prompts to complete a Northwind task end to end — research → draft → critique → finalise.
Updated Feb 11, 2026
Adapt marketing copy per region
Localise Northwind tone and references by market with AI — same message, regional flavour.
Updated Jan 30, 2026