mcpField
G
google-apps-script

google-apps-script

github.com/jezweb生産性1.1k2.0k 回インストール3 週間前に更新
リポジトリ

google-apps-script について

google-apps-script は、Google Sheets や Workspace アプリ向けの自動化スクリプト作成を支援するスキルです。メニュー、トリガー、サイドバー、メール、PDF出力、外部API呼び出しに加え、処理性能や認可の注意点も扱います。

主な機能

  1. 01メニュー、ダイアログ、サイドバー用スクリプトの作成
  2. 02編集、時間指定、フォームのトリガー設定
  3. 03メール送信とPDF出力の自動化
  4. 04外部API呼び出しとエラー処理
  5. 05一括処理と認可に関する指針

活用シーン

  1. 01セル編集時のタイムスタンプ自動入力
  2. 02シートの定期集計とレポートのメール送信
  3. 03シートのPDF出力と配布
  4. 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:インストール手順を案内する

すべてのスクリプトは同じ方法でインストールします。

  1. Google スプレッドシートを開きます
  2. 拡張機能 > Apps Script
  3. エディタ内の既存のコードを削除します
  4. スクリプトを貼り付けます
  5. 保存 をクリックします
  6. Apps Script のタブを閉じます
  7. スプレッドシートを再読み込みします(ページ読み込み時に 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 の代替機能を使用してください):

利用できない APIApps Script の代替機能
setTimeout / setIntervalUtilities.sleep(ms)(ブロッキング)
fetchUrlFetchApp.fetch()
FormDataペイロードを手動で構築
URL文字列操作
cryptoUtilities.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 min1 回の実行につき 6 min
時間主導型トリガーの実行時間30 min30 min
トリガーの 1 日あたりの合計実行時間90 min6 hours
トリガーの合計数スクリプトごとにユーザーあたり 20スクリプトごとにユーザーあたり 20
1 日あたりのメール宛先数1001,500
1 日あたりの URL Fetch 呼び出し数20,000100,000
プロパティのストレージ500 KB500 KB
カスタム関数の実行時間30 seconds30 seconds
同時実行数3030

モーダル進捗ダイアログ

自動的に閉じるスピナーを使って、時間のかかる処理中のユーザー操作をブロックします。数秒以上かかる処理には、どのようなものでも使用してください。

パターン:メニュー関数 > 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

コメント