GASとGemini APIで業務を自動化する方法【2026年・メール・スプレッドシート・フォーム対応コード付き】

「毎日100通のメールを仕分けする時間がもったいない」「売上データの分析レポートを毎週手作業で作っている」――そんな繰り返しの業務を、Google Apps Script(GAS)とGemini APIの組み合わせで自動化できます。

GASはGoogleのサービス(Gmail・スプレッドシート・フォームなど)に直接アクセスできるスクリプト環境です。そこにGeminiのAI判断力を組み合わせると、「読んで・判断して・返信する」という人間だけができると思われていた作業を自動化できます。しかも費用はGemini APIの従量課金のみ。小規模なら実質ゼロに近いコストで実現できます。

この記事ではKAIREが実際に中小企業へ導入してきた3つのユースケース(Gmail自動仕分け・スプレッドシート分析・フォーム自動返信)について、そのままコピーして使えるコード付きで解説します。2026年5月時点の最新Gemini APIに対応した内容です。

この記事でわかること
  • GAS×Gemini APIでできる3つの業務自動化ユースケース
  • Gemini APIキーの取得方法とGASへの安全な設定手順
  • GASからGemini APIを呼び出す基本コード(そのまま使える)
  • Gmail自動仕分け・スプレッドシート分析・フォーム返信の完全実装コード
  • エラーハンドリング・レート制限・セキュリティの実践的対処法

Google Workspace×Geminiの社内研修に興味がある方は、GWS×Gemini研修のページをご覧ください。70名規模の導入実績をもとにカスタムプログラムを提供しています。

目次

GAS×Gemini APIで何ができるか(3つのユースケース概要)

Google Apps ScriptとGemini APIを組み合わせると、「Googleサービスのデータ操作」+「AIによる判断・文章生成」が一つのスクリプトで完結します。従来のGASだけでは「ルールベースの条件分岐」しかできませんでしたが、Geminiを加えることで自然言語の理解や文章の生成が可能になります。

ユースケース①:Gmail自動仕分けと返信ドラフト生成

受信メールの件名・本文をGeminiに渡し、「見積依頼・クレーム・一般問い合わせ」などのカテゴリに分類させます。同時に返信ドラフトをAIが生成し、Gmailの下書きに自動保存します。担当者は届いたドラフトを確認して送信するだけ。1通あたり2〜3分かかっていた初期対応が実質ゼロになります。

ユースケース②:スプレッドシートの売上データをGeminiが分析

毎週手動で作っていた売上サマリーレポートを自動化します。スプレッドシートから最新データを取得し、Geminiに「先週比・前月比・異常値のコメント付き要約」を生成させて、別シートに自動出力します。月曜朝にスプレッドシートを開くと、AIが書いた分析レポートが完成している状態を作れます。

ユースケース③:Googleフォーム回答への自動返信生成

お問い合わせフォームや申し込みフォームへの回答が届いたとき、内容に応じたパーソナライズされた返信メールをGeminiが自動生成してGmailで送信します。定型文の「お問い合わせありがとうございます」から脱却し、相手の質問内容に具体的に答えた返信を即時送れます。

この3つのユースケースに共通しているのは、「繰り返しの判断作業」をAIに委譲するという発想です。完全自動化ではなく「AIが下書きを作り、人間が確認して送る」という半自動化のフローが、現実的かつリスクが低い導入方法です。

事前準備:Gemini APIキーの取得とGASへの設定

GAS×Gemini API自動化を始めるには、まずGemini APIキーを取得してGASのスクリプトプロパティに設定します。APIキーをコードに直書きすることは絶対に避けてください。GitHubや共有ドライブに誤ってアップロードした瞬間にセキュリティ事故になります。

ステップ1:Google AI StudioでAPIキーを取得する

  • Google AI Studio(aistudio.google.com)にアクセスしてGoogleアカウントでサインイン
  • 左メニューの「Get API key」→「Create API key」をクリック
  • プロジェクトを選択(既存のGCPプロジェクト、またはAI Studio専用プロジェクトを新規作成)
  • 生成されたAPIキー(AIzaSy...から始まる文字列)をコピーして安全な場所に一時保管

2026年現在、Gemini 1.5 Flash は無料枠(1分間15リクエスト・1日1,500リクエスト)が提供されています。中小企業の業務自動化であれば、多くのケースで無料枠の範囲内に収まります。

ステップ2:GASのスクリプトプロパティにAPIキーを保存する

