I tried automating the aggregation and Markdown conversion of Meet minutes with Workspace Studio and GAS for local use

I tried automating the aggregation and Markdown conversion of Meet minutes with Workspace Studio and GAS for local use

2026.02.15

This page has been translated by machine translation. View original

Introduction

I am kasama from the Data Business Division.
In this post, I'd like to create a system that automatically converts Meet minutes to Markdown using Google Workspace Studio and Google Apps Script, and synchronizes them locally to utilize as context for AI Agents.

Prerequisites

Google Workspace Studio

Google Workspace Studio is a no-code platform for creating, managing, and sharing AI agents on Google Workspace. By simply describing what you want to automate in natural language, you can build workflows between Workspace apps like Gmail, Drive, Sheets, etc. It was announced in December 2025 and is being rolled out gradually. I am using the Gemini Enterprise plan. Google Workspace Studio cannot be used with personal accounts (free). For details on supported editions, please check the official page.

https://workspace.google.com/studio/

https://workspaceupdates.googleblog.com/2025/12/workspace-studio.html

Objective

Minutes generated in Meet are scattered as Google Docs in shared drives or My Drive for each meeting. When working on multiple projects in parallel, you end up relying on Google Drive search to find the Docs. By converting these minutes to Markdown with YAML frontmatter and synchronizing them locally, AI Agents like Claude Code can directly read them as context. The goal this time is to make meeting minutes available for local use.

Architecture

The overall process is shown in the following two diagrams.
blog_google_workspace_stuidio_1.drawio
blog_google_workspace_stuidio_2.drawio

The whole process is divided into three major phases.

  1. Google Workspace (Automatic): First, Gemini automatically generates minutes in Meet. A few minutes after the meeting ends, a notification email arrives from gemini-notes@google.com, which triggers the Workspace Studio flow. Within the flow, Gemini converts the minutes into a Markdown structure with YAML frontmatter and saves it as a new Google Doc in a monitored folder in My Drive.
  2. GAS + Drive Desktop (Automatic): A regular GAS trigger checks for new and updated files in the monitored folder. It converts target Google Docs to Markdown using Drive API v3's Export API and routes them to project-specific subfolders based on keywords in the filenames. The converted Markdown files are automatically synchronized locally via Google Drive Desktop.
  3. Local Utilization: The .md files synchronized locally can be directly read as context by AI Agents such as Claude Code.

Google Drive Folder Structure

My Drive/
├── meeting_notes_docs/    ← Monitored folder (Docs placed flat)
│   ├── ProjectA_Regular_2025-02-10.gdoc
│   ├── ProjectB_ProgressReport.gdoc
│   └── InternalMTG_Memo.gdoc
└── meeting_notes_md/      ← Output folder (.md files)
    ├── project_a/          ← Filename contains "ProjectA" → auto-sorted
    ├── project_b/          ← Filename contains "ProjectB" → auto-sorted
    └── others/            ← Doesn't match any keywords → others

In the monitored folder, all Docs are placed flat without subfolders. The subfolders in the output folder are automatically created by GAS based on ROUTING_RULES.

Implementation

Implementation code is stored on GitHub.

https://github.com/cm-yoshikikasama/blog_code/tree/main/64_gas_gdocs_to_markdown

64_gas_gdocs_to_markdown/
├── gas-gdocs-to-markdown.js    # GAS script body
└── README.md                   # Setup instructions

Workspace Studio Flow

We build a three-step flow in Workspace Studio. Workspace Studio can be accessed from:

https://studio.workspace.google.com/

Step 1 sets up an email trigger. When minutes are generated in Meet, a notification email with the subject "Notes:" arrives from gemini-notes@google.com after the meeting ends. By setting this sender address and subject in From and Subject has, the flow is automatically triggered when minutes are generated.

Screenshot 2026-02-15 at 7.55.25
Screenshot 2026-02-15 at 7.55.43

In Step 2, Ask Gemini structures the minutes. It copies content from the "Notes" tab of the generated minutes Docs and converts it to a Markdown structure with YAML frontmatter using the following prompt. Also, in Variables, set the Drive links received in the email from Step 1. This could probably be done with Email attachments file name as well, but I'm using Drive link since it worked successfully. This is a variable for having Gemini read the document link included in the email.

Copy the document tab "メモ" and convert to Markdown.
Start with this header (one field per line):

---
date: YYYY-MM-DD
project: lowercase-name
summary: 日本語で一文要約
---

Then the meeting content.

Screenshot 2026-02-15 at 8.08.52

In Step 3, Create a doc saves the structured content as a new Google Doc in meeting_notes_docs. The filename is specified as the email subject, and the Content as Gemini's output from Step 2. This Doc will be converted to Markdown by the subsequent GAS.

