トップ > コラム > 記事

AI活用

GASとGemini APIで
スプレッドシートを自動化問い合わせ仕分けの
コード全文と手順

公開日:2026年10月22日

執筆:

GoogleスプレッドシートとGASとGemini APIで、問い合わせを自動で分類・要約する仕組み

GoogleスプレッドシートにたまるGoogleフォームの回答や問い合わせを、毎日目で読んで分類している担当者向けの記事です。

GAS(Google Apps Script)からGemini APIを呼び出すと、新しく入った行だけをAIが読み、分類・要約・確認事項を隣の列へ書き込む仕組みを作れます。

この記事では、問い合わせ一覧を自動で仕分けするコードの全文と、貼り付けてから自動実行までの手順を載せています。

あわせて、6分の実行上限、同じ行を二重に送らない工夫、業務データを送る前に決めることも説明します。

コードの書き方は、2026年10月22日にGoogleの公式ドキュメントと照合しています。弊社では、スプレッドシートの動きを模したテスト環境から実際のGemini API(gemini-3.8-flash)へ架空の問い合わせ4件を送り、4件とも分類・緊急度・要約・確認事項が書き込まれることを確かめました。

ただし、掲載したコードを実際のスプレッドシートで動かした確認はしていません。貼り付けたら、まず架空の問い合わせ数件で手動実行し、結果を見てから自動実行に切り替えてください。

この記事で分かること

  • 向いているのは、1行に1件の文章が入り、毎回同じ観点で読み分けている作業です
  • 業務の問い合わせを送るなら、無料枠のまま本番運用を始めないでください
  • 貼り付けたら、架空の問い合わせを3〜5行入れて手動で動かし、結果を確かめてから自動実行にします

GASとGeminiで
自動化できること

向いているのは、1行に1件の文章が入り、毎回同じ観点で読み分けている作業です。

GASはスプレッドシートやGmailを操作できるGoogleの無料のプログラム実行環境で、Gemini APIはGoogleの生成AIをプログラムから呼び出す窓口です。

2つを組み合わせると、「表の行を読む→AIに渡す→答えを表に書く」までを、ボタン一つや決まった時刻に動かせます。

向いている作業表の例AIに書かせる列
問い合わせの仕分けフォームの回答シート分類・緊急度・要約・確認事項
アンケート自由記述の整理回答の一覧意見の種類・要約
日報や報告の要点抜き出し日報の集計表トラブルの有無・次の対応
商品名や住所の表記ゆれの候補出し取引先・商品マスタ統一後の表記の候補

反対に、金額の計算、在庫数の集計、日付の比較のように答えが一つに決まる処理は、関数やGASの通常の処理で行います。

AIは文章を読む部分だけに使い、計算と最終判断は表と人に残すと、結果を確かめやすくなります。

AI関数で足りるかを比べる

Googleスプレッドシートには、セルに=AI()と書いて要約や分類をさせるAI関数があります(Googleのヘルプ)。

ヘルプの手順では、関数を入れたセルを選んで[生成して挿入]を押すと結果が入ります(再生成時は[更新して挿入])。対象のGoogle WorkspaceやGoogle AIのプランが必要で、1回に生成できるのは選んだ最初の350セルまで、などの条件も書かれています。

対象プランを契約していて、手元で数十件を一度要約するだけなら、コードを書かずにAI関数から試すほうが早く済みます。

やりたいこと向いている方法判断の目安
手元の表を一度だけ要約・分類したいスプレッドシートのAI関数対象プランの契約があり、件数が少ない
新しい行が入るたびに自動で処理したいGAS+Gemini API(この記事の方法)処理済みの管理と自動実行が必要
複数のシステムや承認の流れとつなぎたい専用の仕組みを開発する権限・記録・例外対応まで設計が必要

問い合わせを仕分ける表を作る

例として、Webサイトの問い合わせフォームの回答が「問い合わせ」というシートにたまる会社を想定します。

以下の表の内容と問い合わせ文は架空の設計例で、特定の会社のデータではありません。

