公開日:2026年10月22日
執筆:西山 聡明

Google
GAS
この
あわせて、
コードの
ただし、
この
向いているのは、
GASは
2つを
| 向いている作業 | 表の例 | AIに |
|---|---|---|
| 問い合わせの仕分け | フォームの | 分類・緊急度・要約・確認事項 |
| アンケート自由記述の整理 | 回答の一覧 | 意見の |
| 日報や | 日報の集計表 | トラブルの |
| 商品名や | 取引先・商品マスタ | 統一後の |
反対に、
AIは
Google=AI()と
ヘルプの
対象プランを
| やりたいこと | 向いている方法 | 判断の目安 |
|---|---|---|
| 手元の | スプレッドシートのAI関数 | 対象プランの |
| 新しい | GAS+Gemini API | 処理済みの |
| 複数の | 専用の | 権限・記録・例外対応まで |
例として、
以下の
A〜C列には
| 列 | 見出し | 入る内容 |
|---|---|---|
| A | 受付日時 | フォームが |
| B | 氏名 | フォームが |
| C | 問い合わせ内容 | フォームが |
| D | 分類 | 見積・料金/納期・在庫/不具合・故障/契約・請求/その他 |
| E | 緊急度 | 高/中/低/不明 |
| F | 要約 | 60字以内の要約 |
| G | 確認事項 | 担当者が |
| H | 処理状態 | 空欄=未処理、 |
| I | 処理日時・エラー | 書き込んだ |
例えば
H列の
AIが
返信や

準備は
Google AI Studioに
料金と
業務の
Gemini APIの
有料の
規約上、
APIキーは
GEMINI_API_KEY、コードに
Apps Scriptのコード.gsの
呼び出し方は
// ===== 設定(ここだけ自社の表に合わせて変更します) =====
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 | 処理する | シート名が |
| COL_TEXT | AIに | 問い合わせ内容が |
| COL_OUT | 結果 | D〜I列が |
| MODEL | 使う | 公式ドキュメントの |
| MAX_ROWS_PER_RUN | 1回に | 1回の |
| CATEGORIES | 分類の選択肢 | 自社の |
COL_OUTからCOL_STATUSのCOL_LOGは必ずCOL_STATUS + 1にし、
結果は
AIの
分類の
貼り付けたら、
承認の
最初の
| 確認すること | よくあるずれ | 直し方 |
|---|---|---|
| 分類が | 「納期」と | CATEGORIESの |
| 要約に | 金額や | SYSTEM_PROMPTの |
| 確認事項が | 「詳細を | 「品番」 |
結果に
時間主導型の
自動実行の
この

貼り付けた
Apps Scriptには、
| 項目 | 個人の | Google Workspace |
|---|---|---|
| 1回の実行時間 | 6分 | 6分 |
| カスタム関数の実行時間 | 30秒 | 30秒 |
| 外部への | 1日2万回 | 1日10万回 |
| トリガーの | 1日90分 | 1日6時間 |
コードでは、
過去の
なお、
GASでは、セルに=GEMINI(C2)のように
自作の
この
Gemini APIが
APIキーの
実行全体がprocessPendingRowsを
答えの
エラーに
また、

コードが
| 決めること | この記事の例 | 理由 |
|---|---|---|
| AIに送る列 | 問い合わせ内容の1列だけ | 氏名や |
| 使う利用枠 | 有料枠で、 | 無料枠では |
| AIの | 担当の | 返信内容や |
| 管理する人 | トリガーと | 止まった |
問い合わせ本文
仕分けた
動かない
| 表示される内容 | 考えられる原因 | 直し方 |
|---|---|---|
| GEMINI_API_KEY が | スクリプト | プロパティ名をGEMINI_API_KEYと |
| HTTP 400 や 403、404 | APIキーの | キーを |
| HTTP 429 | 短時間に | MAX_ROWS_PER_RUNを |
| シートが | SHEET_NAMEと | シート名を |
| 要約または | AIの | H列と |
| 想定外の分類 | 選択肢と | その |
| 何も | H列に | 未処理に |
モデル名やMODELとmimeTypeは'APPLICATION_JSON'と'application/json'では
公式ドキュメントは、
問い合わせの
まずは
一方で、
どこまで
よくある質問
Q. プログラミングの
A. 記事の
ただし、
社内に
Q. Gemini APIは
A. Gemini APIには
無料枠では、
業務の
Q. セルの
A. 自作の
この
毎日新しい
Q. ChatGPTの
A. GASの
ただし、
利用規約と
Q. AIの
A. 記事の
その後、
間違いが
返信や
Q. フォームの
A. 記事の
10分ごとの
APIの
それ以上の
無料相談
スプレッドシートの
いま

編集・検証
まるっとAI編集部
Google