複数のクライアントを抱えて仕事をしていると、半年先までびっしり予定が埋まる。そして、ふとカレンダーを眺めたときに、こう思う瞬間がある。
「この打ち合わせ、いつ入れたんだ?」
「そもそも、誰との約束だっけ……」
「日付変更の操作ミスかな?」
タイトルが雑だったり、当時の自分にしか分からない略称だったりすると、もう手がかりがない。相手に「いつお約束しましたっけ?」と聞くのも気が引ける。
本記事では、この「身に覚えのない予定問題」を、Googleカレンダーの予定が”いつ作成・更新されたか”を調べることで解決するツールを、Claudeとの対話で作った話を紹介する。
例によって動くソースコードを公開しているので、よかったら試してみてね。(注意事項はよく読んで、自己責任で)
そもそも、Googleカレンダーは予定の「作成日」を見せてくれない
身に覚えのない予定があったとき、いちばん知りたいのは「いつこの予定を登録したか」だ。それが分かれば、「ああ、あの日に商談した相手だな」と記憶を辿る起点になる。
ところが困ったことに、Googleカレンダーの標準UIには、予定の作成日を表示する機能がない。 予定をクリックして詳細を開いても、開始・終了時刻や場所は出てくるが、「この予定がいつ作られたか」はどこにも書かれていない。編集しても、更新日が表示されるわけでもない。
つまり標準機能の範囲では、「身に覚えのない予定」を前にしたとき、打てる手は限られる。
- 議事録やメッセンジャー系を片っ端から読み返す(メール・チャット・LINE……)。地道だが、見つからないと徒労に終わる
- 関係しそうな相手に直接聞く。確実ではあるが、毎回やるのは気まずいし失礼にもなりかねない
どちらも筋が悪い。そこで発想を変える。予定を作成した日付さえ分かれば、その日にやり取りした相手から逆算できる——この方針なら、自分のカレンダーの中だけで完結する。
実はこのアイデア、note で Shuhei Maruyama さんが紹介していた手法が元になっている。Google Apps Script(GAS)で getDateCreated() を呼べば、予定の作成日時が取れるというものだ。標準UIにない情報も、APIレベルでは保持されている。これは目から鱗だった。
ただ、元記事のスクリプトはコードエディタで日付を直接書き換えて実行ログを読む形式。毎回コードをいじるのは少し手間だ。そこで今回は、これを日付を選ぶだけで使えるWebアプリに仕立て直し、ついでに「更新日」も取れるよう拡張してみた。
解決策:ClaudeにGASのWebアプリを作ってもらった

