Extract action items from meeting notes
Pull checkboxed lines or `[ ]` bullets from notes Docs into a Tasks sheet.
Published Aug 3, 2025
Northwind’s meetings end the same way every time: someone scrolls through the notes Doc, reads the action items out loud, and promises to “drop them in the tracker later”. Later rarely happens. The actions sit buried in the Doc until the next standup, when someone re-asks who was meant to be doing what.
This script does the boring half. The notes follow a convention — every action
is written as - [ ] @person task — so a regular expression can find them all.
The script walks every Doc in the notes folder, picks out the unchecked
bullets, and appends each one as a row in a Tasks sheet with owner, status,
created date, and a stable ID so re-runs do not duplicate the same action.
What you’ll need
- A Drive folder that holds the meeting notes Docs (one Doc per meeting).
- A Google Sheet with five columns in the first tab:
task,owner,status,created,id. Row 1 is headers, the script writes below. - A house style for actions in the notes:
- [ ] @owner the action. Anything that does not match is treated as ordinary prose and ignored.
The script
// The Drive folder that holds meeting notes Docs.
const NOTES_FOLDER = '1abcNotesFolderId';
// The sheet that receives extracted action items.
const TASKS_SHEET = '1abcTasksSheetId';
// The unchecked-action pattern. Tweak if your house style differs.
// - [ ] @owner the task
const ACTION_PATTERN = /^- \[\s\] @(\S+)\s+(.+)$/gm;
const ACTION_LINE = /^- \[\s\] @(\S+)\s+(.+)$/;
/**
* Walks every notes Doc, extracts action items that look like
* `- [ ] @owner the task`, and appends new ones to the Tasks sheet.
*/
function extractActionItems() {
const tasks = SpreadsheetApp.openById(TASKS_SHEET).getSheets()[0];
// Column E holds the stable ID for each task. Pull it once so we can
// skip rows we have already imported.
const seen = new Set(
tasks.getRange('E2:E')
.getValues()
.flat()
.filter(Boolean)
);
const files = DriveApp.getFolderById(NOTES_FOLDER).getFiles();
const rows = [];
while (files.hasNext()) {
const file = files.next();
// Only read actual Docs — skip PDFs, images, anything else in the folder.
if (file.getMimeType() !== MimeType.GOOGLE_DOCS) continue;
const body = DocumentApp.openById(file.getId()).getBody().getText();
const matches = body.match(ACTION_PATTERN) || [];
for (const m of matches) {
const parsed = m.match(ACTION_LINE);
if (!parsed) continue;
const [, owner, task] = parsed;
// Composite ID — same task in the same Doc is the same row, even if
// it appears more than once in different sections.
const id = file.getId() + '|' + task;
if (seen.has(id)) continue;
rows.push([task, owner, 'open', new Date(), id]);
seen.add(id);
}
}
if (rows.length) {
tasks.getRange(tasks.getLastRow() + 1, 1, rows.length, 5).setValues(rows);
Logger.log('Appended ' + rows.length + ' new action item(s).');
} else {
Logger.log('No new action items to append.');
}
}
How it works
extractActionItemsopens theTaskssheet and reads column E — the stable ID column — into aSet. That set is the de-dup index for the whole run.- It iterates every file in the notes folder. Files that are not Google Docs are skipped, so a stray PDF or image in the folder does not throw.
- For each Doc, it pulls the full body text and runs the
ACTION_PATTERNregex globally. The pattern matches lines that start with- [ ], an@owner, and the rest of the line as the task. - Each match is parsed a second time with
ACTION_LINEto capture the owner and task as groups. The composite ID isdocId|task, which means the same action in the same Doc is identical across runs even if the line moves up or down the page. - If the ID is already in
seen, the script skips it. Otherwise it queues a five-column row: task, owner, status (alwaysopenon first import), created date, and the ID. The ID is added toseenso the same action appearing twice in one pass does not get inserted twice. - Once every Doc is scanned, the queued rows are written in one batched
setValuescall below the existing data.
Example run
The notes folder contains 2025-05-21 Acme sync.gdoc with this section:
## Actions
- [ ] @priya draft pricing options for the call on Friday
- [x] @sam send the contract — already done
- [ ] @priya book the studio for next Tuesday
After the script runs, the Tasks sheet gains two rows:
| task | owner | status | created | id |
|---|---|---|---|---|
| draft pricing options for the call on Friday | priya | open | 2025-05-21 09:00 | …AcmeNotes|draft pricing options... |
| book the studio for next Tuesday | priya | open | 2025-05-21 09:00 | …AcmeNotes|book the studio... |
The [x] (already done) line is skipped because the regex demands a space
between the brackets. On the next run, neither row is re-inserted because
both IDs are already in column E.
Trigger it
Run it on a clock so notes captured during the day land in the tracker without anyone thinking about it.
- Open Triggers in the Apps Script editor.
- Add Trigger for
extractActionItems, event source Time-driven, type Hour timer, interval Every hour. - Approve the Drive, Docs, and Sheets scopes on the first run.
Hourly is plenty for meeting notes; daily is fine if the team writes actions in bursts at the end of the day rather than during the meeting.
Watch out for
- The regex is strict.
- [ ]needs exactly one space inside the brackets. Tabs, two spaces, or-[ ]will not match. Either keep the convention or loosen the pattern. - Owners must be a single token (
@priya, not@Priya Patel). For full names, change(\S+)to(@[^\s]+)and strip the@later, or use a delimiter your team agrees on. Body.getText()flattens formatting, which is good — it means actions inside tables, bullet sublists, or quotes still match. It also means strike-through or highlight cues are lost, so treat the regex as the single source of truth.- The ID embeds the task text. If someone edits an action’s wording, the
edited version is treated as a new row and the old one stays at
openin the sheet. For long-lived tasks, prefer the append-only model and close stale rows by hand, or move to UUIDs written back into the Doc. - Status moves only forward in this script — it never closes a row when
the
[ ]becomes[x]in the Doc. Add a reconciliation pass if you want two-way sync.
Related
Generate personalized study guides from notes
Reformat raw notes into structured study guides — for Northwind's internal training programme.
Updated Feb 8, 2026
Build a contract-clause assembly system
Construct Northwind agreements from a library of approved clauses — drag-drop in code.
Updated Feb 1, 2026
Translate and resolve Doc comments
Localise reviewer feedback on a shared Doc so multilingual teams can collaborate.
Updated Jan 25, 2026
Auto-archive finalized Docs to dated folders
File completed Northwind Docs by month so the active folder stays focused on in-flight work.
Updated Jan 18, 2026
Build a fillable intake form inside a Doc
Create structured intake forms with placeholder fields readers can fill — for client briefs.
Updated Jan 11, 2026