appscript.dev
Automation Intermediate Docs Sheets

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_KEY in 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

  1. writeCrmNote takes a transcript Doc ID and an account name, opens the Doc, and reads its body text, trimmed to MAX_TRANSCRIPT_CHARS so a long call cannot blow the token budget.
  2. If the Doc is empty, it logs a message and stops — no wasted API call.
  3. 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.
  4. callClaude sends the prompt to Claude Sonnet, which is worth the extra cost here because summarising a call needs real comprehension of the transcript.
  5. 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:

DateAccountNoteTranscript
2026-01-26Harbour LogisticsThe 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:

  1. In the Apps Script editor, open writeCrmNote and call it from a small wrapper that passes the transcript ID and account name, for example:
function logHarbourCall() {
  writeCrmNote('1xyzTranscriptDocId', 'Harbour Logistics');
}
  1. Select the wrapper and click Run, approving the authorisation prompt the first time.
  2. 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 and NOTE_MAX_TOKENS together, 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/catch if 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