GASのエディタ(script.google.com)を開き、以下の手順でAPIキーをスクリプトプロパティに保存します。スクリプトプロパティはコードとは別の安全な領域に保存され、スクリプトを共有しても他のユーザーには見えません。

  • GASエディタ上部の「プロジェクトの設定」(歯車アイコン)をクリック
  • 「スクリプト プロパティ」セクションまでスクロール
  • 「スクリプト プロパティを追加」をクリック
  • プロパティ名:GEMINI_API_KEY、値:先ほどコピーしたAPIキーを入力
  • 「スクリプト プロパティを保存」をクリック

保存後は元のAPIキーの文字列を手元のメモから削除してください。以降はGASのコードからPropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY')で取得します。

ステップ3:必要な権限スコープを確認する

GASは初回実行時に権限の承認を求めます。今回の自動化では以下のスコープが必要です。不要なスコープを要求するスクリプトはセキュリティリスクになるため、実装するユースケースに必要なものだけを使います。

  • Gmail自動仕分け:https://mail.google.com/(Gmail全体アクセス)
  • スプレッドシート分析:https://www.googleapis.com/auth/spreadsheets
  • フォーム自動返信:https://www.googleapis.com/auth/forms.currentonly + Gmail
  • 外部API呼び出し(Gemini):https://www.googleapis.com/auth/script.external_request

基本コード:GASからGemini APIを呼び出す

すべての実装の土台となる、GASからGemini APIを呼び出す基本関数を作ります。この関数を一度書いておくと、Gmail自動仕分け・スプレッドシート分析・フォーム返信のどのケースでも再利用できます。

基本呼び出し関数

/**
 * Gemini APIを呼び出す基本関数
 * @param {string} prompt - Geminiに渡すプロンプト
 * @param {string} [model] - 使用するモデル名(省略時はgemini-1.5-flash)
 * @returns {string} Geminiの応答テキスト
 */
function callGeminiAPI(prompt, model) {
  // スクリプトプロパティからAPIキーを取得(コードに直書きしない)
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  if (!apiKey) {
    throw new Error('GEMINI_API_KEY がスクリプトプロパティに設定されていません');
  }

  const modelName = model || 'gemini-1.5-flash';
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;

  const payload = {
    contents: [
      {
        parts: [{ text: prompt }]
      }
    ],
    generationConfig: {
      temperature: 0.3,      // 業務用途は低め(一貫性重視)
      maxOutputTokens: 1024  // 必要に応じて調整
    }
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // エラー時も例外を投げずレスポンスを返す
  };

  const response = UrlFetchApp.fetch(url, options);
  const statusCode = response.getResponseCode();

  if (statusCode !== 200) {
    const errorText = response.getContentText();
    throw new Error(`Gemini API エラー (HTTP ${statusCode}): ${errorText}`);
  }

  const data = JSON.parse(response.getContentText());

  // レスポンス構造の検証
  if (!data.candidates || data.candidates.length === 0) {
    throw new Error('Gemini API からの応答が空です');
  }

  return data.candidates[0].content.parts[0].text;
}

この基本関数のポイントは3つです。

  • APIキーはスクリプトプロパティから取得:コードに直書きしない
  • muteHttpExceptions: true:APIエラー時に例外で処理が止まらないようにし、エラー内容を取得できる
  • temperature: 0.3:業務自動化では創造性より一貫性が重要なため低めに設定

動作確認用テスト関数

基本関数が正しく動くか確認するために、以下のテスト関数を実行してみてください。GASエディタでtestGeminiConnectionを選択して「実行」ボタンを押します。

/**
 * Gemini API接続テスト
 * GASエディタから直接実行して動作確認する
 */
function testGeminiConnection() {
  try {
    const result = callGeminiAPI('「こんにちは」と日本語で一言だけ返してください。');
    Logger.log('接続成功: ' + result);
    SpreadsheetApp.getUi && SpreadsheetApp.getUi().alert('接続成功!\n' + result);
  } catch (e) {
    Logger.log('接続失敗: ' + e.message);
  }
}

ログに「接続成功: こんにちは」のような応答が返れば準備完了です。次のセクションから各ユースケースの実装に入ります。

実装①:Gmail自動仕分け・返信ドラフト生成

最初の実装は、受信メールをGeminiが自動分類して返信ドラフトを作成する機能です。KAIREが中小企業で最初に導入することが多い自動化で、導入初日から効果を実感しやすいユースケースです。

設計思想:完全自動返信ではなく「ドラフト生成」

メールの自動返信はリスクが伴います。誤った内容を自動送信してしまうと信頼を損ないます。そのため、このスクリプトではGeminiが返信文を下書き(ドラフト)として保存し、人間が確認してから送信するフローを採用しています。

完全自動化は「フォームの受付確認メール」など定型度が高い場合にのみ使います(実装③で解説)。

完全実装コード

/**
 * 未読メールを自動分類して返信ドラフトを生成する
 * トリガー設定推奨:時間主導型、1時間おき
 */