Screenshot 2026-02-15 at 8.05.52

Google Apps Script

The GAS trigger regularly executes the exportChanged function. It targets only Google Docs updated since the last execution time and determines the routing destination based on keywords in the filename. After successful conversion, the original Google Docs are moved to trash.

64_gas_gdocs_to_markdown/gas-gdocs-to-markdown.js
/**
 * Load configuration from Script Properties.
 */
function getConfig_() {
  const props = PropertiesService.getScriptProperties();

  // ROUTING_RULES format: "keywordA:folder_a,keywordB:folder_b"
  const routingRaw = props.getProperty('ROUTING_RULES') || '';
  const routingRules = routingRaw
    ? routingRaw.split(',').map((rule) => {
        const [keyword, folder] = rule.split(':').map((s) => s.trim());
        return { keyword, folder };
      })
    : [];

  return {
    watchFolderId: props.getProperty('WATCH_FOLDER_ID'),
    outputFolderId: props.getProperty('OUTPUT_FOLDER_ID'),
    routingRules,
  };
}

/**
 * Determine the routing subfolder name from a file name.
 * Returns "others" if no rule matches.
 */
function resolveFolder_(fileName, routingRules) {
  for (const rule of routingRules) {
    if (fileName.includes(rule.keyword)) {
      return rule.folder;
    }
  }
  return 'others';
}

// ============================================================
// Main functions
// ============================================================

/**
 * Main: convert Docs that have been added or updated since the last run.
 */
function exportChanged() {
  const config = getConfig_();
  const props = PropertiesService.getScriptProperties();
  const lastRun = props.getProperty('last_run_at');
  const watchFolder = DriveApp.getFolderById(config.watchFolderId);
  const outputRoot = DriveApp.getFolderById(config.outputFolderId);
  const query = buildQuery_(lastRun);
  const files = watchFolder.searchFiles(query);

  let count = 0;
  while (files.hasNext()) {
    const file = files.next();
    try {
      count += processFile_(file, outputRoot, config);
    } catch (e) {
      Logger.log(`Error: ${file.getName()}`);
    }
  }

  props.setProperty('last_run_at', new Date().toISOString());
  Logger.log(count > 0 ? `Done. ${count} file(s) exported.` : 'No changes detected.');
}

/**
 * Force-export all files (for initial run).
 */
function exportAll() {
  const config = getConfig_();
  const props = PropertiesService.getScriptProperties();
  const watchFolder = DriveApp.getFolderById(config.watchFolderId);
  const outputRoot = DriveApp.getFolderById(config.outputFolderId);
  const query = 'mimeType = "application/vnd.google-apps.document"';
  const files = watchFolder.searchFiles(query);

  let count = 0;
  while (files.hasNext()) {
    const file = files.next();
    try {
      count += processFile_(file, outputRoot, config);
    } catch (e) {
      Logger.log(`Error: ${file.getName()}`);
    }
  }

  props.setProperty('last_run_at', new Date().toISOString());
  Logger.log(`Done. ${count} file(s) exported.`);
}

/**
 * Process a single file: convert to Markdown, route to subfolder, delete original.
 * @return {number} 1 on success, 0 on failure
 */
function processFile_(file, outputRoot, config) {
  const docName = file.getName();
  const subName = resolveFolder_(docName, config.routingRules);
  const outputSub = getOrCreateSubFolder_(outputRoot, subName);
  const md = exportAsMarkdown_(file.getId());
  const fileName = `${sanitizeFileName_(docName)}.md`;

  saveToFolder_(outputSub, fileName, md);
  Logger.log(`${docName}${subName}/${fileName}`);

  file.setTrashed(true);
  Logger.log(`Deleted: ${docName}`);

  return 1;
}

// ============================================================
// Search and export
// ============================================================

/**
 * Build a search query (Google Docs filtered by modified date).
 */
function buildQuery_(lastRunIso) {
  let q = 'mimeType = "application/vnd.google-apps.document"';
  if (lastRunIso) {
    q += ` and modifiedDate > "${lastRunIso}"`;
  }
  return q;
}

/**
 * Fetch Markdown text from a Google Doc via Drive API v3 REST export.
 */
