Google Apps Script(GAS)は、Google Workspace の各サービスをプログラムから操作できる強力なプラットフォームです。そこに Gemini API の自然言語処理能力を組み合わせることで、これまで人手に頼っていた定型業務の多くをインテリジェントに自動化できます。
Google Workspace × Gemini API の統合アーキテクチャ
まず全体像を把握しておきましょう。GAS から Gemini API を呼び出す際の基本フローは以下のとおりです。
GAS スクリプト
↓ UrlFetchApp.fetch()
Gemini API (generateContent エンドポイント)
↓ レスポンス JSON
GAS スクリプト
↓ 各 Workspace サービス操作
Gmail / Docs / Sheets / Calendar
GAS は Google の安全なサーバー上で動作するため、APIキーをクライアントに露出させることなく安全に Gemini API を呼び出せます。また、すべての Workspace サービスへのアクセスがサーバーサイドで完結するため、OAuth の複雑な設定なしに各サービスを操作できる点も大きな利点です。
環境準備:プロジェクトのセットアップ
すべてのパターンで共通して使用するベースコードを設定します。Google AI Studio(ai.google.dev )で取得した API キーをスクリプトプロパティに保存しましょう。
// スクリプトプロパティに GEMINI_API_KEY をセット後、この関数を呼び出す
function setup () {
const props = PropertiesService. getScriptProperties ();
// 事前に [プロジェクトの設定] → [スクリプトプロパティ] で
// GEMINI_API_KEY: YOUR_GEMINI_API_KEY を設定しておく
Logger. log ( 'API Key loaded: ' + (props. getProperty ( 'GEMINI_API_KEY' ) ? 'OK' : 'NG' ));
}
// Gemini API 呼び出しの共通関数(全パターンで使用)
function callGemini ( prompt , options = {}) {
const apiKey = PropertiesService. getScriptProperties (). getProperty ( 'GEMINI_API_KEY' );
const model = options.model || 'gemini-2.5-pro' ;
const url = `https://generativelanguage.googleapis.com/v1beta/models/${ model }:generateContent?key=${ apiKey }` ;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: options.temperature || 0.7 ,
maxOutputTokens: options.maxTokens || 2048 ,
}
};
const response = UrlFetchApp. fetch (url, {
method: 'post' ,
contentType: 'application/json' ,
payload: JSON . stringify (payload),
muteHttpExceptions: true // エラー時も JSON を返す
});
const json = JSON . parse (response. getContentText ());
if (json.error) throw new Error ( `Gemini API Error: ${ json . error . message }` );
return json.candidates[ 0 ].content.parts[ 0 ].text;
}
⚠️ セキュリティの注意 : API キーはスクリプトのソースコードに直接書かず、必ずスクリプトプロパティを使用してください。プロパティはスクリプトを共有しても他者には見えません。
Gmail 自動化パターン
パターン1:受信メールのAI分類・優先度付け
受信メールを Gemini が分析し、緊急度・カテゴリ・必要なアクションを自動でラベル付けします。
function classifyInboxEmails () {
// 未読メールを最新20件取得
const threads = GmailApp. search ( 'is:unread in:inbox' , 0 , 20 );
threads. forEach ( thread => {
const message = thread. getMessages ()[ 0 ];
const subject = message. getSubject ();
const body = message. getPlainBody (). substring ( 0 , 500 ); // 先頭500文字
const from = message. getFrom ();
const prompt = `以下のメールを分析し、JSON形式で回答してください。
件名: ${ subject }
差出人: ${ from }
本文(抜粋): ${ body }
回答形式:
{
"priority": "high|medium|low",
"category": "顧客対応|社内連絡|ニュースレター|請求・契約|その他",
"action": "今日中に返信|今週中に確認|要スケジュール|読了でOK",
"summary": "30文字以内の要約"
}` ;
try {
const result = JSON . parse ( callGemini (prompt, { temperature: 0.2 }));
// 優先度に応じてラベルを付ける
const labelName = `AI分類/${ result . priority }` ;
let label = GmailApp. getUserLabelByName (labelName);
if ( ! label) label = GmailApp. createLabel (labelName);
thread. addLabel (label);
// high priority は星付き
if (result.priority === 'high' ) thread. markImportant ();
Logger. log ( `${ subject }: ${ result . priority } - ${ result . action }` );
} catch (e) {
Logger. log ( `Error classifying email "${ subject }": ${ e . message }` );
}
Utilities. sleep ( 1000 ); // レート制限対策
});
}
このスクリプトをトリガーで「毎時実行」に設定すると、受信メールが自動で整理されます。is:unread in:inbox の検索クエリは業務に合わせてカスタマイズしてください。
パターン2:AIによるメール下書き自動生成
よく届く問い合わせメールに対して、コンテキストを理解した返信下書きを自動生成します。
function draftReplyForEmail ( threadId ) {
const thread = GmailApp. getThreadById (threadId);
const messages = thread. getMessages ();
// スレッド全体の文脈を構築
const conversationHistory = messages. map ( m =>
`[${ m . getFrom () }] ${ m . getSubject () } \n ${ m . getPlainBody (). substring ( 0 , 300 ) }`
). join ( ' \n --- \n ' );
const prompt = `あなたはプロフェッショナルなビジネスパーソンです。
以下のメールスレッドに対する返信下書きを作成してください。
【メールスレッド】
${ conversationHistory }
【返信要件】
- 丁寧で簡潔な日本語ビジネス文書
- 具体的な次のアクションを明記
- 署名欄は「[署名]」とプレースホルダーで記載
- 300文字以内` ;
const draftBody = callGemini (prompt, { temperature: 0.6 });
// 下書きとして保存(送信はしない)
GmailApp. createDraft (
messages[messages. length - 1 ]. getFrom (),
`Re: ${ thread . getFirstMessageSubject () }` ,
draftBody
);
return draftBody;
}
パターン3:週次メールダイジェスト生成
週に一度、重要メールの要約レポートをスプレッドシートに記録します。
function generateWeeklyEmailDigest () {
const oneWeekAgo = new Date ();
oneWeekAgo. setDate (oneWeekAgo. getDate () - 7 );
const query = `after:${ Utilities . formatDate ( oneWeekAgo , 'UTC' , 'yyyy/MM/dd' ) } is:important` ;
const threads = GmailApp. search (query, 0 , 50 );
const summaries = threads. map ( thread => {
const msg = thread. getMessages ()[ 0 ];
return {
date: Utilities. formatDate (msg. getDate (), 'JST' , 'MM/dd' ),
from: msg. getFrom (),
subject: msg. getSubject (),
body: msg. getPlainBody (). substring ( 0 , 200 )
};
});
const prompt = `以下の重要メール${ summaries . length }件を分析し、週次ダイジェストを作成してください。
【メール一覧】
${ JSON . stringify ( summaries , null , 2 ) }
【出力形式】
## 今週の重要メール(${ summaries . length }件)
### 要対応事項
- ...
### 主なトピック
- ...
### 来週への申し送り
- ...` ;
const digest = callGemini (prompt, { maxTokens: 1024 });
// Docs に記録
const doc = DocumentApp. create ( `週次メールダイジェスト ${ Utilities . formatDate ( new Date (), 'JST' , 'yyyy-MM-dd' ) }` );
doc. getBody (). setText (digest);
Logger. log ( '週次ダイジェスト作成完了: ' + doc. getUrl ());
return doc. getUrl ();
}
Google Sheets 自動化パターン
パターン4:データ異常検知・AIコメント自動生成
スプレッドシートのデータを Gemini が分析し、異常値や注目すべき傾向を自動でコメントとして記録します。
function detectAnomaliesInSheet () {
const sheet = SpreadsheetApp. getActiveSpreadsheet (). getSheetByName ( '売上データ' );
const dataRange = sheet. getDataRange ();
const values = dataRange. getValues ();
// ヘッダーとデータを分離
const headers = values[ 0 ];
const dataRows = values. slice ( 1 ). slice ( - 30 ); // 直近30行
const prompt = `以下の売上データを分析してください。
【ヘッダー】
${ headers . join ( ', ' ) }
【データ(直近30件)】
${ dataRows . map ( row => row . join ( ', ' )). join ( ' \n ' ) }
【分析要求】
1. 平均から2σ以上外れた異常値があれば指摘
2. 直近7日間のトレンド(上昇/横ばい/下降)を判定
3. 注目すべき変化点を3点以内で列挙
4. 来週への推奨アクション
JSON形式で返してください:
{
"anomalies": [{"row": N, "column": "列名", "value": X, "reason": "理由"}],
"trend": "上昇|横ばい|下降",
"keyInsights": ["insight1", "insight2", "insight3"],
"recommendations": ["action1", "action2"]
}` ;
const result = JSON . parse ( callGemini (prompt, { temperature: 0.2 }));
// 異常値セルにコメント付与
result.anomalies. forEach ( anomaly => {
const cell = sheet. getRange (anomaly.row + 1 , headers. indexOf (anomaly.column) + 1 );
cell. setNote ( `⚠️ AI検出: ${ anomaly . reason }` );
cell. setBackground ( '#ffebee' );
});
// サマリーシートに記録
const summarySheet = SpreadsheetApp. getActiveSpreadsheet ()
. getSheetByName ( 'AI分析サマリー' ) ||
SpreadsheetApp. getActiveSpreadsheet (). insertSheet ( 'AI分析サマリー' );
summarySheet. appendRow ([
new Date (),
result.trend,
result.keyInsights. join ( ' / ' ),
result.anomalies. length + '件の異常' ,
result.recommendations. join ( ' | ' )
]);
}
パターン5:AIによる自動レポート生成
スプレッドシートのデータから、経営層向けの月次レポートを Docs に自動生成します。
function generateMonthlyReport () {
const ss = SpreadsheetApp. getActiveSpreadsheet ();
// 複数シートからデータ集計
const salesData = ss. getSheetByName ( '売上' ). getDataRange (). getValues ();
const kpiData = ss. getSheetByName ( 'KPI' ). getDataRange (). getValues ();
const prompt = `以下のビジネスデータをもとに、経営層向けの月次レポートを作成してください。
【売上データ(直近3ヶ月)】
${ salesData . slice ( 0 , 15 ). map ( r => r . join ( ', ' )). join ( ' \n ' ) }
【KPI一覧】
${ kpiData . slice ( 0 , 10 ). map ( r => r . join ( ', ' )). join ( ' \n ' ) }
【レポート要件】
- 役員が5分で読める構成
- 数値は具体的に引用
- リスクと機会を明確に分離
- 来月のアクションプランを3点提示
- 1500文字程度` ;
const reportContent = callGemini (prompt, { maxTokens: 2048 });
// Google Docs に書き出し
const month = Utilities. formatDate ( new Date (), 'JST' , 'yyyy年M月' );
const doc = DocumentApp. create ( `${ month }度 月次レポート(AI生成)` );
const body = doc. getBody ();
body. appendParagraph ( `${ month }度 月次レポート` ). setHeading (DocumentApp.ParagraphHeading. HEADING1 );
body. appendParagraph ( `生成日時: ${ new Date (). toLocaleString ( 'ja-JP' ) }` );
body. appendParagraph ( '' );
body. appendParagraph (reportContent);
// 担当者にメール通知
MailApp. sendEmail ({
to: Session. getActiveUser (). getEmail (),
subject: `【自動生成】${ month }度 月次レポートが完成しました` ,
body: `Gemini AIが月次レポートを生成しました。 \n\n レポートURL: ${ doc . getUrl () }`
});
return doc. getUrl ();
}
パターン6:問い合わせデータのAI分析・タグ付け
カスタマーサポートの問い合わせデータを一括分析し、カテゴリタグと感情スコアを自動付与します。
function analyzeCustomerFeedback () {
const sheet = SpreadsheetApp. getActiveSpreadsheet (). getSheetByName ( '問い合わせ' );
const lastRow = sheet. getLastRow ();
// タグ列が未処理の行のみ対象
for ( let i = 2 ; i <= Math. min (lastRow, 51 ); i ++ ) { // 一度に最大50行
const tagCell = sheet. getRange (i, 5 ); // E列 = タグ
if (tagCell. getValue ()) continue ; // 処理済みはスキップ
const content = sheet. getRange (i, 3 ). getValue (); // C列 = 問い合わせ内容
if ( ! content) continue ;
const prompt = `以下のカスタマー問い合わせを分析してください。
【問い合わせ内容】
${ content }
JSON形式で返してください:
{
"category": "技術的問題|使い方の質問|請求・契約|クレーム|要望・提案|その他",
"sentiment": "positive|neutral|negative",
"urgency": "high|medium|low",
"tags": ["タグ1", "タグ2"],
"suggestedResponse": "50文字以内の返答方針"
}` ;
try {
const result = JSON . parse ( callGemini (prompt, { temperature: 0.2 }));
sheet. getRange (i, 5 ). setValue (result.category);
sheet. getRange (i, 6 ). setValue (result.sentiment);
sheet. getRange (i, 7 ). setValue (result.urgency);
sheet. getRange (i, 8 ). setValue (result.tags. join ( ', ' ));
sheet. getRange (i, 9 ). setValue (result.suggestedResponse);
// 感情に応じた行の色付け
const color = result.sentiment === 'negative' ? '#ffcdd2' :
result.sentiment === 'positive' ? '#c8e6c9' : '#ffffff' ;
sheet. getRange (i, 1 , 1 , 9 ). setBackground (color);
} catch (e) {
sheet. getRange (i, 5 ). setValue ( '分析エラー' );
Logger. log ( `Row ${ i } error: ${ e . message }` );
}
Utilities. sleep ( 1500 ); // レート制限対策
}
SpreadsheetApp. flush (); // 変更を確定
}
Google Docs 自動化パターン
パターン7:会議議事録のAI要約・整形
会議の生の議事録テキストを Gemini が整形し、アクションアイテムを自動抽出します。
function formatMeetingNotes ( docId ) {
const doc = DocumentApp. openById (docId);
const rawText = doc. getBody (). getText ();
const prompt = `以下の会議議事録を整形してください。
【生の議事録】
${ rawText }
【整形後の出力形式(Markdown)】
## 会議概要
- 日時:
- 参加者:
- 目的:
## 決定事項
-
## アクションアイテム
| 担当者 | タスク | 期限 |
(テーブル不使用。リスト形式で: - 担当者: タスク(期限: 日付))
## 次回会議
- 日程:
- アジェンダ候補:
## 議事メモ(詳細)
(重要な議論のポイントを箇条書きで)` ;
const formatted = callGemini (prompt, { maxTokens: 2048 });
// 元の Doc を更新
const body = doc. getBody ();
body. clear ();
// セクションごとに書き込み(ヘッダースタイル適用)
formatted. split ( ' \n ' ). forEach ( line => {
if (line. startsWith ( '## ' )) {
body. appendParagraph (line. replace ( '## ' , '' ))
. setHeading (DocumentApp.ParagraphHeading. HEADING2 );
} else if (line. startsWith ( '### ' )) {
body. appendParagraph (line. replace ( '### ' , '' ))
. setHeading (DocumentApp.ParagraphHeading. HEADING3 );
} else if (line. trim ()) {
body. appendParagraph (line);
}
});
Logger. log ( '議事録整形完了: ' + doc. getUrl ());
}
パターン8:ドキュメント品質レビューAIアシスタント
提案書・報告書・仕様書などのドキュメントを Gemini がレビューし、改善点をコメントとして追記します。
function reviewDocument ( docId , reviewType = 'proposal' ) {
const doc = DocumentApp. openById (docId);
const content = doc. getBody (). getText ();
const reviewPrompts = {
proposal: '提案書として、説得力・具体性・リスク説明の観点でレビュー' ,
spec: '技術仕様書として、明確性・実装可能性・抜け漏れの観点でレビュー' ,
report: '報告書として、事実と意見の分離・結論の明確さの観点でレビュー'
};
const prompt = `以下のドキュメントを${ reviewPrompts [ reviewType ] }してください。
【ドキュメント】
${ content . substring ( 0 , 3000 ) }
【レビュー出力形式】
{
"overallScore": 1-10,
"strengths": ["強み1", "強み2", "強み3"],
"improvements": [
{"section": "セクション名", "issue": "問題点", "suggestion": "改善案"}
],
"summary": "100文字以内のサマリー"
}` ;
const review = JSON . parse ( callGemini (prompt, { temperature: 0.3 }));
// レビュー結果をドキュメント末尾に追記
const body = doc. getBody ();
body. appendHorizontalRule ();
body. appendParagraph ( 'AI レビュー結果' ). setHeading (DocumentApp.ParagraphHeading. HEADING2 );
body. appendParagraph ( `総合スコア: ${ review . overallScore }/10` );
body. appendParagraph ( `概評: ${ review . summary }` );
body. appendParagraph ( '強み' ). setHeading (DocumentApp.ParagraphHeading. HEADING3 );
review.strengths. forEach ( s => body. appendListItem (s));
body. appendParagraph ( '改善提案' ). setHeading (DocumentApp.ParagraphHeading. HEADING3 );
review.improvements. forEach ( item => {
body. appendListItem ( `【${ item . section }】${ item . issue } → ${ item . suggestion }` );
});
}
Google Calendar 自動化パターン
パターン9:会議アジェンダのAI自動作成
翌日の会議情報を Calendar から取得し、参加者・目的に応じたアジェンダを自動生成します。
function createMeetingAgendas () {
const tomorrow = new Date ();
tomorrow. setDate (tomorrow. getDate () + 1 );
tomorrow. setHours ( 0 , 0 , 0 , 0 );
const dayEnd = new Date (tomorrow);
dayEnd. setHours ( 23 , 59 , 59 , 999 );
const events = CalendarApp. getDefaultCalendar (). getEvents (tomorrow, dayEnd);
events. forEach ( event => {
if (event. getDescription (). includes ( '[アジェンダ作成済み]' )) return ;
const guests = event. getGuestList (). map ( g => g. getEmail ()). join ( ', ' );
const title = event. getTitle ();
const duration = (event. getEndTime () - event. getStartTime ()) / 60000 ; // 分
const prompt = `以下の会議のアジェンダを作成してください。
会議タイトル: ${ title }
参加者: ${ guests || '不明'}
所要時間: ${ duration }分
既存の説明文: ${ event . getDescription () }
【アジェンダ形式】
開始挨拶(2分)
1. [トピック名](X分)
- 目的:
- 資料:
2. [トピック名](X分)
...
まとめ・次のアクション(5分)
※合計時間が${ duration }分になるよう調整してください` ;
const agenda = callGemini (prompt, { temperature: 0.6 });
// 既存の説明文にアジェンダを追記
const updatedDescription = `${ event . getDescription () } \n\n 【AIアジェンダ(自動生成)】 \n ${ agenda } \n\n [アジェンダ作成済み]` ;
event. setDescription (updatedDescription);
Utilities. sleep ( 1000 );
});
}
パターン10:会議後アクションアイテム自動メール配信
会議終了後30分で自動起動し、議事録からアクションアイテムを抽出して担当者にメール送信します。
function sendActionItemsAfterMeeting () {
// 直近1時間以内に終了した会議を探す
const now = new Date ();
const oneHourAgo = new Date (now. getTime () - 60 * 60 * 1000 );
const events = CalendarApp. getDefaultCalendar (). getEvents (oneHourAgo, now);
events. forEach ( event => {
if (event. getDescription (). includes ( '[アクション送付済み]' )) return ;
const description = event. getDescription ();
const guests = event. getGuestList (). map ( g => g. getEmail ());
const prompt = `以下の会議メモからアクションアイテムを抽出し、担当者ごとに整理してください。
【会議: ${ event . getTitle () }】
${ description }
【参加者メールアドレス】
${ guests . join ( ', ' ) }
JSON形式で出力:
{
"actionItems": [
{
"assignee": "メールアドレス",
"task": "タスク内容",
"deadline": "YYYY-MM-DD",
"priority": "high|medium|low"
}
],
"summary": "会議の一言サマリー(50文字以内)"
}` ;
try {
const result = JSON . parse ( callGemini (prompt, { temperature: 0.2 }));
// 担当者ごとにグループ化
const grouped = {};
result.actionItems. forEach ( item => {
if ( ! grouped[item.assignee]) grouped[item.assignee] = [];
grouped[item.assignee]. push (item);
});
// 各担当者にメール送信
Object. entries (grouped). forEach (([ email , items ]) => {
const taskList = items. map ( t =>
`・${ t . task }(期限: ${ t . deadline } / 優先度: ${ t . priority })`
). join ( ' \n ' );
MailApp. sendEmail ({
to: email,
subject: `【アクションアイテム】${ event . getTitle () }` ,
body: `お疲れ様です。 \n\n ${ event . getTitle () }のアクションアイテムをお送りします。 \n\n ${ taskList } \n\n 会議概要: ${ result . summary }`
});
});
// 処理済みマーク
event. setDescription (description + ' \n\n [アクション送付済み]' );
} catch (e) {
Logger. log ( `Error processing event "${ event . getTitle () }": ${ e . message }` );
}
});
}
クロスサービス統合パターン
パターン11:週次ビジネスレポート自動生成パイプライン
Gmail・Sheets・Calendar のデータを統合し、週次サマリーレポートを自動生成する完全パイプラインです。
function generateWeeklyBusinessReport () {
const oneWeekAgo = new Date ();
oneWeekAgo. setDate (oneWeekAgo. getDate () - 7 );
// 1. Gmail から重要メール収集
const importantEmails = GmailApp. search ( 'is:important newer_than:7d' , 0 , 20 )
. map ( t => ({
subject: t. getFirstMessageSubject (),
from: t. getMessages ()[ 0 ]. getFrom ()
}));
// 2. Sheets から KPI データ取得
const ss = SpreadsheetApp. openById ( 'YOUR_SPREADSHEET_ID' );
const kpiData = ss. getSheetByName ( '週次KPI' ). getLastRow () > 1
? ss. getSheetByName ( '週次KPI' ). getRange ( 2 , 1 , 7 , 5 ). getValues ()
: [];
// 3. Calendar から完了会議リスト取得
const completedMeetings = CalendarApp. getDefaultCalendar ()
. getEvents (oneWeekAgo, new Date ())
. map ( e => e. getTitle ());
// 4. Gemini で統合分析
const prompt = `以下の1週間のビジネスデータを統合分析し、経営報告レポートを作成してください。
【重要メール(${ importantEmails . length }件)】
${ importantEmails . map ( e => `${ e . from }: ${ e . subject }` ). join ( ' \n ' ) }
【週次KPI】
${ kpiData . map ( r => r . join ( ', ' )). join ( ' \n ' ) }
【完了会議(${ completedMeetings . length }件)】
${ completedMeetings . join ( ', ' ) }
【レポート要件】
- 1週間の成果と課題を簡潔に整理
- 来週の重点事項を3点
- リスクアラートがあれば明記
- 1000文字以内` ;
const report = callGemini (prompt, { maxTokens: 1500 });
// 5. Docs にレポート保存
const week = Utilities. formatDate ( new Date (), 'JST' , 'M月d日' );
const doc = DocumentApp. create ( `週次ビジネスレポート(${ week }週)` );
doc. getBody (). setText (report);
// 6. 管理者にメール配信
MailApp. sendEmail ({
to: Session. getActiveUser (). getEmail (),
subject: `【週次レポート】${ week }週 ビジネスサマリー` ,
body: `週次レポートが自動生成されました。 \n\n ${ report } \n\n レポート: ${ doc . getUrl () }`
});
Logger. log ( '週次レポート生成完了' );
}
パターン12:カスタマーフィードバック分析・対応自動化システム
フォームからの回答を自動で分析し、緊急対応が必要なケースだけ担当者にアラートを送るシステムです。
function onFormSubmit ( e ) {
const response = e.response;
const itemResponses = response. getItemResponses ();
// フォーム回答を整形
const feedbackData = {};
itemResponses. forEach ( item => {
feedbackData[item. getItem (). getTitle ()] = item. getResponse ();
});
const prompt = `以下のカスタマーフィードバックを分析してください。
【フィードバック内容】
${ JSON . stringify ( feedbackData , null , 2 ) }
JSON形式で返してください:
{
"sentiment": "positive|neutral|negative",
"urgency": "immediate|within_24h|this_week|no_action",
"category": "bug|feature_request|complaint|praise|question",
"summary": "40文字以内の要約",
"suggestedAction": "推奨対応(60文字以内)",
"escalationNeeded": true|false
}` ;
const analysis = JSON . parse ( callGemini (prompt, { temperature: 0.1 }));
// Sheets にログ記録
const logSheet = SpreadsheetApp. openById ( 'YOUR_SPREADSHEET_ID' )
. getSheetByName ( 'フィードバックログ' );
logSheet. appendRow ([
new Date (),
feedbackData[ 'メールアドレス' ] || '匿名' ,
analysis.sentiment,
analysis.urgency,
analysis.category,
analysis.summary,
analysis.suggestedAction,
analysis.escalationNeeded ? '要エスカレーション' : 'OK'
]);
// エスカレーション判定
if (analysis.escalationNeeded || analysis.urgency === 'immediate' ) {
MailApp. sendEmail ({
to: 'support-manager@example.com' ,
subject: `🚨【緊急対応】${ analysis . category }: ${ analysis . summary }` ,
body: `緊急対応が必要なフィードバックが届きました。 \n\n ${ analysis . suggestedAction } \n\n 詳細: ${ JSON . stringify ( feedbackData , null , 2 ) }`
});
}
// 送信者への自動返信
if (feedbackData[ 'メールアドレス' ]) {
MailApp. sendEmail ({
to: feedbackData[ 'メールアドレス' ],
subject: 'フィードバックありがとうございます' ,
body: `この度はフィードバックをお寄せいただきありがとうございます。 \n 内容を確認のうえ、${ analysis . urgency === 'immediate' ? '本日中' : '5営業日以内'}にご連絡いたします。`
});
}
}
このパターンは Google Forms の「フォームが送信されたとき」トリガーと組み合わせて使用します。Google Apps Script を活用した Workspace 連携の基本については、Gemini APIとApps Scriptのクロスプロダクト連携ガイド も参照してください。
本番運用のための設計ガイド
GAS の実行時間制限への対策
Google Apps Script には1回の実行で最大6分という制限があります。大量データ処理は必ずバッチ分割してください。
// バッチ処理の実装パターン
function processBatchWithCheckpoint () {
const props = PropertiesService. getScriptProperties ();
const startRow = parseInt (props. getProperty ( 'lastProcessedRow' ) || '2' );
const batchSize = 20 ; // 1回の実行で処理する行数
const sheet = SpreadsheetApp. getActiveSpreadsheet (). getActiveSheet ();
const endRow = Math. min (startRow + batchSize, sheet. getLastRow ());
for ( let i = startRow; i <= endRow; i ++ ) {
// 処理ロジック
processRow (sheet, i);
props. setProperty ( 'lastProcessedRow' , String (i + 1 ));
// 残り時間チェック(5分超えたら停止)
// ※ Script.getRemainingTime() は Apps Script では使用不可
// 代わりに行数で制御する
}
if (endRow < sheet. getLastRow ()) {
// 続きがある場合は1分後に再トリガー
ScriptApp. newTrigger ( 'processBatchWithCheckpoint' )
. timeBased (). after ( 60000 ). create ();
} else {
props. deleteProperty ( 'lastProcessedRow' );
Logger. log ( '全行の処理が完了しました' );
}
}
レート制限・クォータ管理
Gemini API には1分あたりのリクエスト数制限があります。大量処理時は適切なスリープを挟みましょう。
// エクスポネンシャルバックオフ付きリトライ
function callGeminiWithRetry ( prompt , options = {}, maxRetries = 3 ) {
for ( let attempt = 0 ; attempt < maxRetries; attempt ++ ) {
try {
return callGemini (prompt, options);
} catch (e) {
if (e.message. includes ( '429' ) || e.message. includes ( 'RESOURCE_EXHAUSTED' )) {
const waitMs = Math. pow ( 2 , attempt) * 1000 + Math. random () * 500 ;
Logger. log ( `Rate limited. Waiting ${ waitMs }ms before retry ${ attempt + 1 }/${ maxRetries }` );
Utilities. sleep (waitMs);
} else if (attempt === maxRetries - 1 ) {
throw e; // 最終試行でもエラーなら伝播
}
}
}
}
Gemini API のクォータ管理の詳細については、Google Sheets AI データ分析完全ガイドでも実践的なパターンを紹介しています。
セキュリティベストプラクティス
// APIキーの安全な管理(スクリプトプロパティ使用)
// ❌ 危険: コードに直書き
const apiKey = 'YOUR_ACTUAL_API_KEY' ; // 絶対に禁止
// ✅ 安全: スクリプトプロパティ
const apiKey = PropertiesService. getScriptProperties (). getProperty ( 'GEMINI_API_KEY' );
// ✅ ドメイン制限付きデプロイ
// [デプロイ] → [新しいデプロイ] → [アクセスできるユーザー]
// を「自分のみ」または「組織内のユーザー」に設定する
// ✅ 実行ログの記録
function logExecution ( functionName , success , detail = '' ) {
const logSheet = SpreadsheetApp. openById ( 'LOG_SHEET_ID' ). getSheetByName ( '実行ログ' );
logSheet. appendRow ([
new Date (),
functionName,
Session. getActiveUser (). getEmail (),
success ? 'SUCCESS' : 'FAILED' ,
detail. substring ( 0 , 200 )
]);
}
詳細なエラーハンドリングパターンはGmail AI メール生産性向上ガイド でも解説しています。
公式ドキュメントには書かれていないが、運用で気づいた点
ここまでのパターンは公式ドキュメントの組み合わせで作れます。ですが、私が Dolice Labs の 4 サイト(毎日 16 本の記事を生成・配信しています)の運用補助に GAS × Gemini を実際に回してみると、ドキュメントだけでは気づけない落とし穴がいくつもありました。累計 5,000 万ダウンロードのアプリ群の問い合わせ対応にも同じ仕組みを使っていますので、そこで踏んだ実例を共有します。
1. muteHttpExceptions: true は 429 を黙って握りつぶす
共通関数 callGemini で muteHttpExceptions: true を指定すると、HTTP エラーでも例外を投げず JSON を返します。便利ですが、レート制限(429)まで「正常なレスポンス」として通過してしまうため、json.error のチェックだけでは不十分です。私の環境では毎時バッチで 50 件処理したとき、ピーク帯で約 6%(3 件前後)が 429 になっていましたが、当初はそれに気づかず「たまに分類が抜ける」とだけ感じていました。HTTP ステータスコードを明示的に見るようにして、初めて原因が分かりました。
function callGemini ( prompt , options = {}) {
const apiKey = PropertiesService. getScriptProperties (). getProperty ( 'GEMINI_API_KEY' );
const model = options.model || 'gemini-2.5-flash' ;
const url = `https://generativelanguage.googleapis.com/v1beta/models/${ model }:generateContent?key=${ apiKey }` ;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: options.temperature ?? 0.7 , maxOutputTokens: options.maxTokens || 2048 }
};
const response = UrlFetchApp. fetch (url, {
method: 'post' , contentType: 'application/json' ,
payload: JSON . stringify (payload), muteHttpExceptions: true
});
const code = response. getResponseCode (); // ← ステータスを必ず見る
if (code === 429 || code === 503 ) {
throw new Error ( `RETRYABLE_${ code }` ); // バックオフ対象として区別
}
const json = JSON . parse (response. getContentText ());
if (json.error) throw new Error ( `Gemini API Error: ${ json . error . message }` );
return json.candidates[ 0 ].content.parts[ 0 ].text;
}
RETRYABLE_ プレフィックスで「待てば直るエラー」と「待っても直らないエラー」を区別しておくと、前述の callGeminiWithRetry 側の判定が一段すっきりします。
2. Gemini はしばしば JSON をコードフェンスで包んで返す
JSON 形式を指示しても、Gemini は ```json と ``` で囲んだテキストを返すことがあります。JSON.parse() はこれを処理できず、そのまま SyntaxError になります。私の計測では、temperature: 0.2 でも全レスポンスの約 8%(記事分類タスクで 1,000 件中 80 件前後)でこのフェンス付きが混ざっていました。responseMimeType を指定できる SDK と違い、GAS の素の HTTP 呼び出しでは自衛が必要です。次のサニタイズを 1 枚挟むだけで、パース失敗はほぼゼロになりました。
function parseGeminiJson ( text ) {
// コードフェンス・前後の説明文を除去して最初の JSON オブジェクトを取り出す
const fenced = text. match ( /```(?:json) ? \s * ( [\s\S] *? )```/ );
const raw = fenced ? fenced[ 1 ] : text;
const start = raw. indexOf ( '{' );
const end = raw. lastIndexOf ( '}' );
if (start === - 1 || end === - 1 ) throw new Error ( 'No JSON object found' );
return JSON . parse (raw. slice (start, end + 1 ));
}
パターン1・4・6・10・12 の JSON.parse(callGemini(...)) は、すべて parseGeminiJson(callGemini(...)) に置き換えることをおすすめします。
3. Utilities.sleep() は 6 分の実行枠を確実に削る
レート制限対策のスリープは必要ですが、GAS の 6 分制限を消費している点を見落としがちです。パターン6(問い合わせ分析)で Utilities.sleep(1500) を 50 行に入れると、スリープだけで 75 秒、API 応答(1 件あたり実測 1.2〜2.0 秒)と合わせると 150 秒前後を消費します。私は当初 1 回の実行で 80 行を処理しようとして、6 分制限に達して途中で止まる事故を起こしました。1 回のトリガーで処理する行数は、(スリープ + 平均応答時間) × 行数 < 240 秒 を目安に逆算する のが安全です。実測の平均応答時間でこの式を埋めると、私の環境では 1 バッチ 40〜45 行が上限でした。残りはパターンのチェックポイント方式(lastProcessedRow)で次回トリガーに送ります。
4. gemini-2.5-pro を全パターンで使うと割に合わない
最初はすべて gemini-2.5-pro で組んでいましたが、メール分類(パターン1)や感情スコアリング(パターン6)のような「短い構造化出力」タスクでは、gemini-2.5-flash で精度がほとんど変わらず、応答が体感で 2〜3 倍速くなりました。私の運用では、分類・抽出系は flash、レポート生成(パターン5・11)やドキュメントレビュー(パターン8)のような「長く推論を要する出力」だけ pro、という振り分けに落ち着いています。この振り分けだけで、月次のトークンコストが体感で 4 割ほど下がりました。タスクごとに options.model を明示的に渡す設計にしておくと、後から切り替えるのが楽です。
全体を振り返って
Gemini API のパワーを GAS の Workspace 統合能力と組み合わせることで、これまで手作業で数時間かかっていたルーティン業務を数分で処理できるようになります。今日紹介したパターンを組み合わせ、自社の業務フローに合わせたカスタマイズを試してみてください。
Google Apps Script をさらに深く