A〜C列にはフォームの回答が入り、D〜I列はGASが書き込む列として空けておきます。

列見出し入る内容
A受付日時フォームが自動で記録
B氏名フォームが自動で記録(AIには送らない)
C問い合わせ内容フォームが自動で記録(AIに送る唯一の列)
D分類見積・料金/納期・在庫/不具合・故障/契約・請求/その他
E緊急度高/中/低/不明
F要約60字以内の要約
G確認事項担当者が確認すべきこと
H処理状態空欄=未処理、「AI済・未確認」、「エラー」
I処理日時・エラー書き込んだ日時、またはエラーの内容

例えばC列に「100個注文した場合の単価を教えてください。品番は後で送ります」と入った場合、D列に「見積・料金」、G列に「品番が未記載」のような結果が入る想定です。

H列の処理状態が空欄の行だけをAIに送るため、処理状態を保存できた行は、通常の再実行では送り直しません。

AIが書いた分類は、担当者が確認するまで「AI済・未確認」のままにしておきます。確認した担当者は、H列を「確認済」などに書き換えます。H列が空欄でなければ再送されないため、書き換えても問題ありません。

返信や見積もりの判断は人が行い、AIの結果は振り分けの目安として使います。

問い合わせ本文のC列だけをGemini APIへ送り、分類から確認事項までをD〜G列、処理状態をH列、日時やエラーをI列に書き込みます。

APIキーを作って登録する

準備は3つです。Googleアカウントと、社内で外部APIへの接続を認めているかを確認して進めます。

① AI StudioでAPIキーを作る

Google AI Studioにログインし、APIキーを作成します。APIキーは、Gemini APIを使う権利を証明する合言葉のような文字列です。

料金と無料で使える範囲は、モデルや利用状況で変わります。作成前にGemini APIの料金ページで、使うモデルの条件を確認してください。

② データの扱いを確認する

業務の問い合わせを送るなら、無料枠のまま本番運用を始めないでください。

Gemini APIの利用規約では、無料の利用分について、送った内容と回答がGoogleの製品改善に使われ、人が読む場合があると説明しています。さらに、機密情報や個人情報を無料の利用分に送らないよう求めています(Gemini API 追加利用規約、2026年4月28日更新版を確認)。

有料の利用分では、送った内容を製品改善に使わず、不正利用の検知のために一定期間記録する、と書かれています。架空の問い合わせ文で動作を試し、実データを流す前に有料枠の設定と社内の利用ルールを整えます。

規約上、有料の扱いになるのは、有効な請求先アカウントに紐づいたGoogle Cloudのプロジェクトを通じてAPIを使う場合です。実データを送る前に、AI StudioのAPIキー一覧で、登録するキーが請求先を設定したプロジェクトのものかを確かめてください。試験用に作った別プロジェクトのキーを本番に流用すると、無料枠の扱いのまま業務データを送ることになります。

③ APIキーを設定欄に登録する

APIキーはコードに直接書かず、スクリプトプロパティという設定欄に保存します。手順はApps Scriptの公式ガイドに沿っています。

  1. スプレッドシートのメニューで「拡張機能」→「Apps Script」を開きます。
  2. 左側の「プロジェクトの設定」(歯車のアイコン)を開きます。
  3. 「スクリプト プロパティ」の「スクリプト プロパティを追加」を押します。
  4. プロパティにGEMINI_API_KEY、値に作成したAPIキーを入れて保存します。

コードに直接書くと、スプレッドシートを共有した相手や、コードをコピーした先にもキーが残ります。スクリプトを編集できる人は、キーを使うコードも実行できるため、編集権限を渡す範囲がキーを扱える範囲になります。

貼り付けて使うコード全文

Apps Scriptのエディタにあるコード.gsの中身をすべて消し、次のコードを貼り付けて保存します。

呼び出し方は公式のテキスト生成のページにあるApps Scriptの例を、答えをJSONで受け取る指定は構造化出力のページにあるREST APIの例をもとにしています。

