Workspace Studio と GAS で Meet の議事録を自動集約・Markdown 化してローカルで活用できるようにしてみた

Workspace Studio と GAS で Meet の議事録を自動集約・Markdown 化してローカルで活用できるようにしてみた

2026.02.15

はじめに

データ事業本部のkasamaです。
今回は、Meetの議事録をAI Agentのコンテキストとして活用するために、Google Workspace Studio と Google Apps Script で Markdown に自動変換してローカルに同期する仕組みを作ってみたいと思います。

前提

Google Workspace Studio

Google Workspace Studio は、Google Workspace 上で AI エージェントを作成・管理・共有するためのノーコードプラットフォームです。自然言語で自動化したい内容を記述するだけで、Gmail や Drive、Sheets 等の Workspace アプリ間のワークフローを構築できます。2025年12月に発表され、順次ロールアウトされています。私は Gemini Enterprise プランを利用しています。個人アカウント(無料)では Google Workspace Studio は利用できません。対応エディション等の詳細は公式ページをご確認ください。

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

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

目的

Meetで生成された議事録は Google Docs として各ミーティングの共有ドライブやマイドライブに散在します。複数プロジェクトを並行で進めていると、Docsを探すのに Google Drive の検索に頼ることになります。この議事録を YAML frontmatter 付きの Markdown に変換してローカルに同期すれば、Claude Code等の AI Agentがコンテキストとして直接読み込めるようになります。議事録をローカルで活用できるようにすることが今回の目的です。

アーキテクチャ

処理の全体像は以下の2つの構成図で示します。
blog_google_workspace_stuidio_1.drawio
blog_google_workspace_stuidio_2.drawio

全体は大きく3つのフェーズに分かれます。

  1. Google Workspace(自動): まず Meet で Gemini が議事録を自動生成します。会議終了から数分後に gemini-notes@google.com から通知メールが届き、このメールをトリガーとして Workspace Studio フローが起動します。フロー内で Gemini が議事録を YAML frontmatter 付きの Markdown 構造に変換し、新しい Google Docs としてマイドライブの監視フォルダに保存します。
  2. GAS + Drive Desktop(自動): GAS の定期トリガーが監視フォルダ内の新規・更新ファイルをチェックします。対象の Google Docs を Drive API v3 の Export API で Markdown に変換し、ファイル名に含まれるキーワードに基づいてプロジェクト別のサブフォルダにルーティングします。変換された Markdown ファイルは Google Drive Desktop によってローカルに自動同期されます。
  3. ローカル活用: ローカルに同期された .md ファイルを Claude Code 等の AI Agent がコンテキストとして直接読み込めます。

Google Drive のフォルダ構成

マイドライブ/
├── meeting_notes_docs/    ← 監視フォルダ(Docs をフラットに配置)
│   ├── プロジェクトA_定例_2025-02-10.gdoc
│   ├── プロジェクトB_進捗報告.gdoc
│   └── 社内MTG_メモ.gdoc
└── meeting_notes_md/      ← 出力フォルダ(.md を出力)
    ├── project_a/          ← ファイル名に「プロジェクトA」を含む → 自動振り分け
    ├── project_b/          ← ファイル名に「プロジェクトB」を含む → 自動振り分け
    └── others/            ← どのキーワードにもマッチしない → others

監視フォルダにはサブフォルダを作らず、全ての Docs をフラットに配置します。出力フォルダのサブフォルダは ROUTING_RULES に基づいて GAS が自動作成します。

実装

実装コードは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 スクリプト本体
└── README.md                   # セットアップ手順

Workspace Studio フロー

Workspace Studio で3ステップのフローを構築します。Workspace Studio は以下から開けます。

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

Step 1 では、メールトリガーを設定します。Meetで議事録が生成されると、会議終了後に gemini-notes@google.com からNotes:という件名で通知メールが届きます。この送信元アドレスと件名をFromSubject hasに設定することで、議事録の生成を検知してフローを自動起動します。

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

Step 2 では、Ask Gemini で議事録を構造化します。生成された議事録 Docs の「メモ」タブから内容をコピーし、以下のプロンプトで YAML frontmatter 付きの Markdown 構造に変換します。またVariablesで、Step1のemailで受信したDrive linksを設定します。Email attachments file nameでも同じことはできそうですが、Drive linkで成功したのでこちらを利用しています。EmailにドキュメントへのlinkがついているのでそれをGeminiに読み取ってもらうための変数です。

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

Step 3 では、Create a doc で構造化した内容を新しい Google Docs としてmeeting_notes_docsに保存します。ファイル名はemailの件名を、ContentはStep2のGeminiの出力を指定しています。この Docs を後段の GAS が Markdown に変換します。

