みなさん、こんにちは!業務ハックLabのようです。
あけましておめでとうございます。
正月も関係なく、記事をアップしていきますよ~
はい、前回は設計編ということで、AIによるメール自動トリアージシステムの「設計図」を作りましたね! 今回は、その設計図をGoogle Workspace環境で実装していきます。
年末年始の休暇中に「ちょっと作ってみようかな」と思っている方、今がチャンスですよ!
はじめに:Googleのエコシステムは「速さ」が正義
前回設計した「メール自動トリアージシステム」。 これをGoogle Workspace環境で実装する場合、キーワードは「爆速」です。
Google Apps Script(GAS)はJavaScriptベースの環境であり、JSONデータとの親和性が抜群です。
今回は、無料枠で使える最新モデル「Gemini 3 Flash Preview」をAPI経由で呼び出し、Gmailに来たクレームをSlackに即時通知するボットを作成します。
アーキテクチャと使用ツール
まず、全体像を整理しましょう。
Trigger: Gmail(時間主導型トリガー)
↓
Brain: Gemini 3 Flash Preview API(Google AI Studio)
↓
Database: Google Sheets
↓
Notification: Slack(Incoming Webhook)
はい、シンプルですよね!
Googleのエコシステムは、すべてがGoogleアカウント一つで完結するので、外部サービスとの連携が驚くほど簡単なんです。

事前準備:3つのキーを揃える
実装前に、以下を用意してください。
① Gemini API Key
Google AI Studioで取得します(無料枠あり)。
- https://aistudio.google.com/ にアクセス
- 「Get API Key」をクリック
- 新しいプロジェクトを作成(または既存のプロジェクトを選択)
- APIキーをコピーして保存
はい、これで1つ目のキーが手に入りました!
(APIキーは絶対に公開しないように注意してくださいね)

② Slack Webhook URL
通知を送りたいSlackチャンネルのIncoming Webhook URLを取得します。
- Slackワークスペースにログイン
- https://api.slack.com/apps にアクセス
- 「Create New App」→「From scratch」を選択
- App Name: 英数字のみで入力(例: mail-triage-bot)
- ワークスペースを選択→「Create App」
- 左メニューから「Incoming Webhooks」をクリック
- 「Activate Incoming Webhooks」をONに切り替える
- 画面下部の「Add New Webhook to Workspace」をクリック
- 通知を送りたいチャンネルを選択→「許可する」
はい、これで2つ目のキーが手に入りました!
(Webhook URLは秘密情報なので、絶対に公開しないように注意してくださいね)

③ Google Sheets
新規作成し、1行目にヘッダーを記入します。
| 日時 | 送信者 | 件名 | 感情 | 緊急度 | カテゴリ | 要約 | ドラフト返信 |
|---|
はい、これで準備完了です!

実装:コピペで動くGASコード
では、実際にコードを書いていきましょう。
Google Sheetsを開いて、「拡張機能」→「Apps Script」からスクリプトエディタを開きます。