// ===== 設定(ここだけ自社の表に合わせて変更します) =====
const CONFIG = {
  SHEET_NAME: '問い合わせ',          // 処理するシート名
  HEADER_ROWS: 1,                    // 見出しの行数
  COL_TEXT: 3,                       // C列:問い合わせ内容(AIに送るのはこの列だけ)
  COL_OUT: 4,                        // D列から書き込む(D分類・E緊急度・F要約・G確認事項)
  COL_STATUS: 8,                     // H列:処理状態
  COL_LOG: 9,                        // I列:処理日時・エラー内容(必ずCOL_STATUS + 1)
  MODEL: 'gemini-3.8-flash',         // 2026年10月9日時点の公式ドキュメント記載のモデル名
  MAX_ROWS_PER_RUN: 20,              // 1回の実行で処理する最大行数
  TIME_BUDGET_MS: 3 * 60 * 1000,     // 6分の実行上限より余裕をもって止める
  CATEGORIES: ['見積・料金', '納期・在庫', '不具合・故障', '契約・請求', 'その他'],
};

const STATUS_DONE = 'AI済・未確認';
const STATUS_ERROR = 'エラー';

// 設定や上限の問題。行ごとに記録せず、実行全体を止める
class FatalError extends Error {}

const SYSTEM_PROMPT = [
  'あなたは問い合わせの一次仕分け担当です。',
  '入力された問い合わせ本文だけを根拠に、分類・緊急度・要約・確認事項を返してください。',
  '本文に書かれていない事実(金額、納期、在庫、担当者名)を補ってはいけません。',
  '判断できない項目は「不明」とし、担当者が確認すべきことを確認事項に書いてください。',
].join('\n');

// スプレッドシートを開いたときにメニューを追加します
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('AI仕分け')
    .addItem('未処理の行を処理する', 'processPendingRows')
    .addItem('10分ごとの自動実行を設定する', 'setupTrigger')
    .addToUi();
}

// 未処理の行だけをAIで仕分けします
function processPendingRows() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(1000)) {
    console.log('前回の処理が実行中のため終了します');
    return;
  }
  try {
    validateConfig_();
    const started = Date.now();
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
    if (!sheet) throw new Error('シートが見つかりません: ' + CONFIG.SHEET_NAME);
    const lastRow = sheet.getLastRow();
    if (lastRow <= CONFIG.HEADER_ROWS) return;

    const numRows = lastRow - CONFIG.HEADER_ROWS;
    const values = sheet.getRange(CONFIG.HEADER_ROWS + 1, 1, numRows, Math.max(CONFIG.COL_TEXT, CONFIG.COL_LOG)).getValues();
    let processed = 0;

    for (let i = 0; i < values.length; i++) {
      if (processed >= CONFIG.MAX_ROWS_PER_RUN) break;
      if (Date.now() - started > CONFIG.TIME_BUDGET_MS) break;

      const text = String(values[i][CONFIG.COL_TEXT - 1]).trim();
      const status = String(values[i][CONFIG.COL_STATUS - 1]).trim();
      if (!text || status !== '') continue; // 空行・処理済み・エラー行は飛ばす

      const row = CONFIG.HEADER_ROWS + 1 + i;
      try {
        const r = classifyInquiry_(text);
        sheet.getRange(row, CONFIG.COL_OUT, 1, 4)
          .setValues([[r.category, r.urgency, r.summary, r.check_points]]);
        sheet.getRange(row, CONFIG.COL_STATUS, 1, 2)
          .setValues([[STATUS_DONE, new Date()]]);
      } catch (e) {
        if (e instanceof FatalError) throw e; // 全行に影響するエラーは行に書かずに止める
        sheet.getRange(row, CONFIG.COL_STATUS, 1, 2)
          .setValues([[STATUS_ERROR, ('エラー: ' + String(e.message)).slice(0, 300)]]);
      }
      SpreadsheetApp.flush();
      processed++;
    }
    console.log('処理した行数: ' + processed);
  } finally {
    lock.releaseLock();
  }
}

