google-apps-script について
google-apps-script は、Google Sheets や Workspace アプリ向けの自動化スクリプト作成を支援するスキルです。メニュー、トリガー、サイドバー、メール、PDF出力、外部API呼び出しに加え、処理性能や認可の注意点も扱います。
主な機能
- 01メニュー、ダイアログ、サイドバー用スクリプトの作成
- 02編集、時間指定、フォームのトリガー設定
- 03メール送信とPDF出力の自動化
- 04外部API呼び出しとエラー処理
- 05一括処理と認可に関する指針
活用シーン
- 01セル編集時のタイムスタンプ自動入力
- 02シートの定期集計とレポートのメール送信
- 03シートのPDF出力と配布
- 04データの一括処理と完了行のアーカイブ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill google-apps-scriptインストール後、Claude Code で/google-apps-scriptを入力して読み込まれたか確認します。
詳細
Google Apps Script
Google Sheets や Workspace アプリ向けの自動化スクリプトを作成します。スクリプトは Google のインフラストラクチャ上でサーバーサイド実行され、無料枠も充実しています。
作成するもの
- 拡張機能 > Apps Script に貼り付ける Apps Script コード
- カスタムメニュー、ダイアログ、サイドバー
- 自動トリガー(編集時、時間主導型、フォーム送信時)
- メール通知、PDF エクスポート、API 連携
ワークフロー
ステップ 1:自動化の内容を把握する
ユーザーが何を自動化したいのかを尋ねます。一般的なシナリオは次のとおりです。
- アクションを実行するカスタムメニュー(レポートの生成、データ処理)
- 自動トリガーによる動作(編集時、フォーム送信時、スケジュール実行)
- データ入力用のサイドバーアプリ
- シートのデータに基づくメール通知
- PDF のエクスポートと配布
ステップ 2:スクリプトを生成する
以下の構成テンプレートに従います。すべてのスクリプトには、ヘッダーコメント、先頭に置く設定用定数、メニュー設定用の onOpen() が必要です。
ステップ 3:インストール手順を案内する
すべてのスクリプトは同じ方法でインストールします。
- Google スプレッドシートを開きます
- 拡張機能 > Apps Script
- エディタ内の既存のコードを削除します
- スクリプトを貼り付けます
- 保存 をクリックします
- Apps Script のタブを閉じます
- スプレッドシートを再読み込みします(ページ読み込み時に onOpen が実行されます)
ステップ 4:初回の承認
初回実行時に、各ユーザーに Google OAuth の同意画面が表示されます。未確認のスクリプト(ほとんどの内部スクリプト)の場合、ユーザーは次の操作を行う必要があります。
詳細 > [プロジェクト名] に移動(安全ではありません) > 許可
これはユーザーごとに 1 回だけ行います。この点を出力内でユーザーに知らせてください。
スクリプト構成テンプレート
すべてのスクリプトは、次のパターンに従う必要があります。
/**
* [Project Name] - [Brief Description]
*
* [What it does, key features]
*
* INSTALL: Extensions > Apps Script > paste this > Save > Reload sheet
*/
// --- CONFIGURATION ---
const SOME_SETTING = 'value';
// --- MENU SETUP ---
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('My Menu')
.addItem('Do Something', 'myFunction')
.addSeparator()
.addSubMenu(ui.createMenu('More Options')
.addItem('Option A', 'optionA'))
.addToUi();
}
// --- FUNCTIONS ---
function myFunction() {
// Implementation
}
重要なルール
パブリック関数とプライベート関数
末尾が _(アンダースコア)の関数はプライベートであり、クライアントサイドの HTML から google.script.run を介して呼び出すことはできません。これはサイレントエラーとなり、エラーは表示されず、呼び出しが機能しません。
// WRONG - dialog can't call this, fails silently
function doWork_() { return 'done'; }
// RIGHT - dialog can call this
function doWork() { return 'done'; }
次にも適用されます:メニュー項目で参照する関数名は、文字列で指定するパブリック関数名である必要があります。
バッチ処理(パフォーマンス上の重要事項)
データの読み書きは一括で行い、セルごとの処理は避けてください。速度に 70 倍の差があります。
// SLOW (70 seconds on 100x100) - reads one cell at a time
for (let i = 1; i <= 100; i++) {
const val = sheet.getRange(i, 1).getValue();
}
// FAST (1 second) - reads all at once
const allData = sheet.getRange(1, 1, 100, 1).getValues();
for (const row of allData) {
const val = row[0];
}
一括読み書きには、常に getRange().getValues() / setValues() を使用してください。
V8 ランタイム
V8 が唯一のランタイムです(Rhino は 2026 年 1 月に削除されました)。最新の JavaScript 機能(const、let、アロー関数、テンプレートリテラル、分割代入、クラス、async/ジェネレーター)をサポートしています。
利用できない機能(Apps Script の代替機能を使用してください):
| 利用できない API | Apps Script の代替機能 |
|---|---|
setTimeout / setInterval | Utilities.sleep(ms)(ブロッキング) |
fetch | UrlFetchApp.fetch() |
FormData | ペイロードを手動で構築 |
URL | 文字列操作 |
crypto | Utilities.computeDigest() / Utilities.getUuid() |
返す前にフラッシュする
シートを変更する関数から戻る前に SpreadsheetApp.flush() を呼び出してください。特に HTML ダイアログから呼び出される場合は重要です。呼び出さないと、ダイアログに「完了」と表示されても、変更が反映されていないことがあります。
シンプルトリガーとインストール型トリガー
| 機能 | シンプル(onEdit) | インストール型 |
|---|---|---|
| 承認が必要 | いいえ | はい |
| メール送信 | いいえ | はい |
| 他のファイルへのアクセス | いいえ | はい |
| URL の取得 | いいえ | はい |
| ダイアログを開く | いいえ | はい |
| 実行ユーザー | アクティブユーザー | トリガー作成者 |
軽量な処理にはシンプルトリガーを使用します。メール、外部 API、または複数ファイルへのアクセスが必要な場合は、インストール型トリガー(ScriptApp.newTrigger() を使用)を使用してください。
カスタムスプレッドシート関数
セル内で =MY_FUNCTION() として使用する関数には、厳しい制限があります。
/**
* Calculates something custom.
* @param {string} input The input value
* @return {string} The result
* @customfunction
*/
function MY_FUNCTION(input) {
// Can use: basic JS, Utilities, CacheService
// CANNOT use: MailApp, UrlFetchApp, SpreadsheetApp.getUi(), triggers
return input.toUpperCase();
}
@customfunctionの JSDoc タグを含める必要があります- 実行時間は 30 秒(通常の関数は 6 分)です
- 承認が必要なサービスにはアクセスできません
クォータと制限
| リソース | 無料アカウント | Google Workspace |
|---|---|---|
| スクリプトの実行時間 | 1 回の実行につき 6 min | 1 回の実行につき 6 min |
| 時間主導型トリガーの実行時間 | 30 min | 30 min |
| トリガーの 1 日あたりの合計実行時間 | 90 min | 6 hours |
| トリガーの合計数 | スクリプトごとにユーザーあたり 20 | スクリプトごとにユーザーあたり 20 |
| 1 日あたりのメール宛先数 | 100 | 1,500 |
| 1 日あたりの URL Fetch 呼び出し数 | 20,000 | 100,000 |
| プロパティのストレージ | 500 KB | 500 KB |
| カスタム関数の実行時間 | 30 seconds | 30 seconds |
| 同時実行数 | 30 | 30 |
モーダル進捗ダイアログ
自動的に閉じるスピナーを使って、時間のかかる処理中のユーザー操作をブロックします。数秒以上かかる処理には、どのようなものでも使用してください。
パターン:メニュー関数 > showProgress() > ダイアログからアクション関数を呼び出す > 自動的に閉じる
function showProgress(message, serverFn) {
const html = HtmlService.createHtmlOutput(`
<style>
body { font-family: 'Google Sans', Arial, sans-serif; display: flex;
flex-direction: column; align-items: center; justify-content: center;
height: 100%; margin: 0; padding: 20px; box-sizing: border-box; }
.spinner { width: 36px; height: 36px; border: 4px solid #e0e0e0;
border-top: 4px solid #1a73e8; border-radius: 50%;
animation: spin 0.8s linear infinite; margin-bottom: 16px; }
@keyframes spin { to { transform: rotate(360deg); } }
.message { font-size: 14px; color: #333; text-align: center; }
.done { color: #1e8e3e; font-weight: 500; }
.error { color: #d93025; font-weight: 500; }
</style>
<div class="spinner" id="spinner"></div>
<div class="message" id="msg">${message}</div>
<script>
google.script.run
.withSuccessHandler(function(r) {
document.getElementById('spinner').style.display = 'none';
var m = document.getElementById('msg');
m.className = 'message done';
m.innerText = 'Done! ' + (r || '');
setTimeout(function() { google.script.host.close(); }, 1200);
})
.withFailureHandler(function(err) {
document.getElementById('spinner').style.display = 'none';
var m = document.getElementById('msg');
m.className = 'message error';
m.innerText = 'Error: ' + err.message;
setTimeout(function() { google.script.host.close(); }, 3000);
})
.${serverFn}();
</script>
`).setWidth(320).setHeight(140);
SpreadsheetApp.getUi().showModalDialog(html, 'Working...');
}
// Menu calls this wrapper
function menuDoWork() {
showProgress('Processing data...', 'doTheWork');
}
// MUST be public (no underscore) for the dialog to call it
function doTheWork() {
// ... do the work ...
SpreadsheetApp.flush();
return 'Processed 50 rows'; // shown in success message
}
よく使うパターン
トースト通知
SpreadsheetApp.getActiveSpreadsheet().toast('Operation complete!', 'Title', 5);
// Arguments: message, title, duration in seconds (-1 = until dismissed)
アラートとプロンプトのダイアログ
const ui = SpreadsheetApp.getUi();
// Yes/No confirmation
const response = ui.alert('Delete this data?', 'This cannot be undone.',
ui.ButtonSet.YES_NO);
if (response === ui.Button.YES) { /* proceed */ }
// Prompt for input
const result = ui.prompt('Enter your name:', ui.ButtonSet.OK_CANCEL);
if (result.getSelectedButton() === ui.Button.OK) {
const name = result.getResponseText();
}
サイドバーアプリ
右側に表示される HTML パネルです。google.script.run を使ってサーバー関数を呼び出します。
function showSidebar() {
const html = HtmlService.createHtmlOutput(`
<h3>Quick Entry</h3>
<select id="worker"><option>Craig</option><option>Steve</option></select>
<input id="suburb" placeholder="Suburb">
<button onclick="submit()">Add Job</button>
<script>
function submit() {
google.script.run.withSuccessHandler(function() { alert('Added!'); })
.addJob(document.getElementById('worker').value,
document.getElementById('suburb').value);
}
</script>
`).setTitle('Job Entry').setWidth(300);
SpreadsheetApp.getUi().showSidebar(html);
}
function addJob(worker, suburb) { // MUST be public (no underscore)
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().appendRow([new Date(), worker, suburb]);
}
トリガー
onEdit(シンプルトリガー) -- 権限に制限がありますが、認証は不要です。
function onEdit(e) {
const sheet = e.source.getActiveSheet();
if (sheet.getName() !== 'Data') return;
if (e.range.getColumn() !== 3) return;
// Auto-timestamp when column C is edited
sheet.getRange(e.range.getRow(), 4).setValue(new Date());
}
インストール型トリガー -- スクリプトで作成し、セットアップ関数を一度手動で実行します。
function createTriggers() {
// Time-driven: run every day at 8am
ScriptApp.newTrigger('dailyReport')
.timeBased().atHour(8).everyDays(1).create();
// On edit with full permissions (can send email, fetch URLs)
ScriptApp.newTrigger('onEditFull')
.forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
// On form submit
ScriptApp.newTrigger('onFormSubmit')
.forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create();
}
Sheets からメールを送信する
function emailWeeklySchedule() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getRange('A2:E10').getDisplayValues();
let body = '<h2>Weekly Schedule</h2><table border="1" cellpadding="8">';
body += '<tr><th>Job</th><th>Suburb</th><th>Time</th><th>Price</th></tr>';
for (const row of data) {
if (row[0]) body += '<tr>' + row.map(c => '<td>' + c + '</td>').join('') + '</tr>';
}
body += '</table>';
MailApp.sendEmail({ to: '[email protected]',
subject: 'Schedule - Week ' + sheet.getName(), htmlBody: body });
}
PDF のエクスポート
分かりにくい URL の構築方法です。エクスポートパラメーターは文書化されていません。
function exportSheetAsPdf() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const url = ss.getUrl().replace(/\/edit.*$/, '')
+ '/export?exportFormat=pdf&format=pdf&size=A4&portrait=true'
+ '&fitw=true&sheetnames=false&printtitle=false&gridlines=false'
+ '&gid=' + ss.getActiveSheet().getSheetId();
const blob = UrlFetchApp.fetch(url, {
headers: { 'Authorization': 'Bearer ' + ScriptApp.getOAuthToken() }
}).getBlob().setName('report.pdf');
MailApp.sendEmail({ to: '[email protected]', subject: 'Weekly Report PDF',
body: 'Attached.', attachments: [blob] });
}
外部 API 呼び出し
// GET
function fetchData() {
const r = UrlFetchApp.fetch('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer ' + getApiKey() } });
return JSON.parse(r.getContentText());
}
// POST (muteHttpExceptions to handle errors yourself)
function postData(payload) {
const r = UrlFetchApp.fetch('https://api.example.com/submit', {
method: 'post', contentType: 'application/json',
payload: JSON.stringify(payload), muteHttpExceptions: true });
if (r.getResponseCode() !== 200) throw new Error('API error: ' + r.getContentText());
return JSON.parse(r.getContentText());
}
データ検証用のドロップダウン
// Dropdown from list
const rule = SpreadsheetApp.newDataValidation()
.requireValueInList(['Option A', 'Option B', 'Option C'], true)
.setAllowInvalid(false).setHelpText('Select an option').build();
sheet.getRange('C3:C50').setDataValidation(rule);
// Dropdown from range (e.g. a Lookups sheet)
const rule2 = SpreadsheetApp.newDataValidation()
.requireValueInRange(ss.getSheetByName('Lookups').getRange('A1:A100')).build();
sheet.getRange('B3:B50').setDataValidation(rule2);
Properties Service(永続ストレージ)
スコープは 3 種類あります。PropertiesService.getScriptProperties()(共有)、.getUserProperties()(ユーザーごと)、.getDocumentProperties()(スプレッドシートごと)です。いずれも .setProperty(key, value) / .getProperty(key) を使います。上限は 500 KB です。
レシピ
完了した行を自動アーカイブする
ステータスが「Complete」の行を Archive シートに移動します。行インデックスがずれないよう、下から上へ処理します。
function archiveCompleted() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const source = ss.getSheetByName('Active');
const archive = ss.getSheetByName('Archive');
const data = source.getDataRange().getValues();
const statusCol = 4; // column E (0-indexed)
for (let i = data.length - 1; i >= 1; i--) {
if (data[i][statusCol] === 'Complete') {
archive.appendRow(data[i]);
source.deleteRow(i + 1); // +1 for 1-indexed rows
}
}
SpreadsheetApp.flush();
}
重複を検出してハイライトする
パターン:getValues() で列を読み込み、オブジェクトで既出の値を追跡し、元の行と重複行の両方を setBackground('#f4cccc') でハイライトします。すべてのデータを 1 回の getValues() 呼び出しで処理した後、背景色を個別に設定します(点在する箇所をハイライトするには、これが避けられません)。
メールを一括送信する
重要なパターン:送信前に MailApp.getRemainingDailyQuota() を確認し、行ごとにステータスを記録して、送信処理をそれぞれ try/catch で囲みます。
function sendBatchEmails() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Recipients');
const data = sheet.getRange('A2:C' + sheet.getLastRow()).getValues(); // Email, Name, Status
const remaining = MailApp.getRemainingDailyQuota();
if (remaining < data.length) {
SpreadsheetApp.getUi().alert('Only ' + remaining + ' emails left. Need ' + data.length);
return;
}
let sent = 0;
for (let i = 0; i < data.length; i++) {
const [email, name, status] = data[i];
if (!email || status === 'Sent') continue;
try {
MailApp.sendEmail({ to: email, subject: 'Your Weekly Update',
htmlBody: '<p>Hi ' + name + ',</p><p>Here is your update...</p>' });
sheet.getRange(i + 2, 3).setValue('Sent'); sent++;
} catch (e) { sheet.getRange(i + 2, 3).setValue('Error: ' + e.message); }
}
SpreadsheetApp.flush();
}
サマリーダッシュボードを生成する
パターン:週ごとの番号付きタブ(01-52)をループし、それぞれからサマリーのセルを読み込み、集計した行を Summary シートに書き込みます。ss.getSheetByName(tabName) を使って反復し、シートが存在しない場合は ss.insertSheet('Summary') で作成します。最後に summary.autoResizeColumns() を呼び出し、戻る前に flush() を実行します。
エラーハンドリング
外部呼び出しは必ず try/catch で囲みます。HTTP エラーを自分で処理するには muteHttpExceptions: true を使います。ダイアログのエラーハンドラーに渡す場合は、エラーを再スローします。
function fetchExternalData() {
try {
const response = UrlFetchApp.fetch('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer ' + getApiKey() },
muteHttpExceptions: true
});
if (response.getResponseCode() !== 200)
throw new Error('API returned ' + response.getResponseCode());
return JSON.parse(response.getContentText());
} catch (e) { Logger.log('Error: ' + e.message); throw e; }
}
エラーの防止
| よくある間違い | 修正方法 |
|---|---|
| ダイアログから関数を呼び出せない | 関数名の末尾にある _ を削除する |
| 大量のデータを扱うとスクリプトが遅い | getValues()/setValues() による一括操作を使う |
| ダイアログを閉じた後、変更が反映されない | 戻る前に SpreadsheetApp.flush() を追加する |
onEdit からメールを送信できない | ScriptApp.newTrigger() でインストール型トリガーを使う |
| カスタム関数がタイムアウトする | 30s の制限があります。簡素化するか、通常の関数に移します |
setTimeout が見つからない | Utilities.sleep(ms) を使う(ブロッキング処理) |
| スクリプトが 6 min を超える | 処理を分割し、バッチ処理には時間主導型トリガーを使う |
| 認証ポップアップが表示されない | ユーザーが Advanced > Go to (unsafe) > Allow をクリックする必要があります |
デバッグ
- Logger.log() / console.log() -- Apps Script エディターで、表示 > 実行ログ
- 手動で実行 -- エディターの関数ドロップダウンで関数を選択 > 実行
- 実行数タブ -- 最近のすべての実行と、エラーおよびスタックトレースを表示します
- トリガーの失敗 -- script.google.com > マイプロジェクト > 実行数
- デプロイ前に、必ずシートのコピーでテストしてください
デプロイチェックリスト
- HTML ダイアログから呼び出されるすべての関数が公開されている(末尾にアンダースコアがない)
- 変更を行う関数から戻る前に
SpreadsheetApp.flush()が呼び出されている - 外部 API 呼び出しと MailApp の周囲にエラー処理(try/catch)がある
- 設定定数がファイルの先頭にある
- インストール手順を記載したヘッダーコメントがある
- シートのコピーでテスト済み
- マルチユーザーの動作を考慮している(権限の違い、アクティブシートの違い)
- 時間のかかる処理ではモーダル形式の進行状況ダイアログを使用している
- シート名をハードコードしていない。設定定数を使用している
- 一括送信の前にメールの送信上限を確認している
必要に応じて Apps Script のドキュメントから再構成する
- 行/列の表示・非表示 —
sheet.hideRows()、showRows()、isRowHiddenByUser() - 書式設定 —
setBackground()、setFontWeight()、setBorder()、setNumberFormat()、条件付き書式 - データ保護 —
range.protect()、setUnprotectedRanges()、編集者の管理 - 複数のシート —
getSheetByName()、番号付きタブのループ処理、copyTo()、insertSheet() - 行の自動採番 — 列 B が編集されたときに列 A に自動採番する
onEditトリガー - Google Chat のウェブフック — JSON ペイロードを指定して
chat.googleapis.comに POST