GASで議事録をAIで要約し
タスクを自動抽出する方法

Googleドキュメントに溜まった議事録を、DocumentApp(GASからGoogleドキュメントを読み書きする標準機能)とClaude API(Anthropic社の生成AI)で処理し、 要約・決定事項・担当者付きのToDoをスプレッドシートへ自動で書き出す方法を、そのまま動くコードで解説します。

|対象: GAS / Googleドキュメント / Claude API / 議事録管理

Table of Contents

議事録は「書いて終わり」になりやすい

結論から言うと、議事録から要約とToDoを抜き出す作業はGAS(Google Apps Script。Googleサービスを操作する無料のプログラム実行環境)と AIで自動化できます。会議のたびにドキュメントは増えるのに、「誰が・いつまでに・何をするか」だけが取り出されないまま埋もれる——この記事はその穴を塞ぐ仕組みを作ります。

作るのは、指定フォルダのGoogleドキュメントを順に読み、AIに要約とタスク抽出をさせ、 結果をスプレッドシートに追記する自動処理です。1時間おきに動かせば、議事録を書いてフォルダに置くだけで タスク一覧が自動で育ちます。AIに返させるのは次の4つです。

summary(要約)

会議全体を3行以内で。あとから会議を思い出すための手がかり

decisions(決定事項)

その場で決まったことの一覧。検討中の案とは区別する

tasks(タスク)

やること・担当者・期限の3点セット。書かれていない項目は空にする

quote(根拠の原文)

抽出のもとになった議事録中の一文。人が突き合わせて検証できる

最後のquote(根拠の原文)が要です。AIは書かれていない担当者や期限をそれらしく埋めることがあります。根拠の一文を必ず併記させれば、 その場で「本当に議事録に書いてあるか」を確かめられます。

準備:ドキュメントの読み取りとAPIキーの保管

まずスプレッドシートを1つ用意し、タスク要約ログ処理済みの3シートを作ります。そこにスクリプトを紐づけ(拡張機能 → Apps Script)、議事録用のドライブフォルダも1つ決めておきます。

ドキュメントの本文はDocumentApp.openById()で開き、getBody().getText()でプレーンテキストとして取り出せます。書式や表のセルは失われますが、AIに渡すぶんには文字さえあれば十分です。 APIキーはスクリプトプロパティ(PropertiesServiceで読み書きする、スクリプト単位の設定保存領域)に保管します。

// GoogleドキュメントのIDを指定して本文テキストを取り出す
function readDocumentText(docId) {
  const doc = DocumentApp.openById(docId);
  // getBody().getText() で本文全体をプレーンテキストとして取得する
  const text = doc.getBody().getText().trim();
  if (!text) throw new Error("本文が空です: " + docId);
  return { title: doc.getName(), text: text };
}

// APIキーはコードに直書きせず、スクリプトプロパティから読み出す
function getApiKey() {
  const key = PropertiesService.getScriptProperties()
    .getProperty("ANTHROPIC_API_KEY");
  if (!key) throw new Error("APIキーが未設定です。");
  return key;
}

議事録には人事評価や取引条件など、社外に出せない内容が含まれることがあります。外部APIに本文を送る仕組みである以上、どのフォルダを対象にするかを先に決めるのが安全です。対象フォルダを分けておけば、機密性の高い議事録を誤って送る事故を防げます。

要約・決定事項・タスクをJSONで受け取る

議事録本文をAIに渡し、結果をJSON(プログラムがそのまま読めるデータ形式)で受け取ります。 コツは、期待する形を実例として書くこと、そして除外条件を明示することです。「検討中の案や単なる感想はタスクに含めない」と書かないと、雑談まで拾ってしまいます。

// 議事録を渡し、要約・決定事項・タスクをJSONで受け取る
function extractMinutes(title, text) {
  const url = "https://api.anthropic.com/v1/messages";

  const prompt =
    "あなたは会議の議事録から情報を構造化して抜き出します。\n" +
    "次のJSONオブジェクトだけを出力してください。\n\n" +
    "形式:\n" +
    '{\n' +
    '  "summary": "会議全体の要約を3行以内で",\n' +
    '  "decisions": ["決定事項を1件ずつ"],\n' +
    '  "tasks": [\n' +
    '    {"task":"やること","assignee":"担当者名","due":"YYYY-MM-DD","quote":"根拠となった原文"}\n' +
    '  ]\n' +
    '}\n\n' +
    "ルール:\n" +
    "・JSONだけを出力する。説明文やコードブロック記号は書かない。\n" +
    "・議事録に書かれていない担当者・期限は推測せず空文字 \"\" にする。\n" +
    "・quote には、そのタスクの根拠になった議事録中の一文をそのまま入れる。\n" +
    "・検討中の案や単なる感想はタスクに含めない。\n" +
    "・決定事項が無ければ decisions は空配列にする。\n\n" +
    "議事録タイトル: " + title + "\n" +
    "議事録本文:\n" + text;

  const payload = {
    model: "claude-haiku-4-5",
    max_tokens: 3000,
    messages: [{ role: "user", content: prompt }]
  };

  const res = UrlFetchApp.fetch(url, {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": getApiKey(),
      "anthropic-version": "2023-06-01"
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // エラー時も例外にせずレスポンスを受け取る
  });

  if (res.getResponseCode() !== 200) {
    throw new Error("API エラー (" + res.getResponseCode() + "): " + res.getContentText());
  }

  const raw = JSON.parse(res.getContentText()).content[0].text;
  return parseJsonObject(raw);
}