// 列の移動で入力や既存結果を上書きしないよう、配置を確認します
function validateConfig_() {
  const cols = [CONFIG.COL_TEXT, CONFIG.COL_OUT, CONFIG.COL_STATUS, CONFIG.COL_LOG];
  if (cols.some(function (n) { return !Number.isInteger(n) || n < 1; })) {
    throw new FatalError('列番号は1以上の整数にしてください');
  }
  if (CONFIG.COL_LOG !== CONFIG.COL_STATUS + 1) {
    throw new FatalError('ログ列は処理状態列の直後にしてください');
  }
  const outputCols = [CONFIG.COL_OUT, CONFIG.COL_OUT + 1, CONFIG.COL_OUT + 2,
    CONFIG.COL_OUT + 3, CONFIG.COL_STATUS, CONFIG.COL_LOG];
  if (new Set(outputCols).size !== outputCols.length || outputCols.indexOf(CONFIG.COL_TEXT) !== -1) {
    throw new FatalError('入力列と結果・状態・ログの列を重ねないでください');
  }
}

// 1件の問い合わせ本文をGemini APIへ送り、決まった形の結果を受け取ります
function classifyInquiry_(text) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  if (!apiKey) throw new FatalError('スクリプトプロパティ GEMINI_API_KEY が未設定です');

  const payload = {
    systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
    contents: [{ role: 'user', parts: [{ text: '問い合わせ本文:\n' + text }] }],
    generationConfig: {
      responseFormat: {
        text: {
          // 値は大文字の列挙値。小文字の 'application/json' では400になった(2026年10月10日に確認)
          mimeType: 'APPLICATION_JSON',
          schema: {
            type: 'object',
            properties: {
              category: { type: 'string', enum: CONFIG.CATEGORIES },
              urgency: { type: 'string', enum: ['高', '中', '低', '不明'] },
              summary: { type: 'string', description: '60字以内の要約' },
              check_points: { type: 'string', description: '担当者が確認すべきこと。なければ「なし」' },
            },
            required: ['category', 'urgency', 'summary', 'check_points'],
          },
        },
      },
    },
  };

  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + CONFIG.MODEL + ':generateContent';
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  let res;
  for (let attempt = 1; attempt <= 3; attempt++) {
    res = UrlFetchApp.fetch(url, options);
    const code = res.getResponseCode();
    if (code === 200) break;
    if ((code === 429 || code >= 500) && attempt < 3) {
      Utilities.sleep(2000 * attempt); // 混雑時は少し待って再試行
      continue;
    }
    const msg = 'HTTP ' + code + ': ' + res.getContentText().slice(0, 200);
    // キー・モデル名・権限の誤りや、再試行しても続く上限超過は全行に影響する
    if ([400, 401, 403, 404, 429].indexOf(code) !== -1) throw new FatalError(msg);
    throw new Error(msg);
  }

  const data = JSON.parse(res.getContentText());
  const cand = data.candidates && data.candidates[0];
  if (!cand || !cand.content || !cand.content.parts) {
    throw new Error('回答が空です: ' + JSON.stringify(data).slice(0, 200));
  }
  const raw = cand.content.parts.map(function (p) { return p.text || ''; }).join('');
  const result = JSON.parse(raw);

  // 決めた選択肢以外が返ったら、書き込まずにエラー扱いにする
  if (CONFIG.CATEGORIES.indexOf(result.category) === -1) {
    throw new Error('想定外の分類: ' + result.category);
  }
  if (['高', '中', '低', '不明'].indexOf(result.urgency) === -1) {
    throw new Error('想定外の緊急度: ' + result.urgency);
  }
  ['category', 'urgency', 'summary', 'check_points'].forEach(function (key) {
    if (typeof result[key] !== 'string' || !result[key].trim()) {
      throw new Error('要約または確認事項が空か、文字列ではありません');
    }
    result[key] = result[key].trim();
    if (result[key].startsWith('=')) {
      throw new Error('AIの回答に数式として解釈される文字列があります');
    }
  });
  if (Array.from(result.summary).length > 60) {
    throw new Error('要約が60字を超えています');
  }
  return result;
}