以下のコードを貼り付けてください。
// ===== 設定項目(ここを自分の値に書き換えてください) =====
const GEMINI_API_KEY = 'ここにGemini API Keyを入力してください';
const SLACK_WEBHOOK_URL = 'ここにSlack Webhook URLを入力してください';
const SPREADSHEET_ID = 'ここにスプレッドシートのIDを入力してください';
const SHEET_NAME = 'シート1';
// 分析対象のメール検索条件
// ⚠️ 重要: 日付制限を入れないと過去の未読メール全てが処理されます!
const SEARCH_QUERY = 'is:unread after:2025/12/30 -label:ai-processed';
// after: 今日以降のメールのみ(毎日更新する必要がある)
// -label:ai-processed 処理済みラベルが付いていないもの
// 処理済みラベル名
const PROCESSED_LABEL = 'ai-processed';
// ===== メイン処理 =====
function main() {
// 1. 処理済みラベルを取得または作成
let label = GmailApp.getUserLabelByName(PROCESSED_LABEL);
if (!label) {
label = GmailApp.createLabel(PROCESSED_LABEL);
}
// 2. Gmailからメール取得
const threads = GmailApp.search(SEARCH_QUERY, 0, 5); // 一度に処理するのは5件まで(無料枠を考慮)
if (threads.length === 0) {
console.log("未読メールはありませんでした。");
return;
}
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
threads.forEach(thread => {
const messages = thread.getMessages();
const lastMessage = messages[messages.length - 1]; // スレッドの最新のメールを対象
const body = lastMessage.getPlainBody();
// 2. Gemini APIを叩いて分析
try {
const result = callGemini(body);
// 3. スプレッドシートに保存
sheet.appendRow([
lastMessage.getDate(),
lastMessage.getFrom(),
lastMessage.getSubject(),
result.sentiment,
result.urgency,
result.category,
result.summary,
result.draft_reply
]);
// 4. 緊急度が高ければSlack通知
if (result.urgency === 'high') {
sendSlackAlert(result, lastMessage.getSubject(), lastMessage.getFrom());
}
// 5. 処理済みラベルを付けて既読にする
thread.addLabel(label);
thread.markRead();
} catch (e) {
console.error("エラーが発生しました: " + e.toString());
}
});
}
// ===== Gemini API呼び出し関数 =====
function callGemini(emailBody) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${GEMINI_API_KEY}`;
// プロンプト(第1記事目で設計したものを埋め込み)
const prompt = `
あなたは熟練したカスタマーサポートマネージャーです。
入力された「顧客からのメール」を分析し、以下のルールに従ってJSON形式で結果を出力してください。
# 分析ルール
1. **sentiment**: 顧客の感情を \`positive\`, \`neutral\`, \`negative\`, \`severe\` (激怒・法的脅迫など) の4段階で評価。
2. **urgency**: 対応の緊急度を \`high\`, \`medium\`, \`low\` の3段階で評価。
- \`high\`: システム障害、セキュリティ事故、解約の示唆、激しいクレーム
- \`medium\`: 一般的な質問、見積依頼、バグ報告
- \`low\`: 営業メール、スパム、緊急性のない報告
3. **category**: 内容を分類 (\`technical_issue\`, \`billing\`, \`feature_request\`, \`sales_spam\`, \`other\`)。
4. **summary**: メールの内容を日本語50文字以内で要約。
5. **draft_reply**: 返信メールのドラフト案(日本語)。相手の感情に配慮した丁寧な文章を作成すること。
# 制約事項
- 出力は**JSON形式のみ**とすること。Markdown記法(\`\`\`json など)は含めないでください。
- 余計な解説や挨拶文は一切不要です。
# 入力メール文面
${emailBody}
`;
const payload = {
contents: [{ parts: [{ text: prompt }] }]
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
const resultText = json.candidates[0].content.parts[0].text;
// 【重要テクニック】AIは "```json ... ```" というマークダウン記法を含めて返してくることが多いため除去する
const cleanedText = resultText.replace(/```json|```/g, "").trim();
return JSON.parse(cleanedText);
}
// ===== Slack通知関数 =====
function sendSlackAlert(data, subject, sender) {
const message = {
"text": "🚨 緊急クレーム検知",
"attachments": [
{
"color": "#ff0000",
"title": "AIが高優先度のメールを検出しました",
"fields": [
{
"title": "件名",
"value": subject,
"short": false
},
{
"title": "送信者",
"value": sender,
"short": false
},
{
"title": "AI要約",
"value": data.summary,
"short": false
},
{
"title": "カテゴリ",
"value": data.category,
"short": true
},
{
"title": "感情スコア",
"value": data.sentiment,
"short": true
}
],
"footer": "Gmail AI Triage System",
"ts": Math.floor(Date.now() / 1000)
}
]
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(message)
};
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}
はい、コード全体はこんな感じです!