Screenshot 2026-02-15 at 8.05.52

Google Apps Script

GAS のトリガーが exportChanged 関数を定期実行します。前回実行時刻以降に更新された Google Docs のみを対象とし、ファイル名に含まれるキーワードでルーティング先を判定します。変換成功後、元の Google Docs はゴミ箱に移動されます。

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();
}

gas-gdocs-to-markdown.js では、設定の読み込み、Markdown 変換、ファイルルーティングを一つのスクリプトにまとめています。
getConfig_() はスクリプトプロパティから全設定を読み込みます。ROUTING_RULES はカンマ区切りの キーワード:フォルダ名 形式で、ファイル名に含まれるキーワードに応じて出力先サブフォルダを決定します。どのキーワードにもマッチしないファイルは others フォルダに振り分けられます。
exportAsMarkdown_() は Drive API v3 の REST エンドポイントで Google Docs を text/markdown 形式にエクスポートしています。ScriptApp.getOAuthToken() で OAuth トークンを取得し、UrlFetchApp.fetch() で API を呼び出します。レスポンスは getContentText("UTF-8") で文字列に変換します。

https://developers.google.com/workspace/drive/api/reference/rest/v3/files/export
processFile_() が1ファイルの処理単位です。MD 変換 → ルーティング先のサブフォルダに保存 → 元の Docs をゴミ箱に移動、という流れを処理しています。
exportChangedexportAll の2つのメイン関数があります。exportChanged は前回実行時刻(last_run_at)以降に更新された Docs のみを対象とする差分エクスポートで、定期トリガーから呼ばれます。exportAll は全ファイルを対象とする初回実行用です。

デプロイ

Google Drive でフォルダを作成

マイドライブに監視フォルダ(meeting_notes_docs)と出力フォルダ(meeting_notes_md)を作成します。各フォルダの URL からフォルダ ID をコピーします。

https://drive.google.com/drive/folders/【このフォルダID】

GAS プロジェクトを作成

  1. GAS (https://script.google.com)を開く

  2. 「新しいプロジェクト」をクリック

  3. プロジェクト名を入力(例: Docs to Markdown

  4. デフォルトコードの中身を全て削除

  5. gas-gdocs-to-markdown.js の内容を全て貼り付け

  6. Ctrl+S(Mac: Cmd+S)で保存する

ここで注意すべき点は、保存しないと関数が認識されないということです。保存すると上部ツールバーの「No functions」が関数一覧に変わります。

スクリプトプロパティを設定

GAS エディタ左メニュー 「プロジェクトの設定」 → 「スクリプト プロパティ」に以下を登録します。

プロパティ名 説明
WATCH_FOLDER_ID <監視フォルダの ID> 監視対象フォルダの ID
OUTPUT_FOLDER_ID <出力フォルダの ID> .md 出力先の親フォルダの ID
ROUTING_RULES プロジェクトA:project_a,プロジェクトB:project_b ファイル名キーワード:出力サブフォルダ

ROUTING_RULES はカンマ区切りで複数ルールを設定できます。

Google Drive Desktop で同期

公式手順に沿って、Google Drive Desktopを設定します。

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

デプロイ後確認

Workspace Studio の Test run

まずは、Workspace Studio のフローが正しく動作するか確認します。フロー画面下部の「Test run」をクリックし、既存のメールを選択して「Start」をクリックします。
Screenshot 2026-02-14 at 16.23.26

テストが成功すると、監視フォルダ(meeting_notes_docs)に構造化された Google Docs が作成されます。
何回か試しましたが、YAML frontmatter の改行が意図通りに出力されないケースがありました。ですのでGeminiへのプロンプトは必要に応じて調整してください。
Screenshot 2026-02-14 at 16.40.23

GAS 初回実行

GASのツールバーの関数ドロップダウンで exportAll を選択しRunで実行します。
Screenshot 2026-02-14 at 17.12.16
Screenshot 2026-02-14 at 17.12.46

markdownファイルが出力先に生成されています。
Screenshot 2026-02-14 at 17.14.05
Screenshot 2026-02-14 at 17.14.20

定期実行トリガーを設定

GAS エディタ左メニューの「トリガー」(時計アイコン)をクリックし、右下の「トリガーを追加」から設定します。時間設定は実行して欲しい間隔に設定します。

  • 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

markdownに出力されたら、ローカルのVS Codeで参照できたり、AI に読み込ませることができます。
Screenshot 2026-02-14 at 17.31.09

最後に

ここまでできればAI Agentに読み込ませて要約、分析、検索などが容易にできそうです。まだ運用始めて数日なので、不具合あればblogをupdateしたいと思います。どなたかの参考になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事