「議事録に書かれていない担当者・期限は推測せず空文字にする」という一文が、この仕組みの信頼性を決めます。 AIは空欄を嫌い、それらしい値で埋めたがるためです。明示的に「空でよい」と許可を与えることで、 捏造を減らせます。モデルは軽量で安価なclaude-haiku-4-5から始め、抽出漏れが目立つときに上位モデルへ差し替えます。

muteHttpExceptions: trueを付けると、APIがエラーを返しても例外で止まらず、ステータスコードと本文を自分で確認できます。 原因が401(キーの誤り)なのか429(レート制限)なのかが分かるので、必ず付けてください。

AIの推測を混ぜないための検証コード

「JSONだけを出力して」と頼んでも、前後に説明文が混ざることがあります。最初の{から最後の}までを切り出してからJSON.parse()に渡します。さらに、期限が日付形式(YYYY-MM-DD)でないものは空に戻します。

// AIの返答からJSONオブジェクトだけを取り出し、形を検証する
function parseJsonObject(raw) {
  const start = raw.indexOf("{");
  const end = raw.lastIndexOf("}");
  if (start === -1 || end === -1) {
    throw new Error("JSONが見つかりません: " + raw);
  }

  const parsed = JSON.parse(raw.slice(start, end + 1));

  // 期待する形になっていない応答は使わない(後続の書き込みを守る)
  if (typeof parsed.summary !== "string") throw new Error("summary がありません");
  if (!Array.isArray(parsed.decisions)) parsed.decisions = [];
  if (!Array.isArray(parsed.tasks)) parsed.tasks = [];

  // 期限が日付形式でないものは空にして、誤ったスケジュールを防ぐ
  parsed.tasks.forEach(function (t) {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(String(t.due))) t.due = "";
  });

  return parsed;
}

「来週まで」「なるはや」といった曖昧な表現を、AIが勝手に具体的な日付へ変換することがあります。 正規表現で形式をチェックし、合わないものは空にする。間違った日付が入るくらいなら、空欄のほうが害が少ないという判断です。空欄はシートを見た人が気づいて埋められますが、もっともらしい誤った期限は誰も疑いません。

抽出結果をスプレッドシートに書き出す

タスクは1行1件で「タスク」シートへ、要約と決定事項は会議ごとに1行で「要約ログ」シートへ書き出します。 各行に元ドキュメントのURLを添えるのがポイントです。気になったタスクから原文へ1クリックで戻れます。

// 抽出結果をスプレッドシートに書き出す
// 「タスク」シート: A=会議名, B=タスク, C=担当者, D=期限, E=根拠の原文, F=ドキュメントURL
function writeTasks(docId, title, result) {
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName("タスク");
  const docUrl = "https://docs.google.com/document/d/" + docId + "/edit";

  if (result.tasks.length > 0) {
    const rows = result.tasks.map(function (t) {
      return [title, t.task, t.assignee || "", t.due || "", t.quote || "", docUrl];
    });
    // 最終行の次からまとめて追記する(1行ずつのappendRowより速い)
    sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, 6).setValues(rows);
  }

  // 要約と決定事項は別シートに1行ずつ残す
  const logSheet = ss.getSheetByName("要約ログ");
  logSheet.appendRow([
    new Date(),
    title,
    result.summary,
    result.decisions.join("\n"),
    docUrl
  ]);
}

タスクの書き込みはsetValues()でまとめて行います。1件ずつappendRow()を呼ぶより大幅に速く、実行時間制限(無料アカウントで6分)に余裕が生まれます。 なお、タスクが0件の議事録もあります。空配列をsetValues()に渡すとエラーになるため、件数が0のときは書き込みを飛ばしています。

フォルダを監視して自動で処理する

仕上げに、議事録フォルダを走査して未処理のドキュメントだけを処理する本体を作ります。 処理済みのドキュメントIDを「処理済み」シートに記録し、次回以降はスキップします。 ドキュメントIDはファイル名を変えても不変なので、キーとして安定しています。

// 指定フォルダ内の未処理ドキュメントをまとめて処理する
const FOLDER_ID = "xxxxxxxxxxxxxxxxxxxx"; // 議事録を置くフォルダのID
const MAX_DOCS_PER_RUN = 5;               // 1回の実行で処理する上限

function processMinutesFolder() {
  const done = loadProcessedIds();
  const files = DriveApp.getFolderById(FOLDER_ID)
    .getFilesByType(MimeType.GOOGLE_DOCS);

  let count = 0;
  while (files.hasNext() && count < MAX_DOCS_PER_RUN) {
    const file = files.next();
    const docId = file.getId();
    if (done[docId]) continue; // 処理済みは飛ばす

    try {
      const doc = readDocumentText(docId);
      const result = extractMinutes(doc.title, doc.text);
      writeTasks(docId, doc.title, result);
      markProcessed(docId, doc.title);
      count++;
    } catch (e) {
      // 1件の失敗で全体を止めない。未記録なので次回に再挑戦される
      console.error("処理に失敗: " + docId + " / " + e.message);
    }

    Utilities.sleep(500); // 連続呼び出しの間隔をあける
  }
}