コードの解説:思考プロセスを見せます
では、このコードがどう動いているのか、順を追って見ていきましょう。
ステップ1:メール取得
const threads = GmailApp.search(SEARCH_QUERY, 0, 5);
GmailApp.search()は、Gmailの検索ボックスと同じ構文が使えます。
今回使用している検索条件を詳しく見てみましょう:
const SEARCH_QUERY = 'is:unread after:2025/12/30 -label:ai-processed';
⚠️ これが超重要なポイントです!
この検索条件には3つの制限があります:
is:unread→ 未読メールのみafter:2025/12/30→ 今日以降のメールのみ(日付は毎日更新が必要)-label:ai-processed→ 処理済みラベルが付いていないもの
なぜこの制限が必要なのか?
もしis:unreadだけだと、初回実行時に過去の未読メール全てが処理されてしまいます。
例えば、過去に100件の未読メールがあったら、初回実行で100回もGemini APIを呼び出すことになり、無料枠が一瞬で消費されてしまうんですよね!
日付の自動更新について:
この例ではafter:2025/12/30と固定していますが、実運用では以下のように動的に設定するのがベストです:
// 今日の日付を自動取得
const today = Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd');
const SEARCH_QUERY = `is:unread after:${today} -label:ai-processed`;
はい、これで日付を毎回手動で更新する必要がなくなります!
処理件数の制限:
ここで一度に5件までと制限しているのは、Gemini 3 Flash Previewの無料枠(1日1,000リクエスト)を考慮しているためです。
その他の検索条件例:
- “label:顧客対応” → 特定のラベルが付いているものも上記のとおり、1日1,000リクエストまでなので、処理件数は調整が必要ですね
- “from:support@example.com” → 特定の送信者のみ
- “subject:緊急” → 件名に「緊急」を含む
ステップ2:最新メールの本文を取得
const messages = thread.getMessages();
const lastMessage = messages[messages.length - 1];
const body = lastMessage.getPlainBody();
メールスレッドには複数のメッセージが含まれることがあるので、最新のメッセージを取得しています。
getPlainBody()を使うことで、HTMLタグなどを除いたテキストのみを取得できます。
ステップ3:Gemini APIで分析
ここが核心部分ですね!
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
const resultText = json.candidates[0].content.parts[0].text;
Gemini APIのレスポンス構造は、ネストが深いんですよね。
candidates[0].content.parts[0].text と階層を降りていくことで、やっとAIの回答テキストにたどり着きます。
(最初、これがわからなくてめっちゃハマりましたw)
ステップ4:JSONクリーニングの重要性
const cleanedText = resultText.replace(/```json|```/g, "").trim();
はい、ここが超重要です!
生成AIは親切心から、JSONデータの周りに ```json というMarkdown記法を付けて返すことがあります。
人間には読みやすいのですが、プログラム(JSON.parse)にとってはエラーの原因になるんですよね。
なので、.replace(/```json|```/g, "") という正規表現を使って、この余計な装飾をクリーニング(除去)しています。
こうした細かい処理をコードでサクッと書けるのが、GASの最大の強みです!
ステップ5:緊急度による分岐
if (result.urgency === 'high') {
sendGoogleChatAlert(result, lastMessage.getSubject(), lastMessage.getFrom());
}
はい、これぞJSONの真価ですね!
result.urgency が "high" かどうかを明確に判定できるので、プログラムによる自動化が可能になります。
もし「なんとなく緊急そうです」というテキストだったら、こんな分岐は書けませんよね。
ステップ6:処理済みラベルで重複防止
thread.addLabel(label);
thread.markRead();
ここが、重複処理を防ぐための重要な仕組みです!
処理が完了したメールスレッドにai-processedというラベルを付けることで、次回実行時に同じメールを再度処理することを防ぎます。
検索条件に-label:ai-processedを入れているので、既にこのラベルが付いているメールは検索対象から除外されるんですよね。
処理の流れ:
- 検索条件で処理済みラベルがないメールのみ取得
- AIで分析してスプレッドシートに保存
- 処理済みラベルを付けて既読にする
- 次回実行時は、このメールはスキップされる
はい、これで完璧です!
(このラベル管理をしないと、既読にしたメールを未読に戻したときに再度処理されてしまうので要注意ですね)
トリガー設定:定期実行の自動化
コードを保存したら、次はトリガーを設定します。
- スクリプトエディタの左メニューから「トリガー」(時計アイコン)をクリック
- 「トリガーを追加」をクリック
- 以下のように設定:
- 実行する関数:
main - イベントのソース:
時間主導型 - 時間ベースのトリガータイプ:
分ベースのタイマー - 時間の間隔:
10分おき(無料枠を考慮した推奨設定)
- 実行する関数:
はい、これで設定完了です!
あとは放置しておけば、10分ごとに自動でメールをチェックして、分析してくれます。
(無料枠を気にせず使いたい場合は、5分おきでもOKですが、1日制限に注意してくださいね)

実際に動かしてみよう
では、テストメールを送って動作確認してみましょう。
テスト1:緊急クレーム
自分宛に以下のようなメールを送ってみます。
件名: 至急対応願います
本文:
御社のシステムが3時間も停止しており、業務に重大な支障が出ています。
このままでは損害賠償を請求せざるを得ません。
すぐに責任者から連絡をください。
スクリプトエディタで main() 関数を手動実行すると…
はい、Slackに通知が来ました!スプレッドシートにも、きちんと urgency: high でデータが保存されています。
(初めて動いたときは、めっちゃテンション上がりますよねw)


テスト2:営業メール
次は、営業メールで試してみます。
件名: 新サービスのご案内
本文:
この度、弊社では新しいクラウドサービスをリリースいたしました。
貴社のビジネスにお役立ていただけると存じます。
詳細は添付の資料をご覧ください。
こちらは urgency: low と判定され、Slackに通知は飛ばずにスプレッドシートにだけ記録されました。

はい、完璧ですね!
トラブルシューティング
実装中によくあるエラーと対処法をまとめておきます。
エラー1:「APIキーが無効です」
APIキーが正しくコピーされていない可能性があります。 前後にスペースが入っていないか確認してください。
エラー2:「JSONのパースに失敗しました」
Geminiが “`json というマークダウン記法を付けて返している可能性があります。 クリーニング処理がちゃんと動いているか確認してください。
エラー3:「スプレッドシートIDが見つかりません」
スプレッドシートのIDは、URLの /d/ と /edit の間の部分です。 例: https://docs.google.com/spreadsheets/d/【ここがID】/edit

まとめと次回予告
わずか数十行のコードで、高度なAIトリアージシステムが完成しました。
GASの魅力は、スピードと柔軟性です。 思いついたアイデアを、すぐにコードに落として試せる。この開発体験は、本当に楽しいんですよね!
そして、Gemini 3 Flash Previewという最新モデルを使うことで、Pro級の推論能力を無料枠で体験できるのも大きなメリットです。
しかし、企業利用においては「個人のGoogleアカウントに依存するGAS」は管理上のリスクになることがあります。
次回は、Microsoft 365 × Power Automateを用いた、コードを書かない(ノーコード)、かつ組織管理に適した堅牢な実装方法を解説します!
それでは皆さん、良い業務ハックライフを~


コメント