取り組みの背景 — なぜ Workspace Add-ons × Gemini なのか
Google Workspace を日常的に使っている組織にとって、Docs や Sheets、Gmail のサイドパネルから直接 AI の力を借りられることは大きな生産性向上につながります。2026年現在、Google Workspace Add-ons SDK は Card Service v2 によるリッチな UI コンポーネントをサポートしており、Gemini API と組み合わせることで、コンテキストに応じたインテリジェントなアシスタントをサイドパネルに構築できます。
ここではGoogle Workspace Add-ons の基本アーキテクチャから始め、Gemini API を統合したカスタムAIサイドパネルの開発、OAuth スコープ設計、セキュリティレビュー対応、そして Google Workspace Marketplace での公開と収益化までを体系的に解説します。実際に動作するコードを豊富に提供しますので、読み終わる頃にはご自身のアドオンを公開できる状態になっているはずです。
本記事は、Apps Script や GAS の基本を理解している方、Gemini API の基本的な使い方を知っている方を対象としています。Apps Script を使ったワークフロー構築については「Gemini API × Apps Script で Gmail・Calendar・Drive を横断する全自動AIワークフロー構築ガイド 」も参考にしてください。
Workspace Add-ons のアーキテクチャを理解する
Google Workspace Add-ons は、Apps Script または HTTP エンドポイントをバックエンドとして、Google Workspace アプリケーション(Gmail、Docs、Sheets、Slides、Calendar)のサイドパネルにカスタム UI を表示する仕組みです。
ホストアプリとトリガーの関係
Add-ons は「ホストアプリ」ごとにトリガー関数を定義します。たとえば、ユーザーが Gmail でメールを開いた瞬間や、Docs でドキュメントを開いた瞬間に、対応するトリガー関数が呼び出され、サイドパネルの Card UI が生成されます。
Homepage Trigger : アドオンアイコンをクリックしたときに表示されるホーム画面
Contextual Trigger : 特定のコンテキスト(メールを開く、ドキュメントを選択する等)で自動表示される画面
Action Callback : ボタンクリックやフォーム送信時に呼び出される処理
Card Service v2 の UI コンポーネント
Card Service v2 では以下の主要コンポーネントが利用可能です。
CardBuilder : カードのルートコンテナ
CardSection : セクション区切り(ヘッダー付き)
TextParagraph / DecoratedText : テキスト表示
TextInput / SelectionInput : ユーザー入力フォーム
ButtonSet / TextButton / ImageButton : アクションボタン
Grid / GridItem : グリッドレイアウト
Divider : セクション間の区切り線
これらを組み合わせることで、チャット風のインターフェースやフォーム入力画面、分析結果の表示パネルなど、多様な UI を構築できます。
マニフェストファイル(appsscript.json)の構成
{
"timeZone" : "Asia/Tokyo" ,
"dependencies" : {},
"exceptionLogging" : "STACKDRIVER" ,
"runtimeVersion" : "V8" ,
"oauthScopes" : [
"https://www.googleapis.com/auth/gmail.readonly" ,
"https://www.googleapis.com/auth/documents.currentonly" ,
"https://www.googleapis.com/auth/spreadsheets.currentonly" ,
"https://www.googleapis.com/auth/script.external_request" ,
"https://www.googleapis.com/auth/userinfo.email"
],
"addOns" : {
"common" : {
"name" : "Gemini AI Assistant" ,
"logoUrl" : "https://example.com/logo.png" ,
"layoutProperties" : {
"primaryColor" : "#4285F4"
},
"homepageTrigger" : {
"runFunction" : "onHomepage"
}
},
"gmail" : {
"contextualTriggers" : [{
"unconditional" : {},
"onTriggerFunction" : "onGmailMessage"
}]
},
"docs" : {
"homepageTrigger" : {
"runFunction" : "onDocsHomepage"
}
},
"sheets" : {
"homepageTrigger" : {
"runFunction" : "onSheetsHomepage"
}
}
}
}
oauthScopes の設計はセキュリティレビューの合否に直結するため、最小権限の原則 を徹底してください。currentonly スコープが利用できる場合は、フルアクセス(documents や spreadsheets)ではなく currentonly を選びましょう。
Gemini API を統合したサイドパネルの実装
ここからは、実際に Gemini API を呼び出すサイドパネルの実装に入ります。Gmail のメール内容を Gemini で分析し、要約や返信ドラフトを生成するアドオンを例に進めます。
Gemini API 呼び出しのユーティリティ関数
// gemini.gs — Gemini API ユーティリティ
const GEMINI_API_KEY = PropertiesService. getScriptProperties ()
. getProperty ( 'GEMINI_API_KEY' );
const GEMINI_ENDPOINT =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent' ;
/**
* Gemini API にプロンプトを送信し、テキスト応答を返す
* @param {string} prompt - 入力プロンプト
* @param {Object} options - オプション設定
* @returns {string} Gemini の応答テキスト
*/
function callGemini ( prompt , options = {}) {
const payload = {
contents: [{
parts: [{ text: prompt }]
}],
generationConfig: {
temperature: options.temperature || 0.7 ,
maxOutputTokens: options.maxTokens || 2048 ,
topP: options.topP || 0.95
}
};
// System Instruction の設定(任意)
if (options.systemInstruction) {
payload.systemInstruction = {
parts: [{ text: options.systemInstruction }]
};
}
const response = UrlFetchApp. fetch (
`${ GEMINI_ENDPOINT }?key=${ GEMINI_API_KEY }` ,
{
method: 'post' ,
contentType: 'application/json' ,
payload: JSON . stringify (payload),
muteHttpExceptions: true
}
);
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;
}
/**
* 構造化出力(JSON)を要求する Gemini 呼び出し
* @param {string} prompt - 入力プロンプト
* @param {Object} schema - JSON Schema 定義
* @returns {Object} パースされた JSON オブジェクト
*/
function callGeminiStructured ( prompt , schema ) {
const payload = {
contents: [{
parts: [{ text: prompt }]
}],
generationConfig: {
responseMimeType: 'application/json' ,
responseSchema: schema,
temperature: 0.3
}
};
const response = UrlFetchApp. fetch (
`${ GEMINI_ENDPOINT }?key=${ GEMINI_API_KEY }` ,
{
method: 'post' ,
contentType: 'application/json' ,
payload: JSON . stringify (payload),
muteHttpExceptions: true
}
);
const json = JSON . parse (response. getContentText ());
return JSON . parse (json.candidates[ 0 ].content.parts[ 0 ].text);
}
このユーティリティは、通常のテキスト応答と構造化出力(JSON モード)の両方をサポートしています。構造化出力の詳細は「Gemini 構造化出力の実践ガイド 」を参照してください。
Gmail 用 AI サイドパネルの実装
// gmail-addon.gs — Gmail サイドパネル
function onGmailMessage ( e ) {
const messageId = e.gmail.messageId;
const message = GmailApp. getMessageById (messageId);
const subject = message. getSubject ();
const body = message. getPlainBody (). substring ( 0 , 3000 ); // トークン節約
const from = message. getFrom ();
const card = CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader ()
. setTitle ( 'Gemini AI Assistant' )
. setSubtitle (subject)
. setImageUrl ( 'https://example.com/gemini-icon.png' )
. setImageStyle (CardService.ImageStyle. CIRCLE )
);
// 要約セクション
const summarySection = CardService. newCardSection ()
. setHeader ( '📋 メール要約' )
. addWidget (
CardService. newTextParagraph ()
. setText ( 'サイドパネルから AI 分析を実行できます' )
)
. addWidget (
CardService. newButtonSet ()
. addButton (
CardService. newTextButton ()
. setText ( '要約を生成' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'summarizeEmail' )
. setParameters ({
messageId: messageId,
subject: subject,
body: body. substring ( 0 , 1500 )
})
)
)
. addButton (
CardService. newTextButton ()
. setText ( '返信ドラフト作成' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'draftReply' )
. setParameters ({
messageId: messageId,
subject: subject,
body: body. substring ( 0 , 1500 ),
from: from
})
)
)
);
// カスタムプロンプトセクション
const customSection = CardService. newCardSection ()
. setHeader ( '💬 カスタム質問' )
. addWidget (
CardService. newTextInput ()
. setFieldName ( 'customPrompt' )
. setTitle ( 'このメールについて質問' )
. setHint ( '例: このメールの要求事項をリストアップして' )
)
. addWidget (
CardService. newTextButton ()
. setText ( 'AI に聞く' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'askAboutEmail' )
. setParameters ({
body: body. substring ( 0 , 1500 )
})
)
);
card. addSection (summarySection);
card. addSection (customSection);
return card. build ();
}
/**
* メール要約のコールバック
*/
function summarizeEmail ( e ) {
const params = e.commonEventObject.parameters;
const summary = callGemini (
`以下のメールを3行で要約してください。重要なアクションアイテムがあれば箇条書きで追記してください。 \n\n 件名: ${ params . subject } \n\n 本文: \n ${ params . body }` ,
{
systemInstruction: 'あなたはビジネスメールの分析アシスタントです。簡潔かつ正確に要約してください。' ,
temperature: 0.3 ,
maxTokens: 512
}
);
const card = CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader ()
. setTitle ( '要約結果' )
. setSubtitle (params.subject)
)
. addSection (
CardService. newCardSection ()
. addWidget (
CardService. newTextParagraph (). setText (summary)
)
. addWidget (
CardService. newButtonSet ()
. addButton (
CardService. newTextButton ()
. setText ( '← 戻る' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'onGmailMessage' )
)
)
)
);
return CardService. newActionResponseBuilder ()
. setNavigation (
CardService. newNavigation (). pushCard (card. build ())
)
. build ();
}
/**
* 返信ドラフト生成のコールバック
*/
function draftReply ( e ) {
const params = e.commonEventObject.parameters;
const replyDraft = callGemini (
`以下のメールへの返信を日本語で作成してください。ビジネスメールとして丁寧かつ簡潔に、具体的な内容を含めてください。 \n\n 差出人: ${ params . from } \n 件名: ${ params . subject } \n 本文: \n ${ params . body }` ,
{
systemInstruction: 'あなたはビジネスメール作成のプロフェッショナルです。' ,
temperature: 0.5 ,
maxTokens: 1024
}
);
// Gmail にドラフトを作成
const message = GmailApp. getMessageById (params.messageId);
message. getThread (). createDraftReply (replyDraft);
const card = CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader (). setTitle ( '返信ドラフト作成完了' )
)
. addSection (
CardService. newCardSection ()
. addWidget (
CardService. newTextParagraph ()
. setText ( '✅ 下書きフォルダにドラフトを保存しました。 \n\n ' + replyDraft. substring ( 0 , 500 ) + '...' )
)
);
return CardService. newActionResponseBuilder ()
. setNavigation (
CardService. newNavigation (). pushCard (card. build ())
)
. build ();
}
Docs 用 AI サイドパネルの実装
// docs-addon.gs — Google Docs サイドパネル
function onDocsHomepage ( e ) {
const card = CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader ()
. setTitle ( 'Gemini AI Assistant' )
. setSubtitle ( 'ドキュメント分析' )
);
const analysisSection = CardService. newCardSection ()
. setHeader ( '📝 ドキュメント分析' )
. addWidget (
CardService. newButtonSet ()
. addButton (
CardService. newTextButton ()
. setText ( '文章を校正' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'proofreadDoc' )
)
)
. addButton (
CardService. newTextButton ()
. setText ( '要約を生成' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'summarizeDoc' )
)
)
);
const translateSection = CardService. newCardSection ()
. setHeader ( '🌐 翻訳' )
. addWidget (
CardService. newSelectionInput ()
. setFieldName ( 'targetLang' )
. setTitle ( '翻訳先言語' )
. setType (CardService.SelectionInputType. DROPDOWN )
. addItem ( '英語' , 'en' , false )
. addItem ( '中国語(簡体字)' , 'zh-CN' , false )
. addItem ( '韓国語' , 'ko' , false )
. addItem ( 'フランス語' , 'fr' , false )
. addItem ( 'スペイン語' , 'es' , false )
)
. addWidget (
CardService. newTextButton ()
. setText ( '選択テキストを翻訳' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'translateSelection' )
)
);
card. addSection (analysisSection);
card. addSection (translateSection);
return card. build ();
}
/**
* ドキュメント校正
*/
function proofreadDoc ( e ) {
const doc = DocumentApp. getActiveDocument ();
const body = doc. getBody (). getText ();
if ( ! body || body. trim (). length === 0 ) {
return createErrorCard ( 'ドキュメントが空です。テキストを入力してから再度お試しください。' );
}
// 長いドキュメントは先頭5000文字に制限
const textToAnalyze = body. substring ( 0 , 5000 );
const result = callGeminiStructured (
`以下の文章を校正してください。誤字脱字、文法ミス、表現の改善点を指摘してください。 \n\n ${ textToAnalyze }` ,
{
type: 'object' ,
properties: {
corrections: {
type: 'array' ,
items: {
type: 'object' ,
properties: {
original: { type: 'string' , description: '元のテキスト' },
corrected: { type: 'string' , description: '修正後のテキスト' },
reason: { type: 'string' , description: '修正理由' }
},
required: [ 'original' , 'corrected' , 'reason' ]
}
},
overallScore: { type: 'number' , description: '100点満点の文章品質スコア' },
summary: { type: 'string' , description: '全体的な評価コメント' }
},
required: [ 'corrections' , 'overallScore' , 'summary' ]
}
);
const card = CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader ()
. setTitle ( `校正結果 — ${ result . overallScore }点` )
);
const summarySection = CardService. newCardSection ()
. setHeader ( '総合評価' )
. addWidget (
CardService. newTextParagraph (). setText (result.summary)
);
card. addSection (summarySection);
if (result.corrections. length > 0 ) {
const correctionsSection = CardService. newCardSection ()
. setHeader ( `修正候補(${ result . corrections . length }件)` );
result.corrections. slice ( 0 , 10 ). forEach (( c , i ) => {
correctionsSection. addWidget (
CardService. newDecoratedText ()
. setTopLabel ( `#${ i + 1 } — ${ c . reason }` )
. setText ( `❌ ${ c . original } \n ✅ ${ c . corrected }` )
. setWrapText ( true )
);
});
card. addSection (correctionsSection);
}
return CardService. newActionResponseBuilder ()
. setNavigation (
CardService. newNavigation (). pushCard (card. build ())
)
. build ();
}
テスト・デバッグのベストプラクティス
ローカル開発環境のセットアップ
Apps Script エディタだけでは開発効率が低いため、clasp(Command Line Apps Script Projects)を使ってローカル開発環境を構築することを強く推奨します。
# clasp のインストール
npm install -g @google/clasp
# Google アカウントでログイン
clasp login
# 既存の Apps Script プロジェクトをクローン
clasp clone < SCRIPT_I D >
# ローカルで編集後にプッシュ
clasp push
# Apps Script エディタをブラウザで開く
clasp open
デバッグのテクニック
Logger.log と console.log の使い分け : Logger.log() は Apps Script エディタの実行ログに出力、console.log() は Stackdriver(Cloud Logging)に出力されます。本番デバッグでは console.log() を推奨します
テストデプロイの活用 : マニフェストを変更した後、Apps Script エディタの「デプロイ」→「テストデプロイ」で、自分だけが使えるテスト版をインストールできます。本番デプロイ前に必ずテストデプロイで動作確認してください
エラーハンドリングの統一パターン :
// error-handler.gs — エラーハンドリングユーティリティ
function createErrorCard ( message ) {
return CardService. newActionResponseBuilder ()
. setNavigation (
CardService. newNavigation (). pushCard (
CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader (). setTitle ( 'エラーが発生しました' )
)
. addSection (
CardService. newCardSection ()
. addWidget (
CardService. newTextParagraph ()
. setText ( `⚠️ ${ message }` )
)
. addWidget (
CardService. newTextButton ()
. setText ( '← 戻る' )
. setOnClickAction (
CardService. newAction ()
. setFunctionName ( 'onHomepage' )
)
)
)
. build ()
)
)
. build ();
}
/**
* try-catch ラッパー
*/
function safeExecute ( fn ) {
return function ( e ) {
try {
return fn (e);
} catch (error) {
console. error ( `Add-on error: ${ error . message }` , error.stack);
return createErrorCard (
`処理中にエラーが発生しました: ${ error . message } \n\n しばらく待ってから再度お試しください。`
);
}
};
}
Gemini API のレスポンスキャッシュ : 同じ内容に対する重複リクエストを防ぐため、CacheService を活用します:
// cache-helper.gs — キャッシュユーティリティ
function cachedGeminiCall ( prompt , options , ttlSeconds ) {
const cache = CacheService. getUserCache ();
const cacheKey = Utilities. computeDigest (
Utilities.DigestAlgorithm. MD5 ,
prompt
). map ( b => (b < 0 ? b + 256 : b). toString ( 16 ). padStart ( 2 , '0' )). join ( '' );
const cached = cache. get (cacheKey);
if (cached) {
return cached;
}
const result = callGemini (prompt, options);
// CacheService の上限は 100KB / 6時間
cache. put (cacheKey, result, ttlSeconds || 3600 );
return result;
}
OAuth スコープ設計とセキュリティレビュー対策
Workspace Marketplace に公開するアドオンは、Google のセキュリティレビューを通過する必要があります。ここではレビュー通過率を高めるための実践的なポイントを解説します。
スコープ選定の原則
最小権限 : gmail.readonly で十分なら gmail.modify を要求しない
currentonly 優先 : documents.currentonly は開いているドキュメントだけにアクセスできるため、documents フルスコープよりレビュー通過率が高い
段階的スコープ要求 : 初回は最低限のスコープで開始し、ユーザーが特定の機能を使う際に追加スコープを要求する(Incremental Authorization)
セキュリティレビューで必要なドキュメント
プライバシーポリシー : ユーザーデータの収集・利用・保存・削除に関する方針を明記。GDPR / CCPA 準拠が望ましい
利用規約 : サービスの利用条件、免責事項を記載
OAuth スコープの正当性説明 : 各スコープがなぜ必要なのかを機能ごとに説明するドキュメント
データフロー図 : ユーザーデータがどこに送信され、どう処理され、いつ削除されるかを図示
Gemini API にデータを送信する際の注意点
Workspace Add-ons は組織のセンシティブなデータを扱うことが多いため、以下の対策が不可欠です。
// data-sanitizer.gs — データのサニタイズ
function sanitizeForGemini ( text ) {
// メールアドレスをマスク
let sanitized = text. replace (
/ [a-zA-Z0-9._%+-] + @ [a-zA-Z0-9.-] + \. [a-zA-Z] {2,} / g ,
'[EMAIL_MASKED]'
);
// 電話番号をマスク(日本の形式)
sanitized = sanitized. replace (
/0 \d {1,4} - ? \d {1,4} - ? \d {4} / g ,
'[PHONE_MASKED]'
);
// クレジットカード番号パターンをマスク
sanitized = sanitized. replace (
/ \b \d {4} [-\s] ? \d {4} [-\s] ? \d {4} [-\s] ? \d {4}\b / g ,
'[CARD_MASKED]'
);
return sanitized;
}
API キーの管理には Script Properties を使い、コードにハードコードしないことが鉄則です。API キーの安全な管理について詳しくは、Gemini API のドキュメントを参照してください。
Google Workspace Marketplace への公開手順
公開前チェックリスト
Marketplace への申請前に以下を確認してください。
アドオンがすべての対象ホストアプリ(Gmail、Docs、Sheets 等)で正常に動作すること
テストデプロイで最低5名のテスターからフィードバックを得ていること
プライバシーポリシーと利用規約のページが HTTPS で公開済みであること
スクリーンショット(最低3枚、1280x800px 推奨)が準備されていること
アドオンの説明文(英語 + 対象言語)が完成していること
OAuth スコープの正当性説明書が完成していること
Google Cloud Console での設定
Google Cloud プロジェクトの作成 : Apps Script プロジェクトに紐づく GCP プロジェクトを作成
OAuth 同意画面の設定 : 外部ユーザー向けに同意画面を構成し、必要なスコープを登録
Marketplace SDK の有効化 : GCP コンソールで「Google Workspace Marketplace SDK」を有効にし、アプリの設定を入力
アプリの送信 : レビュー用の情報を入力し、審査に送信
審査期間と対応
審査には通常1〜3週間かかります。審査チームから追加情報を求められることが多いため、以下のポイントを押さえておくと修正対応がスムーズです。
デモ動画 : 30秒〜2分の短い画面収録で機能を見せる
テストアカウント : 審査チームがテストできるアカウント情報を提供する(本番データは含めない)
スコープの根拠 : 「この機能でこのスコープが必要」という1対1のマッピングを明示する
サブスクリプション課金モデルの組み込み
Marketplace での収益化にはいくつかの方法がありますが、2026年現在最も一般的なのは、自前のサブスクリプションシステムを組み込む方法です。
ライセンスチェックの実装
// license.gs — ライセンス管理
const LICENSE_API_URL = 'https://your-backend.com/api/license' ;
/**
* ユーザーのライセンス状態を確認する
* @returns {Object} ライセンス情報
*/
function checkLicense () {
const email = Session. getActiveUser (). getEmail ();
const cache = CacheService. getUserCache ();
const cacheKey = `license_${ email }` ;
// キャッシュチェック(5分)
const cached = cache. get (cacheKey);
if (cached) {
return JSON . parse (cached);
}
try {
const response = UrlFetchApp. fetch (
`${ LICENSE_API_URL }/check` ,
{
method: 'post' ,
contentType: 'application/json' ,
payload: JSON . stringify ({ email: email }),
muteHttpExceptions: true ,
headers: {
'Authorization' : `Bearer ${ getServiceToken () }`
}
}
);
const license = JSON . parse (response. getContentText ());
cache. put (cacheKey, JSON . stringify (license), 300 );
return license;
} catch (error) {
console. error ( 'License check failed:' , error);
// フォールバック: エラー時は無料プランとして扱う
return { plan: 'free' , features: [ 'basic' ] };
}
}
/**
* プレミアム機能のゲーティング
*/
function requirePremium ( e ) {
const license = checkLicense ();
if (license.plan === 'free' ) {
return CardService. newActionResponseBuilder ()
. setNavigation (
CardService. newNavigation (). pushCard (
createUpgradeCard (license)
)
)
. build ();
}
return null ; // プレミアムユーザーは通過
}
/**
* アップグレード促進カード
*/
function createUpgradeCard ( license ) {
return CardService. newCardBuilder ()
. setHeader (
CardService. newCardHeader ()
. setTitle ( 'プレミアム機能' )
. setSubtitle ( 'この機能はプレミアムプランで利用できます' )
)
. addSection (
CardService. newCardSection ()
. addWidget (
CardService. newTextParagraph ()
. setText (
'🔒 この機能にはプレミアムプランが必要です。 \n\n ' +
'**プレミアムプランの特典:** \n ' +
'• 無制限のAI分析 \n ' +
'• 高度な校正・翻訳機能 \n ' +
'• 優先サポート \n\n ' +
'月額 $9.99 / 年額 $99.99'
)
)
. addWidget (
CardService. newTextButton ()
. setText ( 'プランを確認する' )
. setOpenLink (
CardService. newOpenLink ()
. setUrl ( 'https://your-site.com/pricing' )
. setOpenAs (CardService.OpenAs. OVERLAY )
)
)
)
. build ();
}
収益最適化のポイント
フリーミアムモデル : 基本機能は無料で提供し、高度な AI 分析・無制限利用・チーム機能をプレミアムにする
使用量ベースの制限 : 無料ユーザーは1日10回の AI 分析まで、プレミアムは無制限など
チームプラン : Google Workspace の組織アカウント向けに、管理者が一括でライセンスを管理できるチームプランを提供する
トライアル期間 : 14日間の無料トライアルを用意し、プレミアム機能を体験してもらう
収益化の戦略を体系的に立てたい方は「Gemini 収益化マスタープラン 2026 」も参考にしてください。
パフォーマンス最適化と本番運用
実行時間制限への対処
Apps Script の実行時間制限(6分 / 呼び出し)は、Gemini API 呼び出しを含むアドオンでは特に注意が必要です。
// performance.gs — パフォーマンス最適化
/**
* 大量テキストを分割して並列風に処理する
* (Apps Script は真の並列実行不可のため、バッチ化で効率化)
*/
function processLargeDocument ( fullText , chunkSize ) {
const chunks = [];
for ( let i = 0 ; i < fullText. length ; i += chunkSize) {
chunks. push (fullText. substring (i, i + chunkSize));
}
const results = [];
const startTime = Date. now ();
const TIME_LIMIT_MS = 300000 ; // 5分(バッファ1分)
for ( const chunk of chunks) {
if (Date. now () - startTime > TIME_LIMIT_MS ) {
results. push ({
partial: true ,
message: '処理時間制限により、残りの部分は次回に分析します'
});
break ;
}
const analysis = callGemini (
`以下のテキストを分析してください: \n ${ chunk }` ,
{ temperature: 0.3 , maxTokens: 1024 }
);
results. push ({ text: analysis });
}
return results;
}
モニタリングとアラート
本番運用では、Cloud Logging と Cloud Monitoring を組み合わせてアドオンの健全性を監視します。
// monitoring.gs — 使用量トラッキング
function trackUsage ( action , metadata ) {
const email = Session. getActiveUser (). getEmail ();
const timestamp = new Date (). toISOString ();
// Cloud Logging に構造化ログを出力
console. log ( JSON . stringify ({
severity: 'INFO' ,
action: action,
user: Utilities. computeDigest (
Utilities.DigestAlgorithm. SHA_256 ,
email
). map ( b => (b < 0 ? b + 256 : b). toString ( 16 ). padStart ( 2 , '0' )). join ( '' ). substring ( 0 , 16 ),
timestamp: timestamp,
metadata: metadata
}));
}
まとめ
ここではGoogle Workspace Add-ons と Gemini API を組み合わせたカスタム AI サイドパネルの開発から Marketplace 公開・収益化までを一通り解説しました。Card Service v2 による UI 構築、Gemini API の統合パターン、OAuth スコープ設計、セキュリティレビュー対策、そしてサブスクリプション課金モデルの実装まで、実際のコードを交えて体系的にカバーしています。
Workspace Add-ons は、Google Workspace を日常的に使う膨大なユーザーベースに直接リーチできるチャネルです。Gemini API の自然言語処理能力と組み合わせることで、ユーザーの生産性を飛躍的に高める AI ツールを構築し、それを持続可能なビジネスとして展開することが可能です。
まずは自分自身の日常業務で使えるシンプルなアドオンから始め、ユーザーフィードバックを集めながら機能を拡張していくのが最も確実なアプローチです。本記事のコードはそのまま動作するように設計していますので、ぜひベースとして活用してください。
また、Google AI エコシステムでの収益の柱について体系的に学びたい方には「Gemini 収益化マスタープラン 2026 」もおすすめです。