Action Plan for Google Docs Issue 12 Apps Script and Gemini

Apps Script + Gemini Mastery — Issue #12

AI Meeting Notes → Action Plan for Google Docs

Turn raw meeting notes into structured decisions, action items, and follow-up emails using Apps Script + Gemini.

https://github.com/lsvekis/Apps-Script-Code-Snippets


⭐ What You Will Build

You’ll build an AI-powered tool that:

📝 Takes messy meeting notes (copied or written in a Google Doc)
🧠 Uses Gemini to understand context and intent
📌 Extracts:

  • Decisions made
  • Action items (with owners + deadlines when possible)
  • Open questions / risks
    📄 Generates a clean, structured meeting summary in Google Docs
    📧 Optionally drafts a follow-up email based on the meeting

All inside Google Workspace.
No external note-taking apps.
No transcription services required.


🧠 Learning Objectives

This issue teaches readers how to:

✔ Read and write Google Docs programmatically
✔ Design prompts for information extraction, not just text generation
✔ Force structured output (JSON → formatted Doc)
✔ Separate “analysis” from “presentation” layers
✔ Build repeatable AI workflows for real business use


🧩 Architecture Overview

Input → AI Analysis → Structured Output → Human Review

  1. Meeting notes (Doc or pasted text)
  2. Gemini extracts structure
  3. Apps Script formats results
  4. Human reviews and sends follow-up

This is decision support AI, not auto-automation.


✅ Build Steps (Complete Lesson)

1️⃣ Create the Menu + Sidebar

Code.gs

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("AI Tools")
    .addItem("Meeting Notes → Action Plan", "showMeetingAssistant")
    .addToUi();
}

function showMeetingAssistant() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createHtmlOutputFromFile("Sidebar")
      .setTitle("Meeting Notes Assistant")
  );
}

2️⃣ Sidebar UI

Sidebar.html

<div style="font-family:Arial;padding:14px;">
  <h2>Meeting Notes → Action Plan</h2>

  <label><b>Google Doc ID (meeting notes)</b></label>
  <input id="docId" style="width:100%;margin-bottom:8px;"
         placeholder="Paste Doc ID here" />

  <label><b>Meeting context (optional)</b></label>
  <textarea id="context" style="width:100%;height:60px;"></textarea>

  <button onclick="run()">Generate Action Plan</button>

  <pre id="out" style="white-space:pre-wrap;margin-top:12px;max-height:260px;overflow:auto;"></pre>

  <script>
    function run() {
      document.getElementById("out").textContent = "Analyzing meeting notes...";
      google.script.run
        .withSuccessHandler(msg => document.getElementById("out").textContent = msg)
        .processMeetingNotes(
          document.getElementById("docId").value,
          document.getElementById("context").value
        );
    }
  </script>
</div>

3️⃣ Read Meeting Notes from Google Docs

DocReader.gs

function getMeetingNotes_(docId) {
  if (!docId) throw new Error("Missing Doc ID.");

  const doc = DocumentApp.openById(docId);
  const text = doc.getBody().getText();

  // Keep prompt size reasonable
  return text.length > 12000 ? text.substring(0, 12000) : text;
}

4️⃣ Gemini Prompt for Structured Extraction

MeetingAnalyzer.gs

function analyzeMeeting_(notes, context) {
  const prompt = `
You are an expert meeting analyst.

Analyze the meeting notes below and return JSON ONLY in this format:

{
  "summary": "brief meeting overview",
  "decisions": ["decision 1", "decision 2"],
  "actionItems": [
    { "task": "task description", "owner": "name or role", "due": "date or unknown" }
  ],
  "openQuestions": ["question 1", "question 2"],
  "risks": ["risk 1", "risk 2"]
}

Rules:
- Do NOT invent details.
- If owner or due date is missing, use "unknown".
- Be concise and factual.

Meeting context (if any):
${context || ""}

Meeting notes:
${notes}
`;

  let out = callGemini(prompt, "");
  out = out.replace(/```json/i, "").replace(/```/g, "").trim();

  return JSON.parse(out);
}

5️⃣ Create the Action Plan Doc

DocWriter.gs

function writeActionPlanDoc_(analysis, sourceDocId) {
  const sourceDoc = DocumentApp.openById(sourceDocId);
  const title = sourceDoc.getName();

  const doc = DocumentApp.create("Action Plan — " + title);
  const body = doc.getBody();

  body.appendParagraph("Meeting Summary")
      .setHeading(DocumentApp.ParagraphHeading.HEADING_1);
  body.appendParagraph(analysis.summary);

  body.appendParagraph("Decisions")
      .setHeading(DocumentApp.ParagraphHeading.HEADING_1);
  (analysis.decisions || []).forEach(d => body.appendListItem(d));

  body.appendParagraph("Action Items")
      .setHeading(DocumentApp.ParagraphHeading.HEADING_1);
  (analysis.actionItems || []).forEach(a => {
    body.appendListItem(`${a.task} (Owner: ${a.owner}, Due: ${a.due})`);
  });

  body.appendParagraph("Open Questions")
      .setHeading(DocumentApp.ParagraphHeading.HEADING_1);
  (analysis.openQuestions || []).forEach(q => body.appendListItem(q));

  body.appendParagraph("Risks")
      .setHeading(DocumentApp.ParagraphHeading.HEADING_1);
  (analysis.risks || []).forEach(r => body.appendListItem(r));

  return doc.getUrl();
}

6️⃣ Orchestrator Function

MeetingProcessor.gs

function processMeetingNotes(docId, context) {
  let notes;
  try {
    notes = getMeetingNotes_(docId);
  } catch (e) {
    return "Doc error: " + e;
  }

  let analysis;
  try {
    analysis = analyzeMeeting_(notes, context);
  } catch (e) {
    return "Gemini analysis error. Check logs.";
  }

  const url = writeActionPlanDoc_(analysis, docId);
  return "✅ Action Plan created:\n" + url;
}

7️⃣ Gemini Helper (Reuse from Previous Issues)

Same GeminiHelpers.gs used in Issues #9–11.


🧪 Testing Checklist

  1. Create a Google Doc with rough meeting notes
  2. Run testGeminiConnection()
  3. Paste Doc ID into sidebar
  4. Generate action plan
  5. Review decisions + tasks before sending

🔥 Exercise Upgrades (Advanced Readers)

✅ Add follow-up email draft generation
✅ Add “decision confidence” tagging
✅ Add meeting template detection
✅ Append action plan back into original Doc
✅ Convert action items to Tasks or Sheets