google-apps-script
Google SheetsやWorkspaceの操作をGoogle Apps Scriptで自動化します。カスタムメニュー・トリガー(onEdit/時間駆動/フォーム送信)・ダイアログ・サイドバー・メール一括送信・PDFエクスポート・外部APIとの連携に対応し、Sheetsの行をメールやWebhookから更新したい場合やワークフローのスケジュール実行など、「Sheetsでこの操作をスクリプト化したい」というあらゆる要望に活用できます。
description の原文を見る
Build Google Apps Script automation for Sheets and Workspace. Custom menus, triggers (onEdit / time-driven / form submit), dialogs, sidebars, email batches, PDF export, external API. Use whenever the user wants to automate a Google Sheet, build a Sheets menu / sidebar / dialog, hit a Sheets row from email or a webhook, schedule a Sheets workflow, or asks 'how do I script this in Sheets'.
SKILL.md 本文
Google Apps Script
Google Sheets と Workspace アプリの自動化スクリプトを構築します。スクリプトは Google インフラストラクチャ上でサーバー側で実行され、寛大な無料枠があります。
成果物
- Extensions > Apps Script に貼り付ける Apps Script コード
- カスタムメニュー、ダイアログ、サイドバー
- 自動トリガー (編集時、時間駆動、フォーム送信時)
- メール通知、PDF エクスポート、API 統合
ワークフロー
ステップ 1: 自動化を理解する
ユーザーが何を自動化したいのかを確認します。一般的なシナリオ:
- カスタムメニューとアクション (レポート生成、データ処理)
- 自動トリガー動作 (編集時、フォーム送信時、スケジュール実行)
- データ入力用のサイドバーアプリ
- シートデータからのメール通知
- PDF エクスポートと配布
ステップ 2: スクリプトを生成
下記の構造テンプレートに従います。すべてのスクリプトには、ヘッダーコメント、先頭の設定定数、メニュー設定用の onOpen() が必要です。
ステップ 3: インストール手順を提供
すべてのスクリプトは同じ方法でインストールします:
- Google Sheet を開く
- Extensions > Apps Script
- エディターの既存コードをすべて削除
- スクリプトを貼り付け
- Save をクリック
- Apps Script タブを閉じる
- スプレッドシートをリロード (onOpen はページロード時に実行)
ステップ 4: 初回認可
各ユーザーは初回実行時に Google OAuth 同意画面を取得します。未検証スクリプト (ほとんどの内部スクリプト) の場合、ユーザーは以下をクリックする必要があります:
Advanced > Go to [Project Name] (unsafe) > Allow
これはユーザーごとの 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
}
重大ルール
パブリック関数 vs プライベート関数
末尾が _ (アンダースコア) の関数はプライベートで、クライアント側の HTML から google.script.run 経由で呼び出すことができません。これはサイレントエラー -- エラーなしで呼び出しが機能しません。
// 間違い - ダイアログはこれを呼び出せず、失敗
function doWork_() { return 'done'; }
// 正しい - ダイアログはこれを呼び出せる
function doWork() { return 'done'; }
適用対象: メニューアイテム関数参照も文字列としてパブリック関数名である必要があります。
バッチ操作 (パフォーマンスに重大)
データを一括で読み書きし、決してセルごとに操作しません。差は 70 倍です。
// 遅い (100x100 で 70 秒) - 一度に 1 セルを読み込む
for (let i = 1; i <= 100; i++) {
const val = sheet.getRange(i, 1).getValue();
}
// 速い (1 秒) - すべてを一度に読み込む
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 ダイアログから呼び出される場合。これがないと、ダイアログが「完了」を表示するときに変更が見えないかもしれません。
シンプル vs インストール可能なトリガー
| 機能 | シンプル (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) {
// 使用可能: 基本 JS、Utilities、CacheService
// 使用不可: MailApp、UrlFetchApp、SpreadsheetApp.getUi()、トリガー
return input.toUpperCase();
}
@customfunctionJSDoc タグを含める必要があります- 30 秒の実行制限 (通常の関数では 6 分)
- 認可が必要なサービスにアクセスできません
クォータと制限
| リソース | 無料アカウント | Google Workspace |
|---|---|---|
| スクリプト実行時間 | 6 分 / 実行 | 6 分 / 実行 |
| 時間駆動トリガーの実行時間 | 30 分 | 30 分 |
| トリガーの合計日次実行時間 | 90 分 | 6 時間 |
| トリガーの合計 | ユーザーあたり 20 / スクリプト | ユーザーあたり 20 / スクリプト |
| メール受信者 / 日 | 100 | 1,500 |
| URL Fetch コール / 日 | 20,000 | 100,000 |
| プロパティストレージ | 500 KB | 500 KB |
| カスタム関数の実行時間 | 30 秒 | 30 秒 |
| 同時実行 | 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);
// 引数: メッセージ、タイトル、秒単位の期間 (-1 = 閉じられるまで)
アラートとプロンプトダイアログ
const ui = SpreadsheetApp.getUi();
// Yes/No 確認
const response = ui.alert('Delete this data?', 'This cannot be undone.',
ui.ButtonSet.YES_NO);
if (response === ui.Button.YES) { /* proceed */ }
// 入力をプロンプト
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;
// 列 C が編集されたときに自動タイムスタンプ
sheet.getRange(e.range.getRow(), 4).setValue(new Date());
}
インストール可能なトリガー -- スクリプト経由で作成、セットアップ関数を一度手動で実行:
function createTriggers() {
// 時間駆動: 毎日午前 8 時に実行
ScriptApp.newTrigger('dailyReport')
.timeBased().atHour(8).everyDays(1).create();
// 編集時 (完全な権限: メール送信、URL フェッチ可能)
ScriptApp.newTrigger('onEditFull')
.forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
// フォーム送信時
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: 'worker@example.com',
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: 'boss@example.com', 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 でエラーを自分で処理)
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());
}
データ検証ドロップダウン
// リストからのドロップダウン
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);
// 範囲からのドロップダウン (例: Lookups シート)
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 制限。
レシピ
完了した行を自動アーカイブ
「完了」ステータスの行を 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 でラップしてください。muteHttpExceptions: true を使用して HTTP エラーを自分で処理してください。ダイアログエラーハンドラーの場合は再スロー。
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() 経由のインストール可能トリガーを使用 |
| カスタム関数がタイムアウト | 30 秒制限 -- 簡素化または通常の関数に移動 |
setTimeout が見つからない | Utilities.sleep(ms) を使用 (ブロッキング) |
| スクリプトが 6 分を超える | チャンクに分割、バッチには時間駆動トリガーを使用 |
| 認可ポップアップが表示されない | ユーザーが Advanced > Go to (unsafe) > Allow をクリック必須 |
デバッグ
- Logger.log() / console.log() -- Apps Script エディターで View > Execution Log を表示
- 手動実行 -- エディターのドロップダウンで関数を選択 > Run
- Executions タブ -- 最近の実行すべてをエラーとスタックトレース付きで表示
- トリガー失敗 -- script.google.com > My Projects > Executions
- 常にコピーで テスト デプロイ前にシートをコピーしてテスト
デプロイメントチェックリスト
- 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 ウェブフック —
chat.googleapis.comに JSON ペイロード付きで POST
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- jezweb
- リポジトリ
- jezweb/claude-skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/jezweb/claude-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。