// 同じ設定者の10分ごとのトリガーを1つにします(他の設定者の分は削除できません)
function setupTrigger() {
  ScriptApp.getProjectTriggers()
    .filter(function (t) { return t.getHandlerFunction() === 'processPendingRows'; })
    .forEach(function (t) { ScriptApp.deleteTrigger(t); });
  ScriptApp.newTrigger('processPendingRows').timeBased().everyMinutes(10).create();
}

自社の表に合わせて変える箇所

シート名や列の位置などの設定は、冒頭のCONFIGを変更します。分類の判断基準を調整するときはSYSTEM_PROMPTも変更します。列の位置を変えた場合は、見出しの並びと列番号がずれていないかを必ず確かめます。

設定名意味変える場面
SHEET_NAME処理するシートの名前シート名が「フォームの回答 1」などの場合
COL_TEXTAIに送る列の番号(A=1)問い合わせ内容がC列以外にある場合
COL_OUT/COL_STATUS/COL_LOG結果4列の開始位置、状態列、ログ列D〜I列がすでに使われている場合。出力4列は連続させ、状態とログの2列も隣接させる
MODEL使うGeminiのモデル名公式ドキュメントのモデル名が変わった場合
MAX_ROWS_PER_RUN1回に処理する最大行数1回の実行が長くなる場合は減らす
CATEGORIES分類の選択肢自社の担当部署や問い合わせの種類に合わせる

COL_OUTから4列を結果用に空け、COL_STATUSの直後をログ列にします。COL_LOGは必ずCOL_STATUS + 1にし、結果用の4列や入力列と重ならない配置にしてください。状態と日時は2列をまとめて書くため、ログだけを別の場所へ移動できません。

結果は読み込んだ時点の行番号に書き込みます。処理中に回答シートを並べ替えたり行を挿入したりすると、結果が別の行に入るため、回答シートは並べ替えずに使います。フォームに質問を追加すると回答の列が増えるため、そのときは列番号を設定し直してください。

AIの返却項目は、空でない文字列かを検査します。要約が60字を超えた場合や、数式と解釈される「=」で始まる文字列は書き込まず、その行をエラーにします。分類と緊急度も決めた選択肢だけを書き込みます。

分類の選択肢は、実際に振り分けている担当の単位に合わせます。選択肢にない分類が返ってきた行は書き込まずに「エラー」にするため、選択肢にない分類が表に記録されることはありません。

手動で確かめて自動実行にする

貼り付けたら、架空の問い合わせを3〜5行入れて手動で動かし、結果を確かめてから自動実行にします。

手動で1回動かす

  1. スプレッドシートを開き直すと、メニューに「AI仕分け」が追加されます。
  2. 「AI仕分け」→「未処理の行を処理する」を押します。
  3. 初回は承認の画面が出ます。外部サービスへの接続とスプレッドシートの編集を許可します。
  4. 個人のGoogleアカウントでは「このアプリは Google で確認されていません」と表示される場合があります。自分で作ったスクリプトのために出る表示なので、「詳細」を押してから、プロジェクト名へ移動するリンクを押して進みます。
  5. D〜G列に結果が入り、H列が「AI済・未確認」になったかを確認します。

承認の画面で求められる外部への接続の権限は、UrlFetchAppでGemini APIへ送るために必要なものです(UrlFetchAppの公式リファレンス)。

結果を見るときの3つの観点

最初の数件は、分類の正しさだけでなく、本文にない内容をAIが書いていないかを見ます。

確認することよくあるずれ直し方
分類が担当の振り分けと合っているか「納期」と「不具合」の両方に当てはまる問い合わせCATEGORIESの説明を足すか、どちらを優先するかを指示に書く
要約に本文にない事実がないか金額や日付を推測で書くSYSTEM_PROMPTの禁止事項に具体例を足す
確認事項が担当者の次の行動になっているか「詳細を確認」のように曖昧になる「品番」「希望納期」など確認する項目名を指示に書く

時間を決めて自動で動かす

結果に問題がなければ、「AI仕分け」→「10分ごとの自動実行を設定する」を押します。

