On the walk home from a meeting I would open my notes and realize I had captured maybe half of what was actually decided. After that happened enough times, I stopped taking notes in the room entirely and started reconstructing them at a coffee shop right afterward.
That reconstruction was always the task that slipped. As an indie developer there is nobody to hand note-taking to, so that particular backlog only ever grew.
An update on August 26, 2026 lets Google Meet note-taking start directly from the home screen on web and mobile. Being able to open a note-taking session without joining a call means you can also run it in a room with the other person sitting across from you.
The catch is that if you just start using it, the generated documents pile up all over Drive. You gain a record and lose the ability to find it. The thing to decide first is not how the notes get written, but where they land.
Know What Comes Out Before You Start
When a note-taking session runs, Gemini listens to the conversation and, once it ends, produces a single Google Doc saved to Drive. Three fairly different kinds of content live inside that one document.
| Output | What it contains | How to treat it downstream |
|---|---|---|
| Structured summary | Headings per topic, a few lines of context under each | Read it directly. Also the part worth feeding to a model |
| Action items | Bulleted who-does-what | Transcribe into whatever tracks your tasks |
| Full transcript | Speech rendered verbatim | Insurance for disputes. You will rarely open it |
Treating all three as equally important is where this falls apart. Even a thirty-minute in-person conversation produces a substantial transcript, and handing the whole thing to the Gemini API later means paying a model to rediscover conclusions that the summary section already states plainly.
In practice you use the summary and the action items. The transcript sits there for the day someone remembers a conversation differently than you do. Drawing that line up front makes everything that follows considerably simpler.
Create One Intake Folder and Look Only at That
If you never choose a destination, the documents simply accumulate in your Drive in date order. The moment two or three projects run in parallel, filenames alone stop telling you which meeting produced which file.
What I settled on is deliberately unambitious: one intake folder, and a habit of moving the file there when the meeting ends.
Meetings/
_inbox/ ← generated docs land here first
2026/
exhibition-prep/
app-development/
The _inbox hop exists so the filing decision can be deferred. Right after a meeting there is usually somewhere else to be, and a workflow that demands a category choice in that moment does not survive contact with a busy week. A move takes seconds.
Everything automated below watches _inbox only. It picks up unfiled documents, writes decisions to a ledger, and marks them processed. Filing stays a human activity that can happen whenever, and the automation never depends on the result.
Once automation and human judgment share the same folder structure, changing one breaks the other. Keeping the entrance narrow avoids that entirely.
Pull Out the Decisions and Drop Them in a Ledger
Now for the Apps Script. It walks the documents in _inbox, sends only the summary section to the Gemini API, gets structured decisions back, and appends them to a spreadsheet.
Configuration and the entry point first.
const CONFIG = {
// ID of the _inbox folder (the tail of its Drive URL)
intakeFolderId: 'YOUR_FOLDER_ID',
// ID of the ledger spreadsheet decisions accumulate in
ledgerSheetId: 'YOUR_SHEET_ID',
// Check the official model list for the current ID
model: 'gemini-3.7-flash',
};
function collectMeetingNotes() {
const folder = DriveApp.getFolderById(CONFIG.intakeFolderId);
const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);
const store = PropertiesService.getScriptProperties();
while (files.hasNext()) {
const file = files.next();
const doneKey = 'done_' + file.getId();
if (store.getProperty(doneKey)) continue;
const section = extractSummarySection(file.getId());
if (!section) continue;
const result = askGemini(section);
appendToLedger(file, result);
store.setProperty(doneKey, String(Date.now()));
}
}The processed marker lives in script properties rather than in the file because I do not want to touch the document. A meeting record should stay as it was produced, and writing a marker into the body for the convenience of a script is exactly the kind of edit that erodes that. File IDs survive folder moves, so filing later never causes a double ingest.
Next, extracting just the summary section.
function extractSummarySection(docId) {
const body = DocumentApp.openById(docId).getBody();
const total = body.getNumChildren();
const HEADINGS = ['Summary', 'Overview', 'Recap'];
const lines = [];
let started = false;
for (let i = 0; i < total; i++) {
const el = body.getChild(i);
const type = el.getType();
if (type !== DocumentApp.ElementType.PARAGRAPH &&
type !== DocumentApp.ElementType.LIST_ITEM) continue;
const item = (type === DocumentApp.ElementType.PARAGRAPH)
? el.asParagraph()
: el.asListItem();
const text = item.getText().trim();
const isHeading = item.getHeading() !== DocumentApp.ParagraphHeading.NORMAL;
if (isHeading) {
if (!started && HEADINGS.some(function (h) { return text.indexOf(h) === 0; })) {
started = true;
continue;
}
if (started) break; // stop at the next heading
continue;
}
if (started && text) lines.push(text);
}
return lines.length ? lines.join('\n') : null;
}It is a plain heading-to-heading slice, nothing clever. Even so, the token volume you send drops by an order of magnitude compared with passing the whole document. Heading wording varies by locale and by how the session was run, so open one real document and check what the first heading actually says before you tune HEADINGS.
When no matching heading exists the function returns null and the caller skips the file. Forcing an unexpected document shape through the pipeline and landing half-formed rows in the ledger is worse than skipping it and noticing later.
Fix the Response Shape Before You Call the API
If decisions are going to be handled by machine afterward, there is no reason to accept free-form prose. Pin the shape with responseSchema.
function askGemini(sectionText) {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
const endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' +
CONFIG.model + ':generateContent';
const payload = {
contents: [{
role: 'user',
parts: [{ text: buildPrompt(sectionText) }]
}],
generationConfig: {
responseMimeType: 'application/json',
responseSchema: {
type: 'object',
properties: {
decisions: { type: 'array', items: { type: 'string' } },
owners: { type: 'array', items: { type: 'string' } },
due: { type: 'string' }
},
required: ['decisions']
}
}
};
const res = UrlFetchApp.fetch(endpoint, {
method: 'post',
contentType: 'application/json',
headers: { 'x-goog-api-key': apiKey },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code !== 200) {
throw new Error('Gemini API ' + code + ': ' + res.getContentText());
}
const body = JSON.parse(res.getContentText());
return JSON.parse(body.candidates[0].content.parts[0].text);
}
function buildPrompt(sectionText) {
return [
'Below is a meeting summary. Extract only what was actually decided.',
'Do not include proposals under consideration, deferred topics, or impressions.',
'Fill owners in the same order as decisions, using an empty string when unknown.',
'---',
sectionText
].join('\n');
}The absence of temperature in generationConfig is intentional. As of August 2026, temperature, top_p and top_k are deprecated. Implementations that relied on them to suppress output variance will need rewriting sooner or later, and there is no reason to bake them into code you are writing today.
That job moves to the schema and to the constraints in the prompt. Spelling out "do not include proposals under consideration" matters because summaries often place a conclusion and an open question side by side, and both get harvested as decisions if you say nothing.
Finally, the write to the ledger.
function appendToLedger(file, result) {
const sheet = SpreadsheetApp.openById(CONFIG.ledgerSheetId).getSheets()[0];
const decisions = result.decisions || [];
const owners = result.owners || [];
if (!decisions.length) return;
const rows = decisions.map(function (text, i) {
return [
Utilities.formatDate(file.getDateCreated(), 'Asia/Tokyo', 'yyyy-MM-dd'),
file.getName(),
text,
owners[i] || '',
result.due || '',
file.getUrl()
];
});
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
.setValues(rows);
}Carrying the source document URL on every row is the unglamorous part that earns its keep. When a line in the ledger makes you pause and wonder whether that is really what was agreed, you can jump straight to the original. A summary is only ever a summary, and I wanted the data structure itself to say that the evidence lives elsewhere.
I run this on a time-driven trigger once a day. Nothing needs to reach the ledger the instant a meeting ends; by the next morning is plenty. If your trigger count starts creeping up, the consolidation approach in When Apps Script Time-Driven Triggers Quietly Run Out applies directly. For permissions, follow the scope design in Keeping Apps Script + Gemini Automations on Least Privilege and declare only what you need in appsscript.json.
Decide What You Tell People Before You Use It in a Room
This part probably matters more than any of the setup above.
In a video call, everyone can see on screen that notes are being taken. Across a table, the other person has no idea what the device sitting there is doing.
I made it a personal rule to say something before starting. "I keep falling behind on writing these up afterward, so would you mind if I use the note-taking feature?" is usually received without friction. When someone would rather I did not, I write by hand and that is the end of it.
Three decisions worth making in advance, so you are not weighing them mid-conversation:
- Sharing: does the generated doc go to the other party, or stay as your own reference
- Retention: how long the full transcript survives. Once decisions reach the ledger, the originals can be cleared on a schedule
- Exclusions: meals with plenty of small talk, or conversations about things not yet public, are not candidates
Being recorded is not automatically welcome. Convenience is not a good enough reason to step over that.
Where to Start
Create the _inbox folder and run a note-taking session at your next meeting. Automation can wait. You need to read one generated document with your own eyes and see how its headings are actually phrased before extractSummarySection can be tuned to anything real.
On the broader question of how much of a new Workspace capability belongs inside your automation, Sheets canvas Takes the Entry Point, Not the Execution Boundary works through where the responsibility for execution should stay.
In-person records were the place my process never quite reached. Having something that reaches them is, quite simply, a relief.