I tried to use Workspace Studio and GAS to automatically aggregate Meet minutes, convert them to Markdown, and make them usable locally

I tried to use Workspace Studio and GAS to automatically aggregate Meet minutes, convert them to Markdown, and make them usable locally

2026.02.15

This page has been translated by machine translation. View original

Introduction

I'm kasama from the Data Business Division.
In this article, I'd like to share how I created a system that automatically converts Google Meet minutes into Markdown and synchronizes them locally using Google Workspace Studio and Google Apps Script, so they can be used 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. You can build workflows across Workspace apps like Gmail, Drive, and Sheets just by describing what you want to automate in natural language. It was announced in December 2025 and is being rolled out gradually. I'm using the Gemini Enterprise plan. Google Workspace Studio is not available for personal (free) accounts. For details on supported editions, please check the official page.

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

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

Purpose

Meeting minutes generated in Meet are scattered as Google Docs across shared drives and personal drives. When working on multiple projects simultaneously, you end up relying on Google Drive search to find these docs. By converting these minutes into Markdown with YAML frontmatter and synchronizing them locally, AI Agents like Claude Code can read them directly as context. The goal here is to make meeting minutes locally available.

Architecture

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

The process is divided into three main 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. In the flow, Gemini converts the minutes into a Markdown structure with YAML frontmatter and saves it as a new Google Doc in the monitored folder in My Drive.
  2. GAS + Drive Desktop (automatic): A GAS time trigger checks for new and updated files in the monitored folder. It converts targeted Google Docs to Markdown using Drive API v3 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 locally synchronized .md files can be directly read by AI Agents like Claude Code as context.

Google Drive Folder Structure

My Drive/
├── meeting_notes_docs/    ← Monitored folder (Docs placed flat)
│   ├── ProjectA_Regular_2025-02-10.gdoc
│   ├── ProjectB_Progress_Report.gdoc
│   └── Internal_MTG_Notes.gdoc
└── meeting_notes_md/      ← Output folder (.md output)
    ├── project_a/          ← Files containing "ProjectA" in name → auto-routed
    ├── project_b/          ← Files containing "ProjectB" in name → auto-routed
    └── others/            ← No keyword match → others

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

Implementation

The 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
└── README.md                   # Setup instructions

Workspace Studio Flow

I built a three-step flow in Workspace Studio. You can access Workspace Studio here:

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

In Step 1, I set up the email trigger. When minutes are generated in Meet, a notification email with the subject "Notes:" arrives from gemini-notes@google.com a few minutes after the meeting ends. By setting this sender address and subject in the From and Subject has fields, 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, I use Ask Gemini to structure the minutes. I copy the content from the "Notes" tab of the generated minutes Doc and convert it to a Markdown structure with YAML frontmatter using the following prompt. I also set up Variables with the Drive links received in the email from Step 1. This could probably also be done with Email attachments file name, but I succeeded with Drive links so I'm using that. This variable allows Gemini to read the link to the document 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: one-line summary in Japanese
---

Then the meeting content.

Screenshot 2026-02-15 at 8.08.52

In Step 3, I use Create a doc to save the structured content as a new Google Doc in meeting_notes_docs. The filename is set to the email subject, and the Content is set to the Gemini output from Step 2. This Doc will be converted to Markdown by GAS in the next stage.

Screenshot 2026-02-15 at 8.05.52

Google Apps Script

The GAS trigger periodically executes the exportChanged function. It only processes Google Docs that have been updated since the last execution time, determining the routing destination based on keywords in the filename. After successful conversion, the original Google Doc is moved to the 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, I combine configuration loading, Markdown conversion, and file routing into a single script.
getConfig_() loads all settings from script properties. ROUTING_RULES is in a comma-separated keyword:folder_name format, determining the output subfolder based on keywords in the filename. Files that don't match any keyword are routed to the others folder.
exportAsMarkdown_() exports Google Docs to text/markdown format using the Drive API v3 REST endpoint. It gets 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 Doc to trash.
There are two main functions: exportChanged and exportAll. exportChanged is a differential export that only targets Docs updated since the last execution time (last_run_at) and is called from the time trigger. exportAll targets all files for the initial run.

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 each folder's URL.

https://drive.google.com/drive/folders/[THIS_IS_THE_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 the default code
  5. Paste the entire contents of gas-gdocs-to-markdown.js
  6. Press Ctrl+S (Mac: Cmd+S) to save

Note that functions won't be recognized until you save. After saving, the "No functions" in the top toolbar will change to a function list.

Set Script Properties

Register the following in the GAS editor's left menu "Project Settings" → "Script Properties":

Property Name Value Description
WATCH_FOLDER_ID <Monitoring folder ID> ID of the folder to monitor
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 following the official instructions.

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

Post-Deployment Verification

Workspace Studio Test Run

First, verify that 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 monitored folder (meeting_notes_docs).
After several attempts, I noticed that the line breaks in the YAML frontmatter weren't always output as intended. Please adjust the Gemini prompt as needed.
Screenshot 2026-02-14 at 16.40.23

GAS Initial Run

Select exportAll from the function dropdown in the GAS toolbar and click 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 Time 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 preferred 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 locally in VS Code or have it read by AI.
Screenshot 2026-02-14 at 17.31.09

Conclusion

With this setup, you should be able to easily summarize, analyze, and search your meeting minutes using AI Agents. I've only been operating this system for a few days, so I'll update this blog if I find any issues. I hope this is useful to someone.

Share this article

FacebookHatena blogX

Related articles