時間主導型のトリガーは、Apps Scriptの公式ガイドで「最短1分ごと」から設定できると説明されています(インストール型トリガー)。

自動実行の設定は担当者1人だけが行います。別の編集者が設定したトリガーは、このコードでは削除できません。同じ設定者のトリガーだけを1つに整理します。

このトリガーは設定した人のアカウントで動くため、退職や異動の予定がない担当者のアカウントで設定します。設定者のアカウント停止や権限の喪失で動かなくなる場合があるため、誰が設定したかを表の近くに書き残します。担当交代時は、旧担当者がトリガーを削除してから、新担当者が設定し直します。旧担当者のトリガーが残ると、2つの設定から同じ処理が重ねて動きます。同時実行の制御と処理状態の確認で同じ行への二重書き込みは防ぎますが、実行回数と利用枠を余分に使います。

自動実行の担当が替わるとき、旧担当者がトリガーを削除し、新担当者が架空データで手動確認してから設定する順番です。
自動実行は設定者の権限で動きます。担当交代時は、旧設定の削除と新担当者の確認を行います。

実行上限と再送への対策

貼り付けたコードには、APIを1回呼び出す処理に加えて、毎日動かし続けたときに起きる問題への対策を入れています。

実行上限に合わせて区切る

Apps Scriptには、1回の実行時間や外部への接続回数に上限があります。主な値は次のとおりです(Apps Scriptの割り当て、2026年9月3日更新版を確認)。

項目個人のGoogleアカウントGoogle Workspace
1回の実行時間6分6分
カスタム関数の実行時間30秒30秒
外部への接続(UrlFetch)1日2万回1日10万回
トリガーの合計実行時間1日90分1日6時間

コードでは、1回の実行を20行まで、かつ3分を過ぎたら次の行へ進まないようにしています。残った行は次の自動実行で続きから処理するため、一度に大量の行が入っても途中で強制終了しにくくなります。3分は次の行を始める前の判定で、進行中のAPI呼び出しを中断する時間ではありません。API送信後に処理状態を保存する前に強制終了すると、その行が次回も送られる場合があります。通信の再試行も含め、API呼び出しが1回だけになることは保証しません。

過去の回答を一度に数百件流すと、個人のGoogleアカウントでは、トリガーの合計実行時間(1日90分)を使い切り、その日の自動実行が止まる場合があります。初回は手動実行を数回に分けて溜まった行を片付けてから、自動実行に切り替えてください。

なお、Googleは割り当てを予告なく変更する場合があると明記しています。自動実行を始める前に、最新の値を公式ページで確かめてください。

セルに書く関数を自作しない

GASでは、セルに=GEMINI(C2)のように書ける自作の関数も作れます。手軽ですが、毎日動かす仕組みには向きません。

自作の関数は実行時間が30秒までで、シートを開き直したときなどに再計算されると、同じ文章をもう一度APIへ送る場合があります。送るたびに料金や回数を使ううえ、AIの答えが前回と変わることもあります。

この記事のコードは、結果を値として書き込み、処理状態の列で「送ったかどうか」を管理します。結果が再計算で変わらないため、後から確認するときの材料を残せます。このコードが記録するのはAIの処理状態と日時だけです。誰が確認したかも追いたい場合は、担当者と確認日時を別の列に記録する運用を追加します。

再試行と同時実行を管理する

Gemini APIが混雑している場合や回数の上限に達した場合は、エラーの番号(429や500番台)が返ります。コードでは少し待って、最大3回まで送ります(送り直しは2回まで)。

APIキーの誤り、モデル名の誤り、送り直しても続く上限超過は、どの行を送っても同じ結果になります。この場合は行に「エラー」を書かずに実行全体を止めるため、設定の誤り一つで未処理の行がまとめてエラーになることはありません。原因はApps Scriptの「実行数」画面(実行履歴)に記録されます。

