確認日:2026年9月24日。Google Apps Script の公式ドキュメント(CalendarApp・SpreadsheetApp のリファレンス、割り当て、トリガー、承認のページ)で API 名と上限の値を確認しました。サンプルコードは Node.js で構文チェックを行い、GAS の部品を模したテストで処理の流れを確かめています(実際の Google アカウント上での実行結果ではありません)。この記事は2025年6月に公開した「スプシの予定をGoogleカレンダーへ──イベント調整が劇的にラクになる自動化術」を、今の情報に合わせて全面的に書き直したものです。
はじめに:この記事で分かること
イベントや会議の予定をスプレッドシートで管理していて、同じ内容を Google カレンダーにも1件ずつ手で入れている。そんな作業は Google Apps Script(GAS、ガス)で自動化できます。GAS は Google が提供するプログラムの実行環境で、ブラウザだけで使え、スプレッドシートやカレンダーを操作できます。
この記事では、プログラミングが初めての方でも最後まで進められるように、次の順番で説明します。
- 全体の仕組みと、シートの列の決め方
- スクリプトエディタの開き方と、そのまま使えるサンプルコード
- 二重登録を防ぐ工夫(イベントIDをシートに書き戻す)と、日付・時刻・終日予定の扱い
- 初回の承認画面の進め方
- メニューからの実行と、トリガーによる自動実行
- 1日の上限(割り当て)と、よくあるエラー
- Gemini・ChatGPT・Claude などの AI にコードを書かせるときのコツと注意
全体の仕組み:スプレッドシートを「予定の原本」にする
考え方はシンプルです。予定はスプレッドシートだけに入力し、カレンダーへの登録は GAS に任せます。入力する場所が1つになるので、書き写しのミスや登録漏れが減ります。
- スプレッドシートに予定を1行ずつ書く
- メニューのボタン(または1時間ごとの自動実行)で GAS を動かす
- GAS がまだ登録していない行だけを Google カレンダーに登録する
- 登録できた行には、カレンダー側の「イベントID」を書き戻す
4つ目がこの記事のポイントです。旧版の記事では「カレンダーの予定をいったん消して、全部作り直す」方法を紹介していましたが、これだと手でカレンダーに加えた変更が消えたり、処理が増えて上限に近づいたりします。今回は、登録済みの行に目印(イベントID)を残し、次回からは飛ばす方法に変えました。
使う主な部品
| 部品(API) | 役割 |
|---|---|
SpreadsheetApp | スプレッドシートを読み書きする |
CalendarApp | Google カレンダーを操作する。createEvent() で時間指定の予定、createAllDayEvent() で終日の予定を作る |
CalendarEvent.getId() | 作った予定の ID(公式には iCalUID)を取り出す |
ScriptApp | トリガー(自動実行の設定)を作る・消す |
LockService | 同じ処理が同時に2回動かないように鍵をかける |
GAS は個人の Google アカウント(gmail.com)でも Google Workspace のアカウントでも使えます。ただし後で説明するとおり、1日に使える量(割り当て)がアカウントの種類で違います。
準備:シートの列を決める
新しいスプレッドシートを作り、シートの名前を「予定」に変えます(シート名はコードの設定で変えられます)。1行目を見出しにして、次のように列を用意します。
| 列 | 見出し | 入れる内容 | 例 |
|---|---|---|---|
| A | 日付 | 予定の日。日付として入力する | 2026/10/01 |
| B | 開始 | 開始時刻。空なら終日の予定になる | 9:30 |
| C | 終了 | 終了時刻。空なら開始から60分 | 11:00 |
| D | タイトル | カレンダーに出る予定名(必須) | 定例会議 |
| E | 場所 | 任意 | 会議室A |
| F | メモ | 予定の説明欄に入る。任意 | 資料を事前共有 |
| G | 終日 | チェックボックス。オンなら終日の予定 | ☑ |
| H | イベントID | 手で入力しない。GAS が書き込む | (自動) |
| I | 状態 | 手で入力しない。登録日時やエラー内容が入る | (自動) |
入力のコツ
- 日付と時刻は、文字ではなく日付・時刻として入力するのがおすすめです。セルに「2026/10/01」「9:30」と打てば、スプレッドシートが自動で日付・時刻として扱います。サンプルコードは「2026/10/01」「9:30」の形の文字でも読めるようにしてあります。
- G 列は、列を選んで「挿入」→「チェックボックス」で作れます。
- H 列・I 列は GAS が書き込む場所です。H 列に何か入っている行は「登録済み」とみなして飛ばします。
- 並べ替えをするときは、H 列・I 列も含めて行全体を並べ替えてください。ID だけがずれると、別の予定と結び付いてしまいます。
タイムゾーンをそろえる
時刻のずれを防ぐため、スプレッドシートとスクリプトのタイムゾーンを同じにしておきます。日本で使うなら、どちらも「(GMT+09:00) 東京」にします。
- スプレッドシートのタイムゾーン:「ファイル」→「設定」→「全般」タブの「タイムゾーン」
- スクリプトのタイムゾーン:スクリプトエディタ左側の「プロジェクトの設定」(歯車のアイコン)→「タイムゾーン」
公式ドキュメント(Script Projects)にあるとおり、スクリプトは実行するときにプロジェクトのタイムゾーンを使います。サンプルコードは、2つのタイムゾーンが違う場合にエラーで止まるようにしてあります。
スクリプトエディタを開いてコードを貼る
スプレッドシートに「くっついた」スクリプト(公式にはコンテナバインド スクリプトと呼びます)を作ります。このスクリプトは、自分が入っているスプレッドシートを ID の指定なしで操作できます。
- 予定を入れたスプレッドシートを開く
- メニューの「拡張機能」→「Apps Script」をクリックする(公式の手順)
- 新しいタブでスクリプトエディタが開く。最初から入っている
function myFunction() {…}を全部消す - 次の章のサンプルコードを貼り付ける
- 上部のプロジェクト名(「無題のプロジェクト」)をクリックして、「カレンダー連携」など分かりやすい名前に変える
- フロッピーディスクのアイコン(または Ctrl+S / Mac は ⌘+S)で保存する
GAS の画面の基本操作に慣れたい方は、同じくスプレッドシートを GAS で操作する スプレッドシートの <br> タグを改行に変換する方法 も参考になります。
サンプルコード全文(コピーして使えます)
変更が必要なのは、先頭の CONFIG(設定)だけです。自分のメインのカレンダーに登録するなら、そのままで動きます。
/**
* スプレッドシートの予定一覧を Google カレンダーに登録するスクリプト
* @OnlyCurrentDoc
*/
// ===== 設定(ここだけ自分の環境に合わせて変える) =====
const CONFIG = {
SHEET_NAME: '予定', // 予定一覧のシート名
CALENDAR_ID: '', // 空ならメインのカレンダー。別のカレンダーはIDを入れる
HEADER_ROWS: 1, // 見出しの行数
DEFAULT_MINUTES: 60, // 終了時刻が空のときの長さ(分)
TIME_LIMIT_MS: 5 * 60 * 1000, // 5分たったら止める(1回の実行の上限は6分)
};
// 列の位置(A列 = 1)
const COL = {
DATE: 1, // A: 日付
START: 2, // B: 開始時刻
END: 3, // C: 終了時刻
TITLE: 4, // D: タイトル
LOCATION: 5, // E: 場所
MEMO: 6, // F: メモ
ALL_DAY: 7, // G: 終日(チェックボックス)
EVENT_ID: 8, // H: イベントID(スクリプトが書き込む)
STATUS: 9, // I: 状態(スクリプトが書き込む)
};
// スプレッドシートを開いたときにメニューを追加する
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('カレンダー連携')
.addItem('未登録の予定をカレンダーに登録', 'registerFromMenu')
.addSeparator()
.addItem('1時間ごとの自動登録をオンにする', 'installHourlyTrigger')
.addItem('自動登録をオフにする', 'uninstallTrigger')
.addToUi();
}
// メニューから実行したとき(結果を画面に表示する)
function registerFromMenu() {
const r = registerEvents();
let message = `登録 ${r.created} 件 / 対象外 ${r.skipped} 件 / エラー ${r.errors} 件`;
if (r.stoppedEarly) {
message += '\n時間がかかったため途中で止めました。もう一度実行すると続きを登録します。';
}
SpreadsheetApp.getUi().alert(message);
}
// トリガーからも呼ばれる本体(画面表示はしない)
function registerEvents() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(10 * 1000)) {
throw new Error('別の登録処理が実行中です。少し待ってからやり直してください。');
}
try {
return processRows_();
} finally {
lock.releaseLock();
}
}
function processRows_() {
const startedAt = Date.now();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
throw new Error(`シート「${CONFIG.SHEET_NAME}」が見つかりません。`);
}
const calendar = CONFIG.CALENDAR_ID
? CalendarApp.getCalendarById(CONFIG.CALENDAR_ID)
: CalendarApp.getDefaultCalendar();
if (!calendar) {
throw new Error('カレンダーが見つからないか、アクセスする権限がありません。');
}
// シートとスクリプトのタイムゾーンがずれていると、時刻がずれて登録される
const tz = ss.getSpreadsheetTimeZone();
if (tz !== Session.getScriptTimeZone()) {
throw new Error(`タイムゾーンが一致しません(シート: ${tz} / スクリプト: ${Session.getScriptTimeZone()})。`);
}
const result = { created: 0, skipped: 0, errors: 0, stoppedEarly: false };
const lastRow = sheet.getLastRow();
if (lastRow <= CONFIG.HEADER_ROWS) {
return result;
}
const firstRow = CONFIG.HEADER_ROWS + 1;
const rows = sheet.getRange(firstRow, 1, lastRow - CONFIG.HEADER_ROWS, COL.STATUS).getValues();
for (let i = 0; i < rows.length; i++) {
if (Date.now() - startedAt > CONFIG.TIME_LIMIT_MS) {
result.stoppedEarly = true;
break;
}
const row = rows[i];
const rowNumber = firstRow + i;
const title = String(row[COL.TITLE - 1]).trim();
const savedId = String(row[COL.EVENT_ID - 1]).trim();
// タイトルが空の行と、イベントIDがある(登録済みの)行は飛ばす
if (!title || savedId) {
result.skipped++;
continue;
}
try {
const event = createEventFromRow_(calendar, row, tz);
const now = Utilities.formatDate(new Date(), tz, 'yyyy/MM/dd HH:mm');
// 1行ごとにすぐ書き戻す(途中で止まっても二重登録しないため)
sheet.getRange(rowNumber, COL.EVENT_ID, 1, 2).setValues([[event.getId(), `登録済み ${now}`]]);
result.created++;
} catch (err) {
sheet.getRange(rowNumber, COL.STATUS).setValue(`エラー: ${err.message}`);
result.errors++;
}
}
return result;
}
function createEventFromRow_(calendar, row, tz) {
const title = String(row[COL.TITLE - 1]).trim();
const day = toDateParts_(row[COL.DATE - 1], tz);
const options = {};
const location = String(row[COL.LOCATION - 1]).trim();
const memo = String(row[COL.MEMO - 1]).trim();
if (location) options.location = location;
if (memo) options.description = memo;
// 終日にチェックがある、または開始時刻が空なら「終日の予定」
const isAllDay = row[COL.ALL_DAY - 1] === true || row[COL.START - 1] === '';
if (isAllDay) {
return calendar.createAllDayEvent(title, new Date(day.y, day.m - 1, day.d), options);
}
const s = toTimeParts_(row[COL.START - 1], tz);
const start = new Date(day.y, day.m - 1, day.d, s.h, s.min);
let end;
if (row[COL.END - 1] === '') {
end = new Date(start.getTime() + CONFIG.DEFAULT_MINUTES * 60 * 1000);
} else {
const e = toTimeParts_(row[COL.END - 1], tz);
end = new Date(day.y, day.m - 1, day.d, e.h, e.min);
}
if (end <= start) {
throw new Error('終了時刻が開始時刻と同じか、それより前です。');
}
return calendar.createEvent(title, start, end, options);
}
// 日付のセル → { y, m, d }
function toDateParts_(value, tz) {
if (isDate_(value)) {
const [y, m, d] = Utilities.formatDate(value, tz, 'yyyy-M-d').split('-').map(Number);
return { y, m, d };
}
const match = String(value).trim().match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$/);
if (!match) {
throw new Error('日付を読み取れません(例: 2026/10/01)。');
}
return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
}
// 時刻のセル → { h, min }
function toTimeParts_(value, tz) {
if (isDate_(value)) {
const [h, min] = Utilities.formatDate(value, tz, 'H:m').split(':').map(Number);
return { h, min };
}
const match = String(value).trim().match(/^(\d{1,2}):(\d{2})$/);
if (!match) {
throw new Error('時刻を読み取れません(例: 9:30)。');
}
return { h: Number(match[1]), min: Number(match[2]) };
}
function isDate_(value) {
return Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value.getTime());
}
// 1時間ごとに registerEvents を実行するトリガーを作る
function installHourlyTrigger() {
deleteTriggers_();
ScriptApp.newTrigger('registerEvents').timeBased().everyHours(1).create();
SpreadsheetApp.getActiveSpreadsheet().toast('1時間ごとの自動登録をオンにしました。');
}
function uninstallTrigger() {
const count = deleteTriggers_();
SpreadsheetApp.getActiveSpreadsheet().toast(`自動登録をオフにしました(${count} 件削除)。`);
}
function deleteTriggers_() {
const targets = ScriptApp.getProjectTriggers()
.filter((t) => t.getHandlerFunction() === 'registerEvents');
targets.forEach((t) => ScriptApp.deleteTrigger(t));
return targets.length;
}
コードの流れ
onOpen():スプレッドシートを開いたときに「カレンダー連携」メニューを追加するregisterFromMenu():メニューから呼ばれる。登録を実行し、件数を画面に表示するregisterEvents():本体。鍵をかけてからprocessRows_()を呼ぶ。トリガーからもこれを呼ぶprocessRows_():シートを読み、未登録の行だけカレンダーに登録して ID を書き戻すcreateEventFromRow_():1行分の予定を作る。終日か時間指定かを判断するtoDateParts_()/toTimeParts_():セルの値を「年・月・日」「時・分」に分解するinstallHourlyTrigger()ほか:1時間ごとの自動実行を設定・解除する
名前の最後が _(アンダースコア)の関数は、GAS では「非公開(private)の関数」の目印です。このサンプルでは、メニューやトリガーから直接呼ばない部品の関数にだけ付けています。
サンプルコードの工夫をやさしく解説
1. 二重登録を防ぐ:イベントIDを書き戻す
createEvent() は、作った予定を CalendarEvent として返します。その getId() で予定の ID を取り出し、H 列に書き込みます。次に実行したとき、H 列に ID がある行は飛ばすので、同じ予定が2回登録されません。
ID は1行登録するごとにすぐ書き込みます。まとめて最後に書く方が速いのですが、途中で止まったときに「カレンダーには登録済みなのに ID がない」行が残り、次回に二重登録されてしまうからです。
- もう一度登録し直したいとき:その行の H 列(ID)を消してから実行します(カレンダー側の古い予定は手で消してください)。
- シートを書き換えても、登録済みの予定は自動では更新されません。更新にも対応したいときは、ID から予定を取り出す
calendar.getEventById(ID)と、setTitle()・setTime()などの更新用メソッドを組み合わせます。
Calendar クラスのリファレンスによると、CalendarApp.getEventById() はメインのカレンダーの予定しか返しません。別のカレンダーに登録した場合は、そのカレンダーの getEventById() を使います。また、見つからないときやアクセスできないときは null が返ります。
2. 日付と時刻の扱い
スプレッドシートの日付・時刻のセルは、getValues() で読むと JavaScript の Date(日時を表す値)として返ってきます(Range リファレンス)。時刻だけのセルも Date になり、日付の部分は意味のない値です。
そこでサンプルでは、日付のセルから「年・月・日」だけ、時刻のセルから「時・分」だけを取り出し、new Date(年, 月-1, 日, 時, 分) で組み立て直しています。取り出すときは Utilities.formatDate() にスプレッドシートのタイムゾーンを渡すので、画面に見えているとおりの値になります。
旧版の記事では「日付を文字にして時刻の文字とつなげ、new Date() に渡す」書き方をしていました。時刻のセルが Date で返ってくると、この方法は正しく動かないことがあるため、書き方を改めました。
3. 終日の予定
G 列にチェックがある行と、開始時刻が空の行は、createAllDayEvent(タイトル, 日付, オプション) で終日の予定にします。公式リファレンスでは、終日の予定は「日付だけが使われ、時刻は無視される」と説明されています。
数日にまたがる終日の予定を作る createAllDayEvent(タイトル, 開始日, 終了日) もあります。このとき終了日はその日を含まない(公式の表現では exclusive)ので、10月1日〜3日の3日間なら、終了日は10月4日にします。サンプルでは簡単にするため1日だけの終日予定にしています。
4. エラーが起きても止まらない
- 1行ごとに
try…catch(エラーを受け止める書き方)で囲み、失敗した行は I 列に「エラー: 理由」と書いて次の行へ進みます。 - エラーの行は ID が入らないので、シートを直して再実行すると、その行だけ登録されます。
- 終了時刻が開始時刻より前の行、日付が読めない行は、登録せずにエラーとして残します。
- 1回の実行は最長6分までなので、5分たったら残りを次回に回して止まります。
LockServiceで鍵をかけ、メニューからの実行と自動実行が重なっても、同時に二重で処理しないようにしています(LockService)。
5. 招待メールは送らない
createEvent() のオプションには guests(招待する人のメールアドレス)や sendInvites(招待メールを送るか)もあります。sendInvites の初期値は false です。サンプルでは参加者を追加していないので、ほかの人にメールが届くことはありません。参加者を入れる場合は、まずテスト用のカレンダーで試してください。
初回の実行と承認画面
GAS がカレンダーやスプレッドシートを操作するには、あなたのアカウントでの許可(承認)が必要です。初回だけ、次の流れで許可します。
- スクリプトエディタ上部の関数の一覧から
registerEventsを選び、「実行」をクリックする - 「承認が必要です」という画面が出たら「権限を確認」をクリックし、自分の Google アカウントを選ぶ
- 個人で作ったスクリプトでは「Google で確認されていません」という警告が出ることがある。自分で書いた(内容を確認した)スクリプトなら、「詳細」→「(プロジェクト名)(安全ではないページ)に移動」と進む
- 求められている権限(カレンダー、このスプレッドシート、トリガーの管理など)を確認し、すべてにチェックを入れて許可する
- 実行が終わったらスプレッドシートを再読み込みする。メニューに「カレンダー連携」が出れば準備完了
画面の文言は、アカウントの種類や時期によって少し違うことがあります。
権限は一部だけ許可することもできる:2025年1月から、スクリプトエディタで実行したときの承認画面では、権限を1つずつ選んで許可できるようになりました(Google Workspace Updates)。カレンダーの権限を外すと、登録の処理でエラーになります。このサンプルでは表示された権限をすべて許可してください。
サンプルの先頭にある @OnlyCurrentDoc という書き込みは、スプレッドシートへのアクセスを「このファイルだけ」に絞るための目印です(公式の説明)。あなたのほかのスプレッドシートまで読める権限を求めないので、安心して許可できます。
許可を取り消したいときは、Google アカウントの「セキュリティ」から、サードパーティのアプリとの接続の一覧を開き、このスクリプトを削除します。
メニューから実行する・トリガーで自動化する
メニューから実行する
スプレッドシートを開くと、onOpen() が自動で動き、「カレンダー連携」メニューが追加されます。予定を書いたら「カレンダー連携」→「未登録の予定をカレンダーに登録」を選ぶだけです。スクリプトエディタを開かなくてよいので、チームのほかのメンバーにも使ってもらえます。
onOpen() のように決まった名前で自動実行される関数をシンプルトリガーと呼びます。公式ドキュメント(Simple Triggers)では、次の制限が挙げられています。
- 30秒より長くは動けない
- 承認が必要なサービス(カレンダーなど)は使えない
- ファイルを閲覧のみ・コメントのみの権限で開いたときは動かない
このため、onOpen() ではメニューを出すだけにし、カレンダーへの登録はメニューを押したときに行う作りにしています。
1時間ごとに自動で登録する(時間主導型トリガー)
決まった時間に自動で動かすには、インストール型トリガーの一種である時間主導型トリガーを使います。方法は2つあります。
- メニューから(サンプルの機能):「カレンダー連携」→「1時間ごとの自動登録をオンにする」を選ぶ。コードの
ScriptApp.newTrigger('registerEvents').timeBased().everyHours(1).create()が実行される - スクリプトエディタの画面から:左側の「トリガー」(時計のアイコン)→ 右下の「トリガーを追加」→ 実行する関数に
registerEvents、イベントのソースに「時間主導型」を選び、間隔を決めて保存
時間主導型トリガーで選べる間隔の例です(ClockTriggerBuilder)。
| 書き方 | 意味 |
|---|---|
everyMinutes(n) | n 分ごと。n は 1・5・10・15・30 のどれか |
everyHours(n) | n 時間ごと |
atHour(5).everyDays(1) | 毎日5時台に1回(5時〜6時のどこか) |
nearMinute(30) | 分を指定する(前後15分の幅がある) |
トリガーについて、知っておきたい点をまとめます。
- 実行時刻はきっちりではない:公式ドキュメント(Installable Triggers)によると、たとえば「毎日9時」にすると、9時〜10時の間で時刻が選ばれ、その後は毎日ほぼ同じ時刻に動きます。
- トリガーを作った人のアカウントで動く:誰が予定を書いても、登録はトリガーを作った人の権限で行われます。
- 失敗するとメールが届く:トリガーでエラーが起きると、Google から通知メールが届きます。
- トリガーの実行中はスプレッドシートの画面がないため、
getUi()で画面を出す処理は使えません。サンプルでは、トリガーから呼ぶregisterEvents()には画面表示を入れていません。 - 1人・1スクリプトあたりのトリガーは20個まで。サンプルは、作り直すたびに古いトリガーを消すので増え続けません。
1日の上限(割り当て)を知っておく
GAS には1日に使える量の上限があり、公式には「割り当て(quotas)」と呼ばれます。上限を超えると、スクリプトはエラーで止まります。この記事に関係する主な値は次のとおりです(公式の割り当てページ、2026年9月24日時点。ページの最終更新は2026年9月3日)。
| 項目 | 個人アカウント(gmail.com など) | Google Workspace |
|---|---|---|
| カレンダーの予定の作成 | 1日 5,000 件 | 1日 10,000 件 |
| 1回の実行時間 | 6分 | 6分 |
| トリガーによる実行時間の合計 | 1日 90分 | 1日 6時間 |
| トリガーの数 | 1人・1スクリプトあたり20個 | 1人・1スクリプトあたり20個 |
| 同時に動かせる実行 | 1人あたり30 | 1人あたり30 |
- 割り当てはユーザーごとで、最初の利用から24時間でリセットされます。
- 公式ページには、上限はテストのための目安で、予告なく変わることがあると書かれています。大量に使う前に最新の値を確認してください。
- 1時間ごとのトリガーでも、未登録の行がなければすぐ終わるため、実行時間はほとんど使いません。
よくあるエラーと対処
エラーの内容は、スクリプトエディタ左側の「実行数」で確認できます。console.log() で書いた内容もここに出ます。
| 表示されるメッセージ(例) | 原因 | 対処 |
|---|---|---|
| シート「予定」が見つかりません。 | シート名が設定と違う | シート名を「予定」にするか、CONFIG.SHEET_NAME を直す |
| カレンダーが見つからないか、アクセスする権限がありません。 | カレンダー ID の間違い、または権限がない | カレンダーの「設定と共有」→「カレンダーの統合」にある ID を確認する。予定を作れる権限が必要 |
| タイムゾーンが一致しません | シートとスクリプトのタイムゾーンが違う | 「準備」の章の手順で両方を同じにする |
| Service invoked too many times: Calendar. | 1日に呼び出せる回数を超えた | 翌日まで待つ。一度に大量に登録しない |
| Service invoked too many times in a short time: Calendar. | 短い時間に呼び出しすぎた | 公式ページは、呼び出しの間に Utilities.sleep(1000) を入れる方法を案内している |
| 実行時間の上限を超えたというエラー | 1回の実行が6分を超えた | サンプルは5分たつと自分で止まる。もう一度実行すると続きを処理する |
| Authorization is required to perform that action. | 承認されていない、または権限の一部が許可されていない | エディタから一度実行し、承認画面ですべての権限を許可する |
| I 列に「日付を読み取れません」 | 日付が空、または読めない形 | 「2026/10/01」の形で入力する |
登録したはずの予定が見当たらないときは、別のカレンダー(CONFIG.CALENDAR_ID で指定した先)に入っていないか、カレンダーの表示がオフになっていないかも確認しましょう。
AI にコードを書かせるときのコツと注意
今は、GAS のコードを AI に書いてもらうのが当たり前になりました。旧版の記事でも「困りごとを書き出す→サンプルを貼る→AI に修正を頼む」という流れを勧めていました。この考え方は今も有効ですが、生成されたコードをそのまま信じないことが大切です。
Apps Script エディタの Gemini(2026年9月時点)
公式ドキュメント(Use the Gemini side panel in the Apps Script editor、2026年9月3日更新)によると、Apps Script のエディタ右側に Gemini のパネルがあり、コードの作成・修正・エラーの調査ができます。ただし、2026年9月24日時点では次の条件があります。
- Gemini Beta プログラムに参加している利用者だけが使える機能
- Google Workspace Business Standard / Plus、Enterprise Standard / Plus、Google AI Pro / Ultra などの対象ライセンスが必要
- Workspace では管理者が Gemini の機能を有効にしている必要がある
- 対応言語は英語のみ
- 提案されたコードは差分として表示され、「Accept All」か「Reject All」で全部を採用するか全部を捨てるかを選ぶ(一部だけの採用はできない)
- エラーが出たときは「Fix with Gemini」で原因の説明と修正案を出せる
使えない場合は、ChatGPT や Claude などのチャット型 AI で書いてもらい、エディタに貼り付けて使えば同じことができます。
AI への頼み方のコツ
あいまいに頼むほど、AI は「それらしい」けれど目的に合わないコードを出します。次の情報をはっきり伝えると、手直しが少なくなります。
- シートの形:シート名、何列目に何が入っているか、見出しの行数
- やりたいこと:「未登録の行だけ登録し、イベントIDを H 列に書き戻す」のように具体的に
- 前提:「コンテナバインドのスクリプト」「V8 ランタイム」「タイムゾーンは Asia/Tokyo」
- 守ってほしいこと:「公式リファレンスにあるメソッドだけを使う」「エラーの行は飛ばして理由を書く」「招待メールは送らない」
依頼文の例です。
Google Apps Script で、スプレッドシート「予定」シートの予定を Google カレンダーに登録するコードを書いてください。
- コンテナバインドのスクリプト(V8)。タイムゾーンは Asia/Tokyo
- 1行目は見出し。A:日付 B:開始 C:終了 D:タイトル E:場所 F:メモ G:終日(チェックボックス) H:イベントID I:状態
- H列が空の行だけ登録し、登録したら getId() の値を H 列に書き戻す
- 開始が空、または G 列がオンなら終日の予定にする
- エラーの行は飛ばし、I 列に理由を書く
- 招待メールは送らない。CalendarApp と SpreadsheetApp の公式リファレンスにあるメソッドだけを使う
- 各メソッドが公式リファレンスのどこに載っているかも一緒に教えて
AI に渡す情報の量と質が結果を左右する、という考え方は コーディングエージェントのコンテキスト管理入門 でも詳しく解説しています。
生成されたコードで必ず確認すること
| 確認すること | 理由 |
|---|---|
| 使っているメソッドが公式リファレンスに実在するか | AI は存在しないメソッドを書くことがある。たとえば旧版の記事で紹介した Calendar.Events.batchUpdate() は、Calendar API の Events のメソッド一覧に存在しない |
予定を消す処理(deleteEvent() など)が入っていないか | 条件の書き間違いで、手で入れた大事な予定まで消えるおそれがある |
| 二重登録を防ぐ仕組みがあるか | 何度も実行すると同じ予定が増えていく |
sendInvites や guests の扱い | テスト中にほかの人へ招待メールが飛ぶおそれがある |
| 求められる権限が必要最小限か | 承認画面で、使うはずのないサービス(Gmail 送信など)の権限を求めていたら理由を確かめる |
| 日付・時刻・タイムゾーンの扱い | ずれは見落としやすく、気付いたときには大量の予定がずれていることがある |
- 最初はテスト用のカレンダーを作り、その ID を
CONFIG.CALENDAR_IDに入れて数行だけで試しましょう。 - AI に貼り付けるデータに、個人名・電話番号・社外秘の予定などを含めないようにします。
- AI に任せきりにすると、動いても中身を誰も説明できないコードになりがちです。作業の分け方については AIエージェントに“丸投げ”しても成果が出ない理由 も参考にしてください。
旧版の記事から直したこと
- LINE Notify は終了:旧版で通知先の例に挙げた LINE Notify は、2025年3月31日でサービスを終了しました(LINE Developers のお知らせ)。LINE で通知したい場合は、公式が代わりに案内している Messaging API を検討してください。
- まとめて処理する API:旧版の
Calendar.Events.batchUpdate()は存在しないため削除しました。 - ローカルで開発したい場合:Google 公式のコマンドラインツール clasp を使うと、手元のエディタで GAS のコードを書けます。README によると、インストールは
npm install -g @google/claspで、Node.js 22 以上と Apps Script API の有効化が必要です。手元で AI エージェントにコードを書かせたい方は Claude Code 入門 もどうぞ。
まとめ
- 予定はスプレッドシートだけに入力し、カレンダーへの登録は GAS に任せると、書き写しの手間とミスが減ります。
- 登録した予定の ID をシートに書き戻し、ID がある行は飛ばすことで、二重登録を防げます。
- 日付と時刻は、スプレッドシートのタイムゾーンで「年月日」「時分」に分けてから組み立て直すと、ずれにくくなります。
- 初回は承認画面ですべての権限を許可します。メニューからの実行と、時間主導型トリガーでの自動実行ができます。
- 予定の作成は1日 5,000 件(Workspace は 10,000 件)、1回の実行は6分までなどの上限があります。
- AI にコードを書かせるときは、シートの形と条件を具体的に伝え、メソッドの実在・削除処理・招待メール・権限を必ず確認しましょう。
参考リンク
- Apps Script リファレンス:CalendarApp
- Apps Script リファレンス:Calendar(createEvent・createAllDayEvent・getEventById)
- Apps Script リファレンス:CalendarEvent(getId など)
- Apps Script リファレンス:Range(getValues・setValues)
- Quotas for Google Services(割り当て)
- Simple Triggers / Installable Triggers
- Custom Menus in Google Workspace
- Authorization for Google Services(承認)
- Troubleshooting(よくあるエラー)
- Use the Gemini side panel in the Apps Script editor