function processUnreadEmails() {
  // 直近1日以内の未読メールを取得(検索条件は必要に応じて変更)
  const threads = GmailApp.search('is:unread newer_than:1d -label:ai-processed');

  if (threads.length === 0) {
    Logger.log('処理対象メールなし');
    return;
  }

  Logger.log(`処理対象スレッド数: ${threads.length}`);

  // 処理済みラベルを取得(なければ作成)
  let processedLabel = GmailApp.getUserLabelByName('ai-processed');
  if (!processedLabel) {
    processedLabel = GmailApp.createLabel('ai-processed');
  }

  threads.forEach((thread, index) => {
    try {
      // レート制限対策:API呼び出しの間隔を空ける
      if (index > 0) {
        Utilities.sleep(1500); // 1.5秒待機
      }

      const messages = thread.getMessages();
      const latestMessage = messages[messages.length - 1]; // 最新メッセージを取得
      const subject = latestMessage.getSubject() || '(件名なし)';
      // 本文は先頭500文字に制限(トークン節約・個人情報流出防止)
      const body = latestMessage.getPlainBody().substring(0, 500);
      const senderEmail = latestMessage.getFrom();

      const prompt = `
あなたは日本の中小企業の丁寧なビジネスメール対応担当者です。
以下のメールを分析し、JSON形式で結果を返してください。

件名:${subject}
差出人:${senderEmail}
本文(先頭500字):
${body}

以下のJSONのみを返してください(マークダウン記法不要):
{
  "category": "見積依頼|クレーム|一般問い合わせ|採用|スパム|その他",
  "priority": "high|medium|low",
  "summary": "メール内容の1行要約(30字以内)",
  "draft_reply": "返信下書き本文(件名・宛名・署名は除く。300字以内)"
}

categoryの判断基準:
- 見積依頼:価格・費用・見積もりに関する問い合わせ
- クレーム:不満・苦情・問題報告
- 採用:求人・採用・インターンシップ関連
- スパム:明らかな広告・勧誘メール
- 一般問い合わせ:上記以外のサービス・業務に関する質問
`;

      const rawResult = callGeminiAPI(prompt);

      // JSONパースを試みる
      let result;
      try {
        // Geminiがマークダウンのコードブロックで囲む場合があるので除去
        const cleanJson = rawResult.replace(/```json\n?|\n?```/g, '').trim();
        result = JSON.parse(cleanJson);
      } catch (parseError) {
        Logger.log(`JSONパースエラー(スレッド: ${subject}): ${parseError.message}`);
        Logger.log('生レスポンス: ' + rawResult);
        result = { category: 'その他', priority: 'medium', summary: '解析失敗', draft_reply: '' };
      }

      // ラベル付け
      const labelName = result.category;
      let categoryLabel = GmailApp.getUserLabelByName(labelName);
      if (!categoryLabel) {
        categoryLabel = GmailApp.createLabel(labelName);
      }
      thread.addLabel(categoryLabel);

      // 優先度の高いメールにはスターを付ける
      if (result.priority === 'high') {
        latestMessage.star();
      }

      // 返信ドラフト生成(スパムと採用は除く)
      if (result.draft_reply && result.category !== 'スパム' && result.category !== '採用') {
        const draftBody = `${result.draft_reply}\n\n---\n(このドラフトはAIが生成しました。送信前に内容を確認してください)`;
        latestMessage.createDraftReply(draftBody);
      }

      // 処理済みラベルを付けて再処理を防ぐ
      thread.addLabel(processedLabel);

      Logger.log(`処理完了: "${subject}" → カテゴリ:${result.category} 優先度:${result.priority}`);

    } catch (e) {
      Logger.log(`エラー(スレッド処理中): ${e.message}`);
      // エラーが起きても次のスレッドの処理を継続する
    }
  });

  Logger.log('全スレッドの処理が完了しました');
}

トリガーの設定方法

このスクリプトは手動実行でも動きますが、自動化するにはGASのトリガーを設定します。GASエディタ左メニューの「トリガー」(時計アイコン)→「トリガーを追加」から以下を設定してください。

  • 実行する関数:processUnreadEmails
  • イベントのソース:時間主導型
  • 時間ベースのトリガーのタイプ:時間ベースのタイマー
  • 時間の間隔:1時間おき(業務時間帯のみに絞る場合は「特定の時間帯」を選択)

初回実行時は権限の承認が必要です。ポップアップが出たら「許可」をクリックしてください。

実装②:スプレッドシートの売上データをGeminiが分析

週次・月次の売上レポート作成は、多くの中小企業でExcel・スプレッドシートを使った手作業が続いています。このスクリプトはスプレッドシートのデータを読み取り、Geminiが分析コメント付きのサマリーを自動生成して別シートに書き出す機能です。