実行全体が止まっても、自動実行のトリガーは残り、次の時刻に再実行されます。トリガーの実行が失敗すると、Apps Scriptから失敗をまとめたメールが届きます(インストール型トリガーの公式ガイド)。このメールを放置すると、止まったことに誰も気づかないため、届いたら設定者がその日のうちに原因を確かめます。設定や利用枠の問題が続くときは、設定者がApps Scriptの「トリガー」画面でprocessPendingRowsを削除して停止します。原因を直し、架空データで手動確認してから、同じ設定者が自動実行を設定し直してください。

答えの形がおかしいなど、その行だけの問題は「エラー」と書いて次の行へ進みます。

エラーになった行は自動では送り直しません。原因を確かめたうえで、H列とI列を消すと次の実行で再処理されます。失敗した行を無条件に送り続けて、料金や回数だけが増える事態を防ぐためです。

また、手動実行と自動実行が重なると同じ行を2回処理するおそれがあるため、LockServiceで同時に1つしか動かないようにしています。

空行と処理済みの行を除外し、正常な回答だけを書き込みます。行のエラーと全体の設定エラーは対応を分け、担当者が停止や再処理を行います。

業務データを送る
前に決めること

コードが動いても、何を送ってよいかが決まっていないと実データでは使えません。社内で次の4点を決めてから本番の表につなぎます。

決めることこの記事の例理由
AIに送る列問い合わせ内容の1列だけ氏名や連絡先は分類に不要なため送らない
使う利用枠有料枠で、利用規約を確認済み無料枠では送った内容が製品改善に使われる
AIの結果の使い道担当の振り分けの目安だけ返信内容や見積もりの判断は人が行う
管理する人トリガーとAPIキーを設定した担当者止まったときに気づき、直せる人を決めておく

問い合わせ本文そのものに、名前や電話番号が書かれている場合もあります。送る列を絞っても本文の中身までは選べないため、送ってよい内容かどうかを社内のルールで先に決めます。AIへ渡す資料と使ってよい範囲の決め方は、中小企業のAI導入の進め方でも扱っています。

仕分けた問い合わせへの返信をAIで下書きする場合は、二重返信の防ぎ方を問い合わせメールの返信を生成AIで効率化する方法で説明しています。

よくあるエラーと直し方

動かないときは、まずApps Scriptの「実行数」画面(実行履歴)のログを見ます。設定や上限の問題はログに、その行だけの問題はI列に記録されます。

表示される内容考えられる原因直し方
GEMINI_API_KEY が未設定ですスクリプトプロパティの名前の打ち間違いプロパティ名をGEMINI_API_KEYと完全に一致させる
HTTP 400 や 403、404APIキーの誤り、モデル名の誤り、送る形式の変更キーを貼り直し、公式ドキュメントのモデル名と書き方を確認する。特定の行だけで続く場合は、その行のH列に「保留」と入れて先へ進める。「保留」の行は以後自動では処理されないため、担当者が原文を読んで手で振り分け、原因が直ったらH列とI列を消して再処理する
HTTP 429短時間に送りすぎた、または利用枠の上限MAX_ROWS_PER_RUNを減らし、利用枠の条件を確認する
シートが見つかりませんSHEET_NAMEとシート名の不一致シート名を空白まで含めて一致させる
要約または確認事項が空か、文字列ではありませんAIの答えに項目が欠けていたH列とI列を消して再処理し、続く場合は指示文を見直す
想定外の分類選択肢と違う答えが返ったその行の本文を見て、CATEGORIESか指示文を見直す
何も書き込まれないH列に何か入っている、C列が空未処理にしたい行のH列とI列を消す

モデル名や送る形式は、Gemini APIの更新で変わる場合があります。動かなくなったら、まずコードのMODELと公式ドキュメントのApps Scriptの例を見比べてください。なお、mimeTypeは'APPLICATION_JSON'と大文字で書きます。弊社が試したときは、小文字の'application/json'では400になりました。

公式ドキュメントは、新しく作るなら後継のInteractions APIを勧めており、ページによってはその例が先に表示されます。この記事のコードが使うgenerateContentは、公式に「引き続き完全にサポートされる」と書かれた書き方です(Interactions APIの案内)。見比べるときは、generateContentの例を開いてください。

