スプレッドシートからGoogleカレンダーに予定を自動で登録するには、予定の一覧を表にしておき、GAS(Google Apps Script)で1行ずつ CalendarApp の createEvent を呼びます。
登録済みIDを表へ残し、同じスクリプトの同時実行をロックで制限します。
登録前には「処理中」を保存し、失敗した行を自動で再登録しない例です。
ただし、コピーした行や人の同時編集を含め、あらゆる二重登録を防ぐ保証はありません。
この記事は、イベントや訪問の予定をスプレッドシートで管理していて、カレンダーへの入力を手で繰り返したくない方に向けたものです。
掲載コードは2026年10月23日に見直しました。
Node.jsの模擬環境で、時刻の検証、送信なしの初期状態、処理中と登録済みの扱い、ロック解放を確認しています。
実際のGoogleカレンダーへの登録は試していないため、使う前にテスト用の行で確かめてください。
表を用意する
「予定」という名前のシートに、次の列を作ります。
| 列 | 入力内容 | 例 |
|---|---|---|
| A | タイトル | 定例会議 |
| B | 日付 | 2026/10/20 |
| C | 開始時刻(終日なら空欄) | 10:00 |
| D | 終了時刻(終日なら空欄) | 11:30 |
| E | 場所 | 会議室A |
| F | 説明 | 週次の進捗確認 |
| G | 登録済みID(空欄のまま始める) | スクリプトが書き込む |
| H | メモ(空欄のまま始める) | 手動確認用(コードは変更しない) |
日付と時刻は、文字ではなく日付・時刻としてセルに入れます。
このコードの時刻付き予定は同じ日の中で終わるものが対象で、23時から翌日1時などの日をまたぐ予定には対応していません。
この例は見出しの次の2~11行を対象にします。
シートの12行目以降に値があると、A~H列以外の値でも処理を停止します。
A~H列は上表の順序で、結合セルや数式を含めず入力してください。
不正な日付・片方だけの時刻・文字列の時刻は、終日予定へ変換せずエラーで止めます。
実行中は編集・並べ替え・行の追加削除をしないでください。
コード
スプレッドシートの「拡張機能」から「Apps Script」を開き、次のコードを貼り付けて保存します。
// 練習用シートとテスト用カレンダーで使う
const SHEET_NAME = '予定';
const CALENDAR_ID = ''; // テスト用カレンダーのIDを必ず設定する
const DRY_RUN = true; // trueでは予定もセルも変更しない
function registerEvents() {
if (!CALENDAR_ID) throw new Error('テスト用カレンダーのIDを設定してください。');
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) throw new Error('別の実行が処理中です。');
try {
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book && book.getSheetByName(SHEET_NAME);
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
if (!sheet || !calendar) throw new Error('シートまたはカレンダーを確認してください。');
const tz = Session.getScriptTimeZone();
if (book.getSpreadsheetTimeZone() !== tz || calendar.getTimeZone() !== tz) {
throw new Error('シート・スクリプト・カレンダーのタイムゾーンをそろえてください。');
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
if (lastRow > 11) throw new Error('この練習例は10行までです。');
const range = sheet.getRange(2, 1, lastRow - 1, 8);
if (range.isPartOfMerge() || range.getFormulas().some(row => row.some(Boolean))) {
throw new Error('練習範囲は結合セル・数式を含めず値で入力してください。');
}
const values = range.getValues();
for (let i = 0; i < values.length; i++) {
const row = values[i];
const [title, date, start, end, location, description, registeredId] = row;
if (registeredId || row.every(value => value === '')) continue;
if (typeof title !== 'string' || !title.trim() || !validDate(date)) {
throw new Error((i + 2) + '行目のタイトル・日付を確認してください。');
}
const allDay = start === '' && end === '';
if (!allDay && (!validDate(start) || !validDate(end))) {
throw new Error((i + 2) + '行目は開始・終了の両方に時刻を入れてください。');
}
const startTime = allDay ? date : combine(date, start);
const endTime = allDay ? null : combine(date, end);
if (!allDay && endTime <= startTime) throw new Error('終了は開始より後にしてください。');
if (DRY_RUN) { console.log('登録候補の行: ' + (i + 2)); continue; }
const target = sheet.getRange(i + 2, 1, 1, 8);
if (JSON.stringify(target.getValues()[0]) !== JSON.stringify(row)) {
throw new Error('入力が変更されました。停止します。');
}
sheet.getRange(i + 2, 7).setValue('処理中');
SpreadsheetApp.flush();
const options = { location: String(location), description: String(description) };
const event = allDay ? calendar.createAllDayEvent(title, date, options)
: calendar.createEvent(title, startTime, endTime, options);
// 作成後の失敗では「処理中」が残り、自動では再登録しない
const expected = row.slice(); expected[6] = '処理中';
if (JSON.stringify(target.getValues()[0]) !== JSON.stringify(expected)) {
throw new Error('作成後に行の変更を検出しました。カレンダーと照合してください。');
}
sheet.getRange(i + 2, 7).setValue(event.getId());
SpreadsheetApp.flush();
}
} finally { lock.releaseLock(); }
}
function validDate(value) {
return value instanceof Date && !Number.isNaN(value.getTime());
}
function combine(date, time) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.getHours(), time.getMinutes());
}CALENDAR_IDに自分が予定を作成できるテスト用カレンダーのIDを設定します。
DRY_RUN = trueのまま、関数一覧でregisterEventsを選んで実行してください。
候補の行番号だけをログに出し、予定もセルも変更しません。
入力と対象カレンダーを確認してからfalseへ変更し、少数の架空の予定で試します。
途中の行でエラーになっても、それより前に作成された予定やG列のIDは取り消されません。
コードの動き
| 処理 | していること |
|---|---|
| 登録済みの確認 | G列にIDや処理中がある行、全項目が空の行は飛ばす |
| 時刻ありの予定 | 日付と時刻を組み合わせて createEvent で登録する |
| 終日の予定 | 開始時刻と終了時刻が空欄なら createAllDayEvent で登録する |
| 時刻の誤り | 片方だけ・文字列の時刻、終了が開始以前ならエラーで停止する |
| IDの書き戻し | 登録した予定のIDをG列に書き、次からは飛ばす |
Apps Scriptのリファレンスでは、createEvent はタイトル、開始と終了の日時、場所や説明などのオプションを受け取り、作った予定を返すと説明されています(Apps Script:Class Calendar)。
CALENDAR_IDが空欄なら停止します。
意図しないデフォルトカレンダーへの登録を避けるため、登録先を明示してください。
共有のカレンダーなど別のカレンダーに登録する場合は、そのカレンダーの設定画面に表示されるカレンダーIDを入れます。
タイムゾーンに注意する
リファレンスでは、createEvent でタイムゾーンを指定しない場合、時刻はスクリプトのタイムゾーンで解釈され、カレンダーのタイムゾーンと違うことがあると説明されています。
Apps Scriptのプロジェクトの設定で、タイムゾーンが日本(東京)など、実際に使う地域になっているかを確かめてください。
予定の時刻が数時間ずれて登録される場合は、まずここを確かめます。
招待メールと上限
参加者を招待したい場合は、オプションに guests(カンマ区切りのメールアドレス)を加えます。
リファレンスでは、招待メールを送るかどうかを決める sendInvites の初期値は false と説明されています。
Apps Scriptの上限の一覧では、1日に作れるカレンダーの予定は、無料のアカウントで5,000件、Google Workspaceのアカウントで10,000件、1回の実行時間は6分までとなっています(Apps Script:Quotas for Google Services)。
掲載コードには、大量の行を自動で分割して再開する機能はありません。
10行を超える本番運用には、処理範囲・再開位置・作成済みIDの保存と復旧を扱う別の設計が必要です。
実行時間の制限への対処は、getValuesとsetValuesで一括処理する方法も参考になります。
自動で実行する
定期実行を試す場合は、手動での登録と再実行の確認を終えてから、時間主導型トリガーへregisterEventsを登録します。
このコードでは、定期実行でもシートの2~11行という制限は変わりません。
トリガーの作成者には、対象シートへの編集権限とカレンダーへの予定作成権限が必要です。
実行するアカウントについてはGoogle公式のインストール型トリガーガイドも確認してください。
G列にIDまたは「処理中」がある行は飛ばします。
「処理中」は作成前・作成済み・結果不明のいずれもあり得るため、カレンダーと照合せず消して再実行しないでください。
IDを消した行やコピーした行は別扱いになるので、定期実行の前に復旧と重複確認の運用を決めます。
トリガーの設定方法は、GASのトリガーの設定方法で説明しています。
登録した後に確かめること
- カレンダーで、予定の日付と時刻が表と同じになっている
- 終日の予定が、終日として表示されている
- G列にIDが入り、もう一度実行しても予定が増えない
- 実行ログのエラーを確認し、「処理中」の行は作成状況をカレンダーと照合した。H列はこの例では変更しない
G列にIDがある行は処理を飛ばすため、表のタイトルや時刻を直しても、登録済みの予定は更新されません。
表から予定を消しても、カレンダーの予定は自動では消えません。
予定を取り消す場合は、カレンダー側でも削除してください。
祝日や休日にスクリプトを動かさないようにしたい場合は、祝日・休日・指定した日はスクリプトをスキップする方法を参考にしてください。