前提となるスプレッドシート構成

以下のシート構成を前提にしています。列名は実際のデータに合わせて変更してください。

  • シート名「売上データ」:A列=日付、B列=商品名、C列=売上金額、D列=担当者名
  • シート名「AIレポート」:分析結果の出力先(なければスクリプトが自動作成)

完全実装コード

/**
 * スプレッドシートの売上データをGeminiが分析してレポートシートに出力する
 * トリガー設定推奨:毎週月曜日の午前8時
 */
function analyzeSalesData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  // 売上データシートを取得
  const dataSheet = ss.getSheetByName('売上データ');
  if (!dataSheet) {
    Logger.log('エラー: 「売上データ」シートが見つかりません');
    return;
  }

  // データの取得(ヘッダー行を除く全データ)
  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) {
    Logger.log('データが存在しません');
    return;
  }

  const data = dataSheet.getRange(2, 1, lastRow - 1, 4).getValues();

  // 直近7日分のデータのみを抽出
  const today = new Date();
  const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);

  const recentData = data.filter(row => {
    const rowDate = new Date(row[0]);
    return rowDate >= sevenDaysAgo && rowDate <= today;
  });

  if (recentData.length === 0) {
    Logger.log('直近7日分のデータがありません');
    return;
  }

  // Geminiに渡すデータをテキスト形式に整形
  // 個人情報・社外秘データを含む場合はこの部分で匿名化処理を追加
  const dataText = recentData.map(row => {
    const dateStr = Utilities.formatDate(new Date(row[0]), 'Asia/Tokyo', 'yyyy/MM/dd');
    return `${dateStr}, ${row[1]}, ${row[2]}円, ${row[3]}`;
  }).join('\n');

  // 集計値の計算
  const totalSales = recentData.reduce((sum, row) => sum + Number(row[2]), 0);
  const avgDailySales = Math.round(totalSales / 7);

  // 商品別集計
  const productSales = {};
  recentData.forEach(row => {
    const product = row[1];
    productSales[product] = (productSales[product] || 0) + Number(row[2]);
  });
  const productSalesText = Object.entries(productSales)
    .sort((a, b) => b[1] - a[1])
    .map(([name, amount]) => `${name}: ${amount.toLocaleString()}円`)
    .join('\n');

  const prompt = `
あなたはビジネスアナリストです。以下の売上データを分析し、日本語で経営者向けの週次レポートを作成してください。

【集計期間】${Utilities.formatDate(sevenDaysAgo, 'Asia/Tokyo', 'yyyy/MM/dd')} ~ ${Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd')}
【総売上】${totalSales.toLocaleString()}円
【1日平均売上】${avgDailySales.toLocaleString()}円
【商品別売上】
${productSalesText}

【詳細データ(日付, 商品, 金額, 担当者)】
${dataText}

以下の形式で分析レポートを作成してください:

## 週次売上サマリー
(3〜4文で今週の全体傾向を説明)

## 好調・注目点
(売上が高い商品・担当者・曜日パターンなど、ポジティブな発見を3点)

## 改善・注意点
(売上が低い時間帯・商品、異常値があれば言及。3点)

## 来週への提言
(データから導き出せる具体的なアクション提案を2点)
`;

  const report = callGeminiAPI(prompt, 'gemini-1.5-flash');

  // レポートシートへの出力
  let reportSheet = ss.getSheetByName('AIレポート');
  if (!reportSheet) {
    reportSheet = ss.insertSheet('AIレポート');
  }

  // 既存内容をクリアして最新レポートを書き込む
  reportSheet.clearContents();

  const reportDate = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm');
  reportSheet.getRange('A1').setValue(`AI売上分析レポート(生成日時: ${reportDate})`);
  reportSheet.getRange('A1').setFontSize(14).setFontWeight('bold');

  // レポート本文を書き込む(セルA3から)
  reportSheet.getRange('A3').setValue(report);
  reportSheet.getRange('A3').setWrap(true);
  reportSheet.setColumnWidth(1, 600); // 列幅を広げて読みやすく

  // 生成完了の通知(任意)
  const ownerEmail = Session.getEffectiveUser().getEmail();
  GmailApp.sendEmail(
    ownerEmail,
    `【自動レポート】週次売上分析が完了しました(${reportDate})`,
    `AIによる週次売上分析レポートが生成されました。\n\nスプレッドシートの「AIレポート」シートをご確認ください。\n\n${report}`
  );

  Logger.log('売上分析レポートの生成が完了しました');
}


/**
 * 月次レポート版(前月データを分析する)
 * トリガー設定推奨:毎月1日の午前7時
 */
