appscript.dev
Automation Intermediate Docs Sheets

Turn meeting notes into assigned action items

Extract tasks and owners from Northwind meeting transcripts into the Tasks sheet.

Published Aug 7, 2025

Every Northwind meeting ends the same way: someone says “I’ll write that up” and the action items sink into a wall of notes nobody opens again. The decisions are in the document — they are just buried between the discussion and the digressions, with no owner attached and no due date.

This script reads a meeting-notes doc, asks Claude to pull out only the concrete tasks and who owns each one, and appends them to a shared Tasks sheet with a status and a timestamp. Run it the moment a meeting wraps and the follow-ups are tracked before anyone has left the room.

What you’ll need

  • A Google Doc of meeting notes — the script takes the document ID as an argument, so you can point it at whichever doc you just finished.
  • A Google Sheet to collect the tasks, with a header row of Task, Owner, Status, Logged.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.

The script

// The spreadsheet that collects extracted tasks.
const TASKS_SHEET_ID = '1abcTasksId';

// Status applied to every freshly extracted task.
const DEFAULT_STATUS = 'open';

/**
 * Reads a meeting-notes doc, asks Claude to extract action items with
 * owners, and appends each one to the Tasks sheet.
 * @param {string} notesDocId The ID of the Google Doc holding the notes.
 */
function extractActionItems(notesDocId) {
  // 1. Pull the full plain text of the meeting-notes document.
  const text = DocumentApp.openById(notesDocId).getBody().getText();

  if (!text.trim()) {
    Logger.log('The notes document is empty — nothing to extract.');
    return;
  }

  // 2. Ask Claude for strict JSON: a task and an owner per item.
  const prompt =
    'Extract action items from these Northwind meeting notes. ' +
    'Return ONLY a JSON array — no prose, no markdown — in this shape: ' +
    '[{"task": string, "owner": string}]. ' +
    'Use "unassigned" as the owner when the notes name no one.\n\n' + text;

  // 3. Parse the reply into real objects.
  const items = JSON.parse(stripFences(callClaude(prompt)));

  if (!items.length) {
    Logger.log('No action items found in the notes.');
    return;
  }

  // 4. Append one row per task with a status and a timestamp.
  const sheet = SpreadsheetApp.openById(TASKS_SHEET_ID).getSheets()[0];
  for (const it of items) {
    sheet.appendRow([it.task, it.owner, DEFAULT_STATUS, new Date()]);
  }
  Logger.log('Logged ' + items.length + ' action items 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(text) {
  return text.replace(/```(?:json)?/g, '').trim();
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt) {
  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: 'claude-sonnet-4-6',
      max_tokens: 1000,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. extractActionItems opens the meeting-notes document by ID and reads its body as plain text.
  2. If the document 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 and an owner — and tells Claude to fall back to "unassigned" when no name is mentioned.
  4. It calls Claude Sonnet, which handles the reasoning of separating real commitments from general discussion. stripFences removes any code fence, then JSON.parse turns the reply into objects.
  5. If nothing was extracted, it stops there.
  6. It appends one row per task to the Tasks sheet, tagging each with the default status and the current date so follow-ups have a paper trail.

Example run

Say the meeting-notes doc contains a passage like this:

Sandra agreed to send the revised quote to the Harlow client by Friday. We also need updated render thumbnails — Tom will handle that. General agreement that the booking form is too long, but we left that for later.

After a run, the Tasks sheet gains two rows (the booking-form remark is discussion, not a commitment, so it is left out):

TaskOwnerStatusLogged
Send the revised quote to the Harlow client by FridaySandraopen2026-05-25
Produce updated render thumbnailsTomopen2026-05-25

Run it

This is a once-per-meeting job, so run it by hand when the notes are ready:

  1. In the Apps Script editor, open extractActionItems and pass it the doc ID — either edit a wrapper function or call it from the editor with the ID.
  2. Approve the authorisation prompt the first time.
  3. Open the Tasks sheet to see the new rows.

To trigger it straight from a doc, add a custom menu so the meeting owner can run it without touching the editor:

function onOpen() {
  DocumentApp.getUi()
    .createMenu('Meeting tools')
    .addItem('Extract action items', 'extractFromThisDoc')
    .addToUi();
}

function extractFromThisDoc() {
  extractActionItems(DocumentApp.getActiveDocument().getId());
}

Watch out for

  • Claude decides what counts as an action item. A vague “we should look into that” may or may not become a task — review the rows after the first few runs and tighten the prompt if it is too eager or too cautious.
  • Owners are matched on names in the notes. If the doc says “Tom” but your team tracker uses full names, normalise the owner column before assigning.
  • Running the same doc twice appends duplicate rows. Extract once per meeting, or add a check against an already-logged doc ID if re-runs are likely.
  • Long transcripts can exceed the token budget. For hour-long meetings, raise max_tokens or split the notes into sections and extract each in turn.

Related