// 「処理済み」シートのA列にドキュメントIDを蓄積する
function loadProcessedIds() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("処理済み");
  const lastRow = sheet.getLastRow();
  const map = {};
  if (lastRow < 2) return map;

  sheet.getRange(2, 1, lastRow - 1, 1).getValues().forEach(function (r) {
    map[String(r[0])] = true;
  });
  return map;
}

function markProcessed(docId, title) {
  SpreadsheetApp.getActive()
    .getSheetByName("処理済み")
    .appendRow([docId, title, new Date()]);
}

1件の失敗で全体を止めないのが設計の要点です。失敗した議事録はIDを記録しないため、 次回の実行で自動的に再挑戦されます。一時的なAPIエラーなら、放っておくだけで回復します。MAX_DOCS_PER_RUNで1回あたりの件数を絞っているのも、6分制限に引っかからないための保険です。

あとは時間主導トリガー(決まった間隔で関数を自動実行するGASの仕組み)を仕掛ければ完成です。 トリガー登録の関数は一度だけ手動で実行します。

// 1時間おきに processMinutesFolder を自動実行する
// この関数は一度だけ手動で実行する(実行のたびに重複登録されるため)
function createTrigger() {
  // 既存の同名トリガーを消してから作り直す
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === "processMinutesFolder") {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger("processMinutesFolder")
    .timeBased()
    .everyHours(1)
    .create();
}

既存の同名トリガーを消してから作り直しているのは、実行のたびにトリガーが増殖するのを防ぐためです。 これを忘れると、1時間おきの処理が2重・3重に走り、API利用料が無駄に膨らみます。

まとめ

議事録からの要約とタスク抽出は、GASとClaude APIで自動化できます。押さえるべきは5つです。 本文はDocumentAppでプレーンテキストとして取り出す。出力形式は実例と除外条件つきで指定する。 書かれていない担当者・期限は空にすると明示する。根拠の原文を併記させて検証可能にする。 処理済みIDの記録とトリガーで、置くだけで回る仕組みにする。

この記事の肝は、AIを「正解を出す装置」ではなく「下書きを作る装置」として扱っている点です。根拠の原文を残し、怪しい日付は空に戻す。人が最後に目を通す前提で作れば、 AIの推測に振り回されずに時間だけを取り戻せます。同じ型は、契約書からの条件抽出や日報からの進捗集計にも応用できます。

よくある質問

できます。Googleドライブ上のWord(.docx)やPDFは、Drive APIの変換機能でGoogleドキュメント形式にコピーしてから本文を読み取ります。ただしPDFの場合、スキャン画像だと文字が取り出せないことがあります。テキストが選択できるPDFかどうかを先に確認してください。手軽に済ませたいなら、議事録の作成段階からGoogleドキュメントに統一するのが確実です。

プロンプト(AIへの指示文)で「議事録に書かれていない場合は空文字を返す」と明示するのが基本です。本文のコードでもそう指示しています。それでも埋めてしまう場合に備え、抽出結果には必ず「発言の根拠となった原文」を一緒に返させ、シートに並べて書き出しています。人が突き合わせて確認できる状態にしておけば、推測混入に気づけます。

1回の会議の議事録であれば、そのまま送って問題になることはほとんどありません。数万文字を超えるような場合は、見出し(Heading)ごとに本文を区切って複数回に分けて送り、抽出したタスクを最後に結合します。DocumentAppでは各段落のスタイルを取得できるため、見出し単位での分割が可能です。

処理済みのドキュメントIDをスプレッドシートやスクリプトプロパティに記録し、次回以降はスキップします。本文のコードでは「処理済み」シートにドキュメントIDを蓄積し、実行時に照合しています。ドキュメントIDはファイル名を変えても変わらないため、キーとして安定しています。

議事録側の書き方を少し整えるのが最も効果的です。「決定事項」「ToDo」といった見出しがあるだけで抽出精度は上がります。プロンプト側では、期待する出力の実例を1〜2件添える(フューショット)ことと、「タスクではないもの(単なる感想や検討中の案)は含めない」と除外条件を書くことが効きます。

Googleドキュメントの読み取り、Googleドライブのファイル一覧取得、スプレッドシートへの書き込み、外部API呼び出しの4つです。初回実行時にGoogleの認可画面が出るので、内容を確認して許可します。社内アカウントで管理者が外部API呼び出しを制限している場合は、事前に情報システム部門へ確認してください。

AI×GASの業務自動化を
相談する。

議事録の要約・タスク抽出のほかにも、問い合わせ分類・翻訳・返信下書きの自動生成など、 AI APIとGASを組み合わせた自動化をご相談いただけます。既存のドキュメント運用にもそのまま組み込めます。

AI×GAS自動化の対応範囲を見る