function analyzeMonthlyData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dataSheet = ss.getSheetByName('売上データ');
  if (!dataSheet) return;

  const today = new Date();
  // 前月の初日と末日を計算
  const firstDayLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
  const lastDayLastMonth = new Date(today.getFullYear(), today.getMonth(), 0);

  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) return;

  const data = dataSheet.getRange(2, 1, lastRow - 1, 4).getValues();

  const monthlyData = data.filter(row => {
    const rowDate = new Date(row[0]);
    return rowDate >= firstDayLastMonth && rowDate <= lastDayLastMonth;
  });

  if (monthlyData.length === 0) {
    Logger.log('前月のデータがありません');
    return;
  }

  const totalSales = monthlyData.reduce((sum, row) => sum + Number(row[2]), 0);
  const monthStr = Utilities.formatDate(firstDayLastMonth, 'Asia/Tokyo', 'yyyy年M月');

  const dataText = monthlyData.map(row => {
    const dateStr = Utilities.formatDate(new Date(row[0]), 'Asia/Tokyo', 'yyyy/MM/dd');
    return `${dateStr}, ${row[1]}, ${row[2]}円`;
  }).join('\n');

  const prompt = `
${monthStr}の売上データ(合計: ${totalSales.toLocaleString()}円)を分析し、
経営者向けの月次レポートを以下の構成で日本語で作成してください。

【データ】
${dataText}

## 月次サマリー
## 前月比・トレンド分析
## 重点改善提案(具体的に3点)
## 翌月の売上目標提案(根拠付きで)
`;

  const report = callGeminiAPI(prompt, 'gemini-1.5-pro'); // 月次は精度重視でProを使用

  let reportSheet = ss.getSheetByName('AIレポート') || ss.insertSheet('AIレポート');
  const reportDate = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd');

  // 月次レポートは既存内容の下に追記
  const nextRow = reportSheet.getLastRow() + 2;
  reportSheet.getRange(nextRow, 1).setValue(`【月次レポート】${monthStr}(生成: ${reportDate})`);
  reportSheet.getRange(nextRow, 1).setFontSize(12).setFontWeight('bold').setBackground('#e8f4f8');
  reportSheet.getRange(nextRow + 1, 1).setValue(report).setWrap(true);

  Logger.log(`${monthStr}の月次レポートを生成しました`);
}

このコードの重要な設計ポイントは、データをGeminiに渡す前に「直近7日分のみ」に絞っている点です。全期間のデータをそのままプロンプトに入れると、トークン数が爆発的に増加してコストが跳ね上がります。分析に必要な範囲のデータだけを渡すことがコスト管理の基本です。

社外秘データを扱う際の注意点

売上データには個人名・顧客名などの機密情報が含まれることがあります。Gemini APIに送信するデータについて、以下の方針を社内で確認してください。

  • 顧客名・担当者の本名をAPIに送信しない場合は、コード内で匿名化(例:担当者A、担当者B)して渡す
  • Google Workspace の利用規約上、入力データはモデルのトレーニングに使用されない(2024年以降のAPI利用規約)
  • 機密レベルが高いデータはVertex AI(GCP上のGemini)経由での処理を検討する

実装③:Googleフォーム回答への自動返信生成

お問い合わせフォームへの回答が届いたとき、内容に応じたパーソナライズされた返信メールをGeminiが生成してGmailから自動送信します。「ありがとうございます。担当者が確認の上ご連絡します」という定型文から脱却し、相手の質問に具体的に答えた返信を即座に送れるユースケースです。

フォームとスクリプトの連携方法

Googleフォームには「フォーム送信時」のトリガーが用意されています。フォームに紐づいたスプレッドシートからではなく、フォーム自体のスクリプトエディタにコードを書くのが確実です。

  • Googleフォームを開く → 右上「︙」メニュー →「スクリプトエディタ」
  • 以下のコードを貼り付けてスクリプトプロパティにGEMINI_API_KEYを設定
  • トリガーを追加:「フォームから」「フォーム送信時」にonFormSubmitを設定

完全実装コード

/**
 * Googleフォーム送信時に自動返信メールを生成して送信する
 * トリガー:フォーム送信時(自動で設定)
 * @param {Object} e - フォーム送信イベントオブジェクト
 */