やったことはシンプルで、元記事のアイデアをClaudeに伝え、「指定した日付の予定について、作成日と更新日を一覧表示するGASのWebアプリを作って。カレンダーはプルダウンで選べるように」と頼んだだけ。最初の出力で、サーバーサイドの コード.gs とフロントの index.html が揃った動作版が出てきた。
完成したツールの構成
技術的には前回のDrive Copierと同じく、GAS(Google Apps Script)のWebアプリ。HtmlService でHTMLをホストし、google.script.run でサーバーサイド関数を非同期に呼び出す、オーソドックスな2ファイル構成だ。
核になっているのは、CalendarApp.getEventsForDay() で指定日の予定を取得し、各予定に対して getDateCreated()(作成日時)と getLastUpdated()(最終更新日時)を呼ぶ部分。元記事のロジックそのままだが、「作成日」と「更新日」の両方を見られるようにしたことで、「いつ作ったか」だけでなく「あとから編集したか」も分かるようになっている。
GASで完結するので、Google Workspaceの有料契約もクレジットカードも、サーバーも不要。必要なのはGoogleアカウントだけ。デプロイしたURLをブックマークしておけば、いつでも呼び出せる。
できること
- カレンダーをプルダウンで選択。自分がアクセスできる全カレンダーから選べる(メインカレンダーが先頭に来る)
- 日付を選んで「調べる」を押すだけで、その日の予定と作成日・更新日が一覧表示される
- 作成日が新しい順にソート。「最近うっかり入れた予定」を上から確認しやすい
- 作成日と更新日が違う場合だけ更新日も表示。編集済みの予定がひと目で分かる
- 終日/時間帯・場所・参加者(ゲスト)もまとめて表示。作成日と突き合わせれば、相手の特定がぐっと早くなる
コードを書く知識そのものより、「自分が何に困っていて、どう解決したいか」を言語化してAIに渡す。今回もその一往復で、実用的なものが形になった。
導入手順とソースコード
手順
- Google Apps Script にアクセスし、「新しいプロジェクト」を作成
- デフォルトの
コード.gsに、下記の コード.gs の内容を貼り付ける - エディタ左のファイル一覧で「+」→「HTML」を選択し、ファイル名を
indexにする(.htmlは自動で付く) - 作成された
index.htmlに、下記の index.html の内容を貼り付ける - 「デプロイ」→「新しいデプロイ」→ 種類を「ウェブアプリ」に設定
- 「アクセスできるユーザー」は必ず「自分のみ」にする
- 「デプロイ」を押してURLを取得。ブックマークしておくと便利
初回はカレンダーへのアクセス許可を求められる。自分で作ったスクリプトなので、「このアプリはGoogleで確認されていません」という警告が出ても、「詳細」→「(プロジェクト名)に移動」と進んで許可して問題ない。
コード.gs
/**
* Googleカレンダー 予定の「作成日・更新日」チェッカー
* note記事(its_maru 氏)の getDateCreated() を使うアイデアを
* Webアプリ化し、作成日に加えて更新日も取得できるように拡張したもの。
*/
// Webアプリのエントリポイント(URLにアクセスすると呼ばれる)
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('カレンダー予定の作成日チェッカー')
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}
// 利用可能なカレンダー一覧を返す(UIのプルダウン用)
function getCalendarList() {
var cals = CalendarApp.getAllCalendars();
var defaultId = CalendarApp.getDefaultCalendar().getId();
// デフォルト(メイン)カレンダーを先頭に並べる
cals.sort(function(a, b) {
if (a.getId() === defaultId) return -1;
if (b.getId() === defaultId) return 1;
return a.getName().localeCompare(b.getName(), 'ja');
});
return cals.map(function(c) {
return {
id: c.getId(),
name: c.getName(),
isDefault: c.getId() === defaultId
};
});
}
// 指定日の予定と、その作成日・更新日を取得する
// dateStr : 'yyyy-MM-dd' 形式の日付文字列
// calendarId: カレンダーID(空 or 'default' でメインカレンダー)
function getEventsInfo(dateStr, calendarId) {
var cal = (calendarId && calendarId !== 'default')
? CalendarApp.getCalendarById(calendarId)
: CalendarApp.getDefaultCalendar();
if (!cal) {
throw new Error('カレンダーが見つかりませんでした(ID: ' + calendarId + ')');
}
// 'yyyy-MM-dd' をローカル日付として解釈して当日0時を作る
var parts = String(dateStr).split('-');
var y = Number(parts[0]);
var m = Number(parts[1]);
var d = Number(parts[2]);
if (!y || !m || !d) {
throw new Error('日付の形式が正しくありません: ' + dateStr);
}
var date = new Date(y, m - 1, d, 0, 0, 0);
var tz = cal.getTimeZone() || Session.getScriptTimeZone();
var events = cal.getEventsForDay(date); // 記事と同じく指定日の予定を取得
var fmt = function(dt) {
if (!dt) return '';
return Utilities.formatDate(dt, tz, 'yyyy/MM/dd HH:mm:ss');
};
var rows = events.map(function(ev) {
return {
title: ev.getTitle() || '(タイトルなし)',
allDay: ev.isAllDayEvent(),
start: fmt(ev.getStartTime()),
end: fmt(ev.getEndTime()),
created: fmt(ev.getDateCreated()),
updated: fmt(ev.getLastUpdated()),
location: ev.getLocation() || '',
guests: ev.getGuestList().map(function(g){ return g.getEmail(); }).join(', ')
};
});
// 作成日が新しい順に並べ替え
rows.sort(function(a, b) {
return (a.created < b.created) ? 1 : (a.created > b.created) ? -1 : 0;
});
return {
calendarName: cal.getName(),
timeZone: tz,
date: Utilities.formatDate(date, tz, 'yyyy/MM/dd (E)'),
count: rows.length,
events: rows
};
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<base target="_top">
<meta charset="utf-8">
<style>
:root {
--bg: #f4f5f7;
--card: #ffffff;
--ink: #1f2933;
--muted: #687387;
--line: #e3e6ec;
--brand: #3b6ef5;
--brand-dark: #2a55d4;
--accent: #fff4e5;
--accent-line: #f0c178;
--shadow: 0 1px 3px rgba(16,24,40,.08), 0 6px 18px rgba(16,24,40,.06);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans",
"Noto Sans JP", Meiryo, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
header h1 {
font-size: 22px; margin: 0 0 4px; letter-spacing: .01em;
}
header p { margin: 0; color: var(--muted); font-size: 13.5px; }
.panel {
background: var(--card);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: var(--shadow);
padding: 22px;
margin-top: 22px;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 14px;
align-items: end;
}
@media (max-width: 640px) {
.controls { grid-template-columns: 1fr; }
}
label {
display: block; font-size: 12.5px; font-weight: 600;
color: var(--muted); margin-bottom: 6px;
}
input[type="date"], select {
width: 100%; padding: 10px 12px; font-size: 14px;
border: 1px solid var(--line); border-radius: 9px;
background: #fff; color: var(--ink); font-family: inherit;
}
input[type="date"]:focus, select:focus {
outline: none; border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(59,110,245,.15);
}
button {
padding: 11px 22px; font-size: 14px; font-weight: 600;
border: none; border-radius: 9px; cursor: pointer;
background: var(--brand); color: #fff; white-space: nowrap;
transition: background .15s;
}
button:hover { background: var(--brand-dark); }
button:disabled { opacity: .55; cursor: default; }
.status {
margin-top: 16px; font-size: 13.5px; color: var(--muted);
min-height: 20px;
}
.status.err { color: #c0392b; }
.summary {
display: none;
margin-top: 4px; font-size: 13.5px; color: var(--ink);
}
.summary b { color: var(--brand); }
.results { margin-top: 8px; }
.event {
border: 1px solid var(--line);
border-radius: 12px;
padding: 16px 18px;
margin-top: 12px;
background: #fff;
}
.event .ttl {
font-size: 15.5px; font-weight: 700; margin: 0 0 10px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.badge {
font-size: 11px; font-weight: 600; padding: 2px 8px;
border-radius: 999px; background: #eef2fb; color: var(--brand-dark);
}
.badge.allday { background: #e9f6ee; color: #1f7a44; }
.meta {
display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
gap: 6px 22px; font-size: 13px;
}
@media (max-width: 520px) { .meta { grid-template-columns: 1fr; } }
.meta .row { display: flex; gap: 8px; }
.meta .k { color: var(--muted); min-width: 64px; }
.meta .v { color: var(--ink); }
.meta .v.created { font-weight: 700; }
.empty {
text-align: center; color: var(--muted);
padding: 36px 0; font-size: 14px;
}
.note {
margin-top: 26px; font-size: 12.5px; color: var(--muted);
background: var(--accent); border: 1px solid var(--accent-line);
border-radius: 10px; padding: 12px 14px;
}
.spinner {
display: inline-block; width: 14px; height: 14px;
border: 2px solid #cdd5e3; border-top-color: var(--brand);
border-radius: 50%; animation: spin .7s linear infinite;
vertical-align: -2px; margin-right: 7px;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>🗓 カレンダー予定の作成日チェッカー</h1>
<p>指定した日の予定が「いつ作成・更新されたか」を調べます。</p>
</header>
<div class="panel">
<div class="controls">
<div>
<label for="cal">カレンダー</label>
<select id="cal"><option>読み込み中…</option></select>
</div>
<div>
<label for="date">調べたい日付</label>
<input type="date" id="date">
</div>
<div>
<button id="run" onclick="run()">調べる</button>
</div>
</div>
<div class="status" id="status"></div>
<div class="summary" id="summary"></div>
</div>
<div class="results" id="results"></div>
<div class="note">
ヒント: 作成日が「身に覚えのある日」と一致すれば、その日にやり取りした相手との予定である可能性が高いです。
予定を編集している場合は更新日が作成日より後になります。
</div>
</div>
<script>
// ── 起動時: カレンダー一覧の取得 & 日付を今日に初期化 ──
window.addEventListener('load', function() {
var today = new Date();
var iso = today.getFullYear() + '-'
+ String(today.getMonth() + 1).padStart(2, '0') + '-'
+ String(today.getDate()).padStart(2, '0');
document.getElementById('date').value = iso;
google.script.run
.withSuccessHandler(fillCalendars)
.withFailureHandler(function(e){ showError(e.message); })
.getCalendarList();
});
function fillCalendars(list) {
var sel = document.getElementById('cal');
sel.innerHTML = '';
list.forEach(function(c) {
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name + (c.isDefault ? '(メイン)' : '');
sel.appendChild(opt);
});
}
// ── 実行 ──
function run() {
var calId = document.getElementById('cal').value;
var date = document.getElementById('date').value;
if (!date) { showError('日付を選んでください。'); return; }
var btn = document.getElementById('run');
btn.disabled = true;
document.getElementById('summary').style.display = 'none';
document.getElementById('results').innerHTML = '';
setStatus('<span class="spinner"></span>取得中…', false);
google.script.run
.withSuccessHandler(function(data) { btn.disabled = false; render(data); })
.withFailureHandler(function(e) { btn.disabled = false; showError(e.message); })
.getEventsInfo(date, calId);
}
// ── 結果表示 ──
function render(data) {
setStatus('', false);
var sum = document.getElementById('summary');
sum.style.display = 'block';
sum.innerHTML = '<b>' + escapeHtml(data.calendarName) + '</b> / '
+ escapeHtml(data.date) + ' の予定: <b>' + data.count + '件</b>'
+ '(タイムゾーン: ' + escapeHtml(data.timeZone) + ')';
var box = document.getElementById('results');
if (!data.events.length) {
box.innerHTML = '<div class="panel"><div class="empty">この日には予定がありませんでした。</div></div>';
return;
}
var html = '';
data.events.forEach(function(ev) {
var badge = ev.allDay
? '<span class="badge allday">終日</span>'
: '<span class="badge">' + escapeHtml(timePart(ev.start)) + '〜' + escapeHtml(timePart(ev.end)) + '</span>';
var updatedRow = (ev.updated && ev.updated !== ev.created)
? '<div class="row"><span class="k">更新日</span><span class="v">' + escapeHtml(ev.updated) + '</span></div>'
: '';
var locRow = ev.location
? '<div class="row"><span class="k">場所</span><span class="v">' + escapeHtml(ev.location) + '</span></div>'
: '';
var guestRow = ev.guests
? '<div class="row"><span class="k">参加者</span><span class="v">' + escapeHtml(ev.guests) + '</span></div>'
: '';
html +=
'<div class="event">' +
'<p class="ttl">' + escapeHtml(ev.title) + ' ' + badge + '</p>' +
'<div class="meta">' +
'<div class="row"><span class="k">作成日</span><span class="v created">' + escapeHtml(ev.created || '不明') + '</span></div>' +
updatedRow + locRow + guestRow +
'</div>' +
'</div>';
});
box.innerHTML = html;
}
function timePart(s) {
// 'yyyy/MM/dd HH:mm:ss' → 'HH:mm'
var t = (s || '').split(' ')[1] || '';
return t.slice(0, 5);
}
function setStatus(html, isErr) {
var el = document.getElementById('status');
el.innerHTML = html;
el.className = 'status' + (isErr ? ' err' : '');
}
function showError(msg) {
setStatus('エラー: ' + escapeHtml(msg || '不明なエラー'), true);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function(c) {
return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c];
});
}
</script>
</body>
</html>使用上の注意
便利なツールだが、運用にあたって押さえておきたいポイントがある。
デプロイ時のアクセス権限は「自分のみ」に
ウェブアプリとして公開する際、「アクセスできるユーザー」を「全員」にすると、URLを知る第三者があなたの権限でカレンダーを読めてしまう。必ず「自分のみ」に設定し、自分専用のプライベートツールとして運用すること。
見られるのは自分がアクセスできるカレンダーだけ
このツールが扱えるのは、あなたのアカウントが閲覧権限を持つカレンダーに限られる。他人のカレンダーを勝手に覗けるわけではないので、その点は安心してほしい。逆に言えば、共有された相手のカレンダーであれば作成日も取得できる。
getEventsForDay() の挙動
指定日に「開催される」予定を返す仕様なので、複数日にまたがる予定は開始日・終了日の両方の検索でヒットする。1件を厳密に追いたい場合だけ、頭の片隅に置いておくといい。
GASの「6分ルール」
GASは1回の実行が最大6分という制限がある。1日分の予定を読む程度ならまず問題にならないが、何かと組み合わせて大量処理する場合は気に留めておきたい。
実際にかかった時間と使ったツール
今回は要件がシンプルだったこともあり、最初のプロンプト一回で、Webアプリ UI 込みの動作版が出てきた。 元記事のロジックがしっかりしていたおかげで、こちらは「作成日に加えて更新日も」「カレンダーはプルダウンで」と方針を渡すだけで済んだ。実働は数分。
使った生成AIは Claude。GAS のような単機能のツールは、要件さえ言語化できれば一往復でほぼ完成形まで届くことが多い。前回のDrive Copierでも感じたが、「最初から完璧な指示を出す必要はない」——大まかに伝えて、動くものを触って、足りなければ追加で頼む。このループで実用品ができてしまう。
まとめ
Googleカレンダーは予定の作成日を標準では見せてくれない。だが、APIレベルではちゃんと保持されていて、GASの getDateCreated() を使えば取り出せる。元になったnote記事の手法を、日付を選ぶだけで使えるWebアプリにして、更新日まで確認できるよう少し広げてみた。
こうした「ちょっとした不便」は、業務のあちこちに転がっている。標準機能でできないと分かった瞬間に諦めるのではなく、「APIなら取れるのでは?」「AIに頼めば数分で道具になるのでは?」と一歩進めてみる。その小さな習慣の積み重ねが、結局いちばん効いてくると思う。
身に覚えのない予定を前にして途方に暮れたら、まずは作成日を調べてみてね。

