appscript.dev
Automation Intermediate Docs

Draft meeting agendas from past notes

Generate Northwind meeting agendas from history and stated goals — no blank-page syndrome.

Published Oct 18, 2025

The hardest part of a recurring Northwind meeting is starting the agenda. The context is all there — last week’s notes list what was decided, what stalled, and what got pushed — but turning that into a fresh agenda means re-reading everything and remembering which threads are still open. So the agenda gets thrown together five minutes before the call, and the same items slip week after week.

This script reads the running notes Doc, takes a short list of goals for the next session, and asks Claude to draft an agenda that carries forward the open threads. It appends the draft to the bottom of the same Doc, so the agenda and the history live together.

What you’ll need

  • A Google Doc holding your running meeting notes — the script reads the most recent text and appends the new agenda to the end.
  • A short list of goals for the next session, set in the MEETING_GOALS constant (or passed in from a menu prompt).
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.

The script

// The running meeting-notes Doc. The script reads it and appends to it.
const NOTES_DOC_ID = '1abcNotesDocId';

// What the next session should focus on. Edit this before each run.
const MEETING_GOALS = 'Q2 launch timeline, hiring plan, support backlog';

// How much of the notes Doc to send. The most recent text matters most,
// so we read from the end — see "Watch out for".
const CONTEXT_CHARS = 6000;

/**
 * Reads the recent meeting notes, asks Claude to draft an agenda for
 * the next session, and appends that agenda to the bottom of the Doc.
 */
function appendNextAgenda() {
  const doc = DocumentApp.openById(NOTES_DOC_ID);
  const body = doc.getBody();

  // 1. Read the most recent slice of notes for context.
  const draft = draftAgenda(NOTES_DOC_ID, MEETING_GOALS);
  if (!draft) {
    Logger.log('No agenda returned — nothing to append.');
    return;
  }

  // 2. Append the agenda under a dated heading so it is easy to find.
  const today = Utilities.formatDate(
    new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd');
  body.appendParagraph('Agenda — ' + today)
    .setHeading(DocumentApp.ParagraphHeading.HEADING2);
  body.appendParagraph(draft);
  Logger.log('Appended an agenda dated ' + today + '.');
}

/**
 * Asks Claude to draft a numbered agenda from the tail of the notes
 * Doc, focused on the goals it is given.
 */
function draftAgenda(notesDocId, goals) {
  // Read the END of the notes — the most recent session matters most.
  const full = DocumentApp.openById(notesDocId).getBody().getText();
  const past = full.slice(-CONTEXT_CHARS);

  if (!past.trim()) {
    Logger.log('Notes Doc is empty — cannot draft from history.');
    return '';
  }

  const prompt =
    'Given these recent Northwind meeting notes:\n' + past +
    '\n\nDraft an agenda for the next session focused on: ' + goals + '. ' +
    'Carry forward any unresolved items from the notes. ' +
    'Return as a numbered list, each item with a short time estimate.';
  return callClaude(prompt, 'claude-sonnet-4-6', 600);
}

/**
 * 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. appendNextAgenda opens the notes Doc and calls draftAgenda with the goals set in MEETING_GOALS.
  2. draftAgenda reads the Doc’s full text and keeps only the last CONTEXT_CHARS characters — the most recent session, which is the part that actually predicts the next agenda.
  3. If that slice is empty, it logs a message and stops — there is nothing to draft from.
  4. It builds a prompt that includes the recent notes and the goals, and asks Claude to carry forward any unresolved items and add a time estimate per line.
  5. It calls Claude Sonnet, which handles the reasoning needed to spot which threads are still open.
  6. Back in appendNextAgenda, the draft is appended under a dated Heading 2, so the agenda and its source notes stay in one Doc.

Example run

Say the bottom of the notes Doc reads:

2025-10-11 — Agreed the Q2 launch slips two weeks. Hiring: still need a second designer; JD not written. Support backlog hit 40 tickets — no owner assigned yet.

With MEETING_GOALS set to Q2 launch timeline, hiring plan, support backlog, a run appends:

Agenda — 2025-10-18

  1. Confirm the revised Q2 launch date and lock dependencies (15 min)
  2. Designer hire: assign an owner to write the JD this week (10 min)
  3. Support backlog: pick an owner, agree a target to clear 40 tickets (15 min)
  4. Quick wins and blockers round-table (10 min)

Every open thread from last week is on the agenda — nothing slips because someone forgot.

Run it

This is a once-per-meeting job, so run it by hand before each session:

  1. Update MEETING_GOALS with what the next session should cover.
  2. In the Apps Script editor, select appendNextAgenda and click Run.
  3. Approve the authorisation prompt the first time.
  4. Open the notes Doc — the new agenda is at the bottom under a dated heading.

To make it self-serve, add a custom menu that prompts for goals at run time:

function onOpen() {
  DocumentApp.getUi()
    .createMenu('Meeting tools')
    .addItem('Draft next agenda', 'appendNextAgenda')
    .addToUi();
}

Watch out for

  • It drafts from the end of the Doc. If your most recent notes are at the top, read the start instead with .slice(0, CONTEXT_CHARS) — otherwise Claude works from stale context.
  • The agenda is a starting point, not a final one. Always read it before the meeting: Claude can carry forward an item that was actually closed, or miss one phrased vaguely.
  • It appends, it never overwrites. Run it twice and you get two agendas — fine, but delete the stale one so the Doc does not sprawl.
  • Meeting notes can be sensitive. The recent text is sent to the API, so keep anything confidential — salaries, personnel issues — out of the notes Doc, or shorten CONTEXT_CHARS so it stays out of the prompt.

Related