自作と外部への相談を比べる

問い合わせの仕分けのように、1つのシートの中で完結する作業なら、この記事のコードを土台に自社で始められます。

まずは架空の問い合わせ5件で分類の選択肢を固め、次に有料枠と社内ルールを整え、最後に自動実行へ切り替える順番がおすすめです。この順番なら、実データを送る前に結果のずれと情報の扱いを確かめられます。

一方で、仕分けた結果を顧客管理の仕組みへ登録したい、担当者へチャットで通知したい、返信の承認までつなぎたい、となると、権限・記録・例外の扱いを含めた設計が必要になります。シートをまたいで複数の仕組みをつなぐ段階が、自作と外部の支援を比べる目安です。

どこまで自社で作り、どこから任せるかは、AI開発の内製・外注の判断表も参考にしてください。まるっとAIでは、いま使っているスプレッドシートやGoogle Workspaceを活かしたまま、AIに任せる範囲と人が確認する工程を整理し、仕組みの開発から運用まで月額でお引き受けしています。

よくある質問

Q. プログラミングの経験がなくても、GASとGeminiの連携はできますか?

A. 記事のコードを貼り付け、冒頭の設定を自社の表に合わせて変えるだけなら、プログラミングの経験がなくても試せます。

ただし、エラーの内容を読んで直す作業や、権限の承認画面の判断は必要です。

社内に相談できる人がいない場合は、架空のデータで十分に試してから実データにつないでください。

Q. Gemini APIは無料で使えますか?

A. Gemini APIには無料で使える利用枠がありますが、対象モデルや上限は料金ページで確認してください。

無料枠では、送った内容と回答がGoogleの製品改善に使われ、人が読む場合があると利用規約に書かれています。

業務の問い合わせや個人情報を送る場合は、有料枠と社内ルールを整えてから使います。

Q. セルの自作関数(=GEMINI()など)とは何が違いますか?

A. 自作のセル関数は手軽ですが、実行時間が30秒までで、再計算のたびに同じ文章を送り直す場合があります。

この記事の方法は、結果を値として書き込み、処理状態の列で送ったかどうかを管理します。

毎日新しい行が増える表を自動で処理するなら、値として書き込む方法が向いています。

Q. ChatGPTのAPIでも同じことはできますか?

A. GASのUrlFetchAppから外部のAPIを呼び出す仕組みは同じなので、ChatGPTのAPIでも作れます。

ただし、送り先のURL、APIキーの渡し方、送る形式、答えの取り出し方が違うため、記事のコードはそのままでは動きません。

利用規約とデータの扱いも、使うサービスごとに確認してください。

Q. AIの分類が間違っていた場合はどうなりますか?

A. 記事のコードでは、AIが書いた行を「AI済・未確認」にします。

その後、担当者が確認します。

間違いが多い分類は、選択肢の名前や指示文に判断の基準を足して直します。

返信や見積もりの判断は、AIの分類だけで決めずに人が行ってください。

Q. フォームの回答が1日に数百件ある場合も動きますか?

A. 記事のコードは1回に20行まで処理し、残りは次の自動実行で続きから処理します。

10分ごとの6回すべてで20行を処理できた場合は、計算上1時間120行です。

APIの応答時間やエラー、実行の遅れによって少なくなります。

それ以上の件数がある場合は、Apps ScriptとGemini APIの上限を確認し、処理の間隔や1回の行数を調整するか、別の仕組みを検討します。

無料相談

スプレッドシートの業務をAIで自動化したい方へ

いま使っている表とGoogle Workspaceを活かしたまま、AIに任せる作業と人が確認する工程を整理します。

この記事の執筆者と編集部

執筆

Web制作・デザイン出身。AIを使ったアプリ開発と業務自動化を、設計から実装まで担当しています。自ら開発・運用する経験をもとに、仕事でAIを使う手順と注意点を解説します。

プロフィールを見る

編集・検証

まるっとAI編集部

Googleスプレッドシートを使い続けながら、生成AIに任せる範囲と人が確認する工程を分けて自動化する方法を説明しています。