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
- Meeting notes (Doc or pasted text)
- Gemini extracts structure
- Apps Script formats results
- 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
- Create a Google Doc with rough meeting notes
- Run
testGeminiConnection() - Paste Doc ID into sidebar
- Generate action plan
- 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