function onFormSubmit(e) {
  // フォームの回答データを取得
  const itemResponses = e.response.getItemResponses();

  // 回答を「質問: 回答」の形式でテキスト化
  const responseText = itemResponses.map(item => {
    return `${item.getItem().getTitle()}: ${item.getResponse()}`;
  }).join('\n');

  // メールアドレスの取得(フォームで「メールアドレスを収集する」を有効化しておく)
  const respondentEmail = e.response.getRespondentEmail();
  if (!respondentEmail) {
    Logger.log('回答者のメールアドレスが取得できませんでした');
    return;
  }

  // フォームのタイトルを取得(返信メールの件名に使用)
  const formTitle = FormApp.getActiveForm().getTitle();

  const prompt = `
あなたは丁寧なビジネスメール担当者です。
以下のお問い合わせに対して、誠実で具体的な返信メールの本文を作成してください。

【フォーム名】${formTitle}
【お問い合わせ内容】
${responseText}

【返信メール作成の条件】
- 冒頭に「この度はお問い合わせいただきありがとうございます。」から始める
- 相手の質問・要望に具体的に回答する(内容が不明な場合は確認事項を明示)
- 次のアクション(いつ誰が何をするか)を明確に伝える
- 丁寧すぎる敬語は避け、読みやすい文体にする
- 署名は除く(後でシステムが追加する)
- 400字以内で簡潔にまとめる
`;

  let replyBody;
  try {
    replyBody = callGeminiAPI(prompt);
  } catch (apiError) {
    Logger.log(`Gemini API エラー: ${apiError.message}`);
    // APIエラー時はフォールバックの定型文を使用
    replyBody = `この度はお問い合わせいただきありがとうございます。\nご回答内容を確認の上、担当者よりご連絡いたします。\nしばらくお待ちくださいますようお願い申し上げます。`;
  }

  // 署名を追加
  const signature = `\n\n---\n株式会社KAIRE\nhttps://kaire.jp\n(このメールはAIにより自動生成されました)`;
  const fullBody = replyBody + signature;

  // メール送信
  GmailApp.sendEmail(
    respondentEmail,
    `Re: ${formTitle}のお問い合わせへのご返信`,
    fullBody,
    {
      name: 'KAIRE サポートチーム', // 差出人表示名
      replyTo: 'info@kaire.jp'      // 返信先アドレス
    }
  );

  // 管理者への通知(任意)
  const adminEmail = 'info@kaire.jp';
  GmailApp.sendEmail(
    adminEmail,
    `[フォーム通知] ${formTitle}への回答がありました`,
    `回答者: ${respondentEmail}\n\n【回答内容】\n${responseText}\n\n【送信した自動返信】\n${replyBody}`
  );

  Logger.log(`自動返信を送信しました: ${respondentEmail}`);
}


/**
 * フォームの送信トリガーをプログラムから設定するヘルパー関数
 * 初回セットアップ時に一度だけ実行する
 */