function exportAsMarkdown_(docId) {
  const url = `https://www.googleapis.com/drive/v3/files/${docId}/export?mimeType=${encodeURIComponent('text/markdown')}`;
  const response = UrlFetchApp.fetch(url, {
    headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` },
  });
  return response.getContentText('UTF-8');
}

// ============================================================
// Utilities
// ============================================================

/**
 * Get an existing subfolder by name, or create one if it doesn't exist.
 */
function getOrCreateSubFolder_(parentFolder, subName) {
  const folders = parentFolder.getFoldersByName(subName);
  if (folders.hasNext()) {
    return folders.next();
  }
  return parentFolder.createFolder(subName);
}

/**
 * Save a .md file to a folder (overwrite if it already exists).
 */
function saveToFolder_(folder, fileName, content) {
  const existing = folder.getFilesByName(fileName);

  if (existing.hasNext()) {
    const file = existing.next();
    file.setContent(content);
  } else {
    folder.createFile(fileName, content, 'text/markdown');
  }
}

/**
 * Remove characters that are invalid in file names.
 */
function sanitizeFileName_(name) {
  return name.replace(/[\/\\?%*:|"<>\s]/g, '_').trim();
}

In gas-gdocs-to-markdown.js, loading configurations, Markdown conversion, and file routing are combined into a single script.
getConfig_() loads all settings from script properties. ROUTING_RULES is in the format keyword:folder_name separated by commas, determining the output subfolder based on keywords in the filename. Files that don't match any keywords are routed to the others folder.
exportAsMarkdown_() exports Google Docs to text/markdown format using Drive API v3's REST endpoint. It obtains an OAuth token with ScriptApp.getOAuthToken() and calls the API with UrlFetchApp.fetch(). The response is converted to a string with getContentText("UTF-8").

https://developers.google.com/workspace/drive/api/reference/rest/v3/files/export
processFile_() is the processing unit for a single file. It handles the flow of MD conversion → saving to the routing subfolder → moving the original Docs to trash.
There are two main functions: exportChanged and exportAll. exportChanged is a differential export targeting only Docs updated since the last execution time (last_run_at), called from a regular trigger. exportAll targets all files for initial execution.

Deployment

Create folders in Google Drive

Create a monitoring folder (meeting_notes_docs) and an output folder (meeting_notes_md) in My Drive. Copy the folder ID from the URL of each folder.

https://drive.google.com/drive/folders/[This folder ID]

Create a GAS project

  1. Open GAS (https://script.google.com)
  2. Click "New project"
  3. Enter a project name (e.g., Docs to Markdown)
  4. Delete all contents of the default code
  5. Paste all the contents of gas-gdocs-to-markdown.js
  6. Press Ctrl+S (Mac: Cmd+S) to save

Note that functions are not recognized until you save. After saving, "No functions" in the top toolbar will change to a function list.

Configure script properties

Register the following in "Project Settings" → "Script Properties" in the left menu of the GAS editor.

Property Name Value Description
WATCH_FOLDER_ID <Monitoring folder ID> ID of the target folder
OUTPUT_FOLDER_ID <Output folder ID> ID of the parent folder for .md output
ROUTING_RULES ProjectA:project_a,ProjectB:project_b Filename keyword:Output subfolder

Multiple rules can be set in ROUTING_RULES separated by commas.

Sync with Google Drive Desktop

Set up Google Drive Desktop according to the official procedure.

https://support.google.com/a/users/answer/13022292?hl=ja

Post-deployment Verification

Workspace Studio Test run

First, check if the Workspace Studio flow works correctly. Click "Test run" at the bottom of the flow screen, select an existing email, and click "Start".
Screenshot 2026-02-14 at 16.23.26

If the test is successful, a structured Google Doc will be created in the monitoring folder (meeting_notes_docs).
I tried it several times, but there were cases where the line breaks in YAML frontmatter weren't output as intended. So, please adjust the prompt to Gemini as needed.
Screenshot 2026-02-14 at 16.40.23

GAS Initial Execution

Select exportAll from the function dropdown in the GAS toolbar and execute with Run.
Screenshot 2026-02-14 at 17.12.16
Screenshot 2026-02-14 at 17.12.46

Markdown files are generated in the output destination.
Screenshot 2026-02-14 at 17.14.05
Screenshot 2026-02-14 at 17.14.20

Set up a Regular Execution Trigger

Click "Triggers" (clock icon) in the left menu of the GAS editor, then "Add Trigger" in the bottom right to configure. Set the time settings to your desired execution interval.

  • Choose which function to run: exportChanged
  • Choose which deployment should run: Head
  • Select event source: Time-driven
  • Select type of time based trigger: Day timer
  • Select time of day: 8am to 9am
  • Failure notification settings: Notify me daily

Screenshot 2026-02-15 at 7.47.09

Once output to Markdown, you can reference it in local VS Code or have AI read it.
Screenshot 2026-02-14 at 17.31.09

In Conclusion

If you've made it this far, it should be easy to have AI Agents read, summarize, analyze, and search your meeting notes. As I've only been using this for a few days, I'll update the blog if I find any issues. I hope this is helpful to someone.

Share this article

FacebookHatena blogX