function setupFormTrigger() {
  // 既存のトリガーを削除(重複防止)
  ScriptApp.getProjectTriggers().forEach(trigger => {
    if (trigger.getHandlerFunction() === 'onFormSubmit') {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  // フォーム送信時トリガーを作成
  ScriptApp.newTrigger('onFormSubmit')
    .forForm(FormApp.getActiveForm())
    .onFormSubmit()
    .create();

  Logger.log('フォーム送信トリガーを設定しました');
}

フォールバック設計が重要な理由

コード中のcatchブロックで定型文にフォールバックしている点に注目してください。GeminiのAPIが一時的に利用できない場合でも、「返信ゼロ」にならないようにするのが業務システムとしての最低ラインです。APIエラーで何も送信されないと、問い合わせした顧客が不安になります。

自動返信の最後に「このメールはAIにより自動生成されました」と付記している点も重要です。AI生成であることを開示することは、現在の倫理基準として推奨されています。

エラーハンドリングとデバッグのコツ

GAS×Gemini APIの自動化で最も多いトラブルは、①レート制限エラー、②JSONパースエラー、③タイムアウトの3つです。それぞれの対処法を解説します。

①レート制限エラー(429 Too Many Requests)

Gemini APIには無料枠の制限(1分間15リクエスト)があります。メール処理など複数のAPIコールが連続する場合は必ず待機時間を入れてください。

/**
 * レート制限対応付きのGemini API呼び出し関数
 * 失敗時に自動リトライ(最大3回)
 */
function callGeminiAPIWithRetry(prompt, maxRetries) {
  maxRetries = maxRetries || 3;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = callGeminiAPI(prompt);
      return result;
    } catch (e) {
      const isRateLimitError = e.message.includes('429') || e.message.includes('RESOURCE_EXHAUSTED');
      const isLastAttempt = attempt === maxRetries;

      if (isLastAttempt) {
        // 最終試行でも失敗した場合は例外を再スロー
        throw new Error(`Gemini API: ${maxRetries}回リトライ後も失敗。${e.message}`);
      }

      if (isRateLimitError) {
        // レート制限の場合は長めに待機(指数バックオフ)
        const waitMs = Math.pow(2, attempt) * 2000; // 4秒→8秒→16秒
        Logger.log(`レート制限エラー。${waitMs}ms待機後にリトライ(${attempt}/${maxRetries})`);
        Utilities.sleep(waitMs);
      } else {
        // その他のエラーは短く待機
        Utilities.sleep(1000);
        Logger.log(`エラー発生。リトライ中(${attempt}/${maxRetries}): ${e.message}`);
      }
    }
  }
}

②JSONパースエラー

GeminiにJSON形式で返答を求めても、必ずしも純粋なJSONが返るとは限りません。マークダウンのコードブロック(```json ... ```)で囲まれて返ってくることがあります。以下のクリーニング関数を使ってください。

/**
 * GeminiのレスポンスからJSONを安全に抽出する
 * @param {string} rawResponse - Geminiの生レスポンス
 * @returns {Object} パースされたJSONオブジェクト
 */
function parseGeminiJSON(rawResponse) {
  // パターン1: そのままパースを試みる
  try {
    return JSON.parse(rawResponse.trim());
  } catch (e1) {}

  // パターン2: マークダウンコードブロックを除去してパース
  try {
    const cleaned = rawResponse
      .replace(/^```(?:json)?\s*/m, '')  // 開始の```jsonまたは```を除去
      .replace(/\s*```\s*$/m, '')         // 末尾の```を除去
      .trim();
    return JSON.parse(cleaned);
  } catch (e2) {}

  // パターン3: {から}の間だけを抽出してパース
  try {
    const match = rawResponse.match(/\{[\s\S]*\}/);
    if (match) {
      return JSON.parse(match[0]);
    }
  } catch (e3) {}

  // すべて失敗した場合はエラーログを出してnullを返す
  Logger.log('JSONパース失敗。生レスポンス: ' + rawResponse);
  return null;
}


// 使用例
function exampleUsage() {
  const prompt = 'テスト商品の情報をJSON形式で返してください: {"name": "", "price": 0}';
  const raw = callGeminiAPI(prompt);
  const result = parseGeminiJSON(raw);

  if (result) {
    Logger.log('商品名: ' + result.name);
    Logger.log('価格: ' + result.price);
  } else {
    Logger.log('JSONの解析に失敗しました');
  }
}

③GAS実行時間の上限(6分)とタイムアウト対策

GASの無料版は1回の実行時間が最大6分に制限されています。大量のメールや行数の多いスプレッドシートを一度に処理しようとするとタイムアウトになります。

/**
 * 実行時間を監視して、上限に近づいたら安全に終了する
 * 大量データ処理時に使用する
 */
function processBatchWithTimeLimit() {
  const startTime = new Date().getTime();
  const MAX_RUNTIME_MS = 5 * 60 * 1000; // 5分(6分上限の1分前に終了)

  const threads = GmailApp.search('is:unread');
  let processedCount = 0;

  for (let i = 0; i < threads.length; i++) {
    // 実行時間チェック
    const elapsedMs = new Date().getTime() - startTime;
    if (elapsedMs > MAX_RUNTIME_MS) {
      Logger.log(`実行時間上限に近づいたため終了。処理済み: ${processedCount}件`);
      // 次回の継続処理のために進捗をスクリプトプロパティに保存
      PropertiesService.getScriptProperties().setProperty('LAST_PROCESSED_INDEX', String(i));
      break;
    }

    try {
      // ここに各スレッドの処理を書く
      // (processUnreadEmailsの内部処理と同様)
      processedCount++;
      Utilities.sleep(1500); // レート制限対策
    } catch (e) {
      Logger.log(`エラー (スレッド${i}): ${e.message}`);
    }
  }

  Logger.log(`処理完了: ${processedCount}件`);
}

デバッグのコツ:Logger vs console.log

GASではconsole.log()ではなくLogger.log()を使います。実行後に「実行数」ログからログを確認できます。Stackdriver Logging(Cloud Logging)を有効にすると過去のトリガー実行ログも参照できます。また、機密情報(APIキー、メール本文、顧客名など)はログに出力しないように注意してください。

セキュリティ:APIキー管理とアクセス制御

業務自動化スクリプトはメール・スプレッドシート・フォームなど重要なデータにアクセスします。セキュリティの基本を押さえておかないと、APIキーの漏洩や意図しないデータ共有が起きます。

ルール①:APIキーは絶対にコードに直書きしない

最も重要なルールです。コードにAPIキーを直書きすると、スクリプトを誰かと共有した瞬間にAPIキーが漏洩します。必ずPropertiesService.getScriptProperties()から取得してください。

// ❌ 絶対にやってはいけない
const apiKey = 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXX';

// ✅ 正しい方法
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');

// 複数のAPIキーを管理する場合の例
function getAPIKeys() {
  const props = PropertiesService.getScriptProperties();
  return {
    gemini: props.getProperty('GEMINI_API_KEY'),
    slack: props.getProperty('SLACK_WEBHOOK_URL'),
    // 他のAPIキーも同様に管理
  };
}

ルール②:スクリプトの共有設定を最小化する

GASプロジェクトは「共有」設定があります。自動化スクリプトを社内の複数人と共有する場合は、以下の原則を守ってください。

  • スクリプトの共有は「閲覧者」権限で行い、「編集者」権限は必要最小限の管理者のみに付与
  • スクリプトプロパティ(APIキーを含む)はスクリプトの閲覧者には表示されないが、編集者には見える点に注意
  • チームでの運用には専用のGoogleアカウント(サービスアカウント)を使うことを検討する

ルール③:OAuthスコープを最小化する

GASは必要なサービスにアクセスするためにOAuthスコープを要求します。コード内で使用しているサービスだけのスコープに留めてください。appsscript.jsonで明示的にスコープを制限することで、意図しない広範囲アクセスを防げます。

// appsscript.json でスコープを明示的に制限する例
// GASエディタの「プロジェクトの設定」→「appsscript.jsonを表示」を有効化して編集

/*
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.modify",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.scriptapp"
  ]
}
*/

// Gmail自動仕分けのみの場合はSpreadsheetのスコープは不要
// スコープを最小化するとOAuth承認時にユーザーへの表示権限が少なくなり安全

ルール④:ログに機密情報を出力しない

GASの実行ログはGoogleのサーバーに保存されます。以下の情報はログに出力しないでください。

  • APIキーの値(存在確認のみ行う)
  • メール本文の全文(デバッグ時は先頭50字まで)
  • 顧客のメールアドレスや個人情報
  • 社内機密に相当するスプレッドシートのデータ
// ❌ 機密情報をそのままログに出力
Logger.log('APIキー: ' + apiKey);
Logger.log('メール本文: ' + emailBody);

// ✅ 安全なログ出力
Logger.log('APIキー取得: ' + (apiKey ? '成功' : '失敗(未設定)'));
Logger.log('処理したメール件名: ' + subject); // 件名は問題なし
Logger.log('処理件数: ' + processedCount);

ルール⑤:APIキーを定期的にローテーションする

Google AI StudioでAPIキーを新しく生成し、スクリプトプロパティを更新した後に古いキーを削除するローテーションを、3〜6ヶ月に一度実施することを推奨します。誰かがスクリプトの編集権限を持っていた期間に漏洩した可能性がある場合は、即座にローテーションしてください。

まとめ:GAS×Geminiは「ゼロコスト自動化」の最強コンビ

この記事で解説した3つの実装をまとめます。

  • Gmail自動仕分け・返信ドラフト生成:受信メールをGeminiが分類してラベル付けし、返信下書きを自動作成。担当者は確認して送信するだけ
  • スプレッドシート売上データ分析:週次・月次の売上データをGeminiが自動分析し、コメント付きレポートをシートに出力。月曜朝にAIレポートが出来上がっている状態を実現
  • フォーム自動返信生成:問い合わせ内容に応じたパーソナライズ返信をGeminiが生成してGmailから即時送信。APIエラー時のフォールバックも実装済み

GASとGemini APIの組み合わせが「ゼロコスト自動化の最強コンビ」である理由は3つです。

第一に、GASはGoogleサービスのデータに直接アクセスできるため、他のサービスとの連携に追加コストがかかりません。GmailもスプレッドシートもフォームもGoogleが提供しているので、APIの認証や接続設定が最小限で済みます。

第二に、Gemini 1.5 Flashの無料枠は中小企業の業務量をほぼカバーできるレベルです。1日1,500リクエスト・1分間15リクエストという制限は、数十〜数百通のメール処理には十分です。月間コストが数百円から始められるのは、他のAI APIにはない強みです。

第三に、GASはJavaScriptで書けるため技術ハードルが低いです。プログラミング経験がある担当者なら、この記事のコードをベースに自社の業務フローに合わせたカスタマイズが比較的容易に行えます。

一方で、注意点もあります。完全自動化は慎重に設計する必要があります。特にメールの自動送信は誤動作時のリスクが高いため、まずは「ドラフト生成→人間確認→送信」というハイブリッドフローから始めることを推奨します。自動化の範囲を少しずつ広げながら、信頼性を確認していくアプローチが現実的です。

KAIREでは熊本を拠点に、こうしたGAS×Gemini自動化の設計・実装支援を中小企業向けに提供しています。「コードは書けないが自動化したい」「既存のスクリプトをGemini対応に改修したい」というご相談もお気軽にどうぞ。

Google Workspace×Geminiの社内研修に興味がある方は、GWS×Gemini研修のページをご覧ください。70名規模の導入実績をもとにカスタムプログラムを提供しています。

関連記事

目次