Notionでタスクや請求の期日管理をしていて、「期日が来たらリマインドしたい」と思ったとき、まず思いつくのはGoogle Apps Script(GAS)だと思います。無料で、Notion APIをUrlFetchAppで叩くだけで動き、数時間で「動くもの」ができます。実際に私も、Kapselという通知ツールを個人開発する前、最初のプロトタイプはGASで書いていました。この記事は、GASで何ができて・どこで詰まって・最終的にどう作り直したか(pg_cron+Notion API)を、実装を交えて振り返る技術遍歴です。対象は「Notionの期日をGASで通知したい人」「GASからの移行先を検討している人」です。
GASで作れること
Notion公式APIはHTTPで叩けるので、GASのUrlFetchAppとNotion側のIntegration Tokenがあれば、期日が近いページを取得してメールを送る、というところまでは素直に書けます。
// 概念コード: GASで期日リマインドを組む最小構成
function checkNotionDueDates() {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty('NOTION_TOKEN');
const databaseId = props.getProperty('NOTION_DB_ID');
const today = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
const res = UrlFetchApp.fetch(
`https://api.notion.com/v1/databases/${databaseId}/query`,
{
method: 'post',
headers: {
Authorization: `Bearer ${token}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
payload: JSON.stringify({
filter: {
and: [
{ property: '期日', date: { equals: today } },
{ property: 'ステータス', select: { does_not_equal: '完了' } },
],
},
}),
}
);
const { results } = JSON.parse(res.getContentText());
results.forEach((page) => {
const title = page.properties['名前']?.title?.[0]?.plain_text ?? '(無題)';
const email = page.properties['担当者メール']?.email;
if (!email) return;
MailApp.sendEmail(email, `【期日】${title}`, `${title} の期日が本日です。`);
});
}
これに、スクリプトエディタの「トリガー」から時間主導型トリガーを設定すれば、定期実行が完成します。ScriptApp.newTrigger().timeBased().everyMinutes(10)のように、分単位の間隔指定もAPIとしては用意されています。ここまでは半日あれば動くもので、実際「期日が来ても気づけないまま放置する」ことはなくなります。GASは、この最初の一歩を作るには十分な道具でした。
GASの限界にぶつかったところ
プロトタイプとして使い続けるうちに、いくつかの壁にぶつかりました。
実行時間とトリガーの粒度
GASのスクリプトは、無料のGoogleアカウントだと1回の実行が最大6分で強制終了します。監視対象のデータベースが小さいうちは問題になりませんが、ページ数が増えてページングが必要になったり、複数のデータベースを1回の実行でまとめて処理しようとすると、6分の壁は思ったより近くにあります。
さらに、everyMinutes(1)のようにAPI上は分単位の指定ができても、実際の発火タイミングはGoogle側の内部スケジューリングに委ねられていて、指定通りの間隔で正確に動く保証はありません。加えて、無料アカウントには全トリガー合計の1日あたりの実行時間quotaもあります。「毎分ポーリングして期日ちょうどに送りたい」という要件に対して、GASのトリガーは体感としては「時間単位、良くて十数分単位」の粒度に落ち着く印象でした。日次バッチとしては十分でも、期日当日の"タイミング"まで意識したい通知には粗すぎます。
秘匿情報とコードの保守
PropertiesServiceにNotionのトークンを入れる運用は手軽ですが、暗号化はされていません。スクリプトエディタへのアクセス権が、そのままトークンへのアクセス権になります。個人のプロトタイプなら許容範囲でも、アクセス権限を細かく分ける・トークンの利用範囲を最小化する、といった設計をしようとすると、GAS単体では手が届きにくいところです。バージョン管理も、claspを使えばgit管理はできますが、レビューやCIを組み込む前提のワークフローではありません。
双方向の書き戻しができない
督促メールを送るだけなら良いのですが、「受信者がボタンを押したらNotion側のステータスが変わる」という双方向のやり取りをやろうとすると、GASの範囲を超えます。GASでもWebアプリとしてdoGet/doPostを公開すること自体はできますが、そこに署名付きトークンの検証やアクセス制御を積んでいくと、実質的にはWebアプリケーションを1から組んでいるのと変わらなくなってきます。この部分の実装は別の記事(署名付きアクションリンクの設計)で詳しく書いています。
社外への到達性
MailApp/GmailAppはGoogleアカウントに紐づいた送信で、消費者アカウントには1日の送信通数の上限があります。また送信元ドメインは自分のGoogleアカウントに依存するため、独自ドメインでSPF・DKIM・DMARCを整えて到達性を管理する、という設計の自由度は持てません。社内向けの通知ならこれで十分ですが、Notionを持たない社外の相手に迷惑メール扱いされずに届けたい場合は、送信基盤ごと自分で持つ必要が出てきます。
本格実装への発展
上記の壁を踏まえて作り直したのが、いま動いている実装です。技術スタックはNext.js(App Router) / Vercel(Fluid Compute) / Supabase(Postgres・RLS) / Resend / Notion API / pg_cron(毎分)。
-
定期実行: GASのトリガーの代わりに、Supabaseの
pg_cronから毎分HTTPエンドポイントを叩く構成にしました。実行時間の制約はVercel側のインフラの範囲に委ねられ、GASの6分やトリガーquotaのような天井を気にする必要がなくなりました。 - 冪等性: 毎分ポーリングすると同じ対象に何度も条件が一致するので、送信記録テーブルと一意制約で二重送信を防ぐ設計にしています。ここは別記事(送りすぎ・送り漏れの設計ノート)で詳しく書いたので、この記事では割愛します。
-
到達性: 送信元を独自ドメイン(
notify@getkapsel.com)にし、SPF・DKIM・DMARCを揃えました。GmailのDMARCレポートでpassを確認しています。 -
双方向: 受信者がメールのボタンを押すと、署名付きトークンを検証したうえでNotionのページを
PATCHで更新する経路を用意しました。
大きく変わったのは「頻度」と「保守できる形」です。GASは"動くものを最短で作る"のには向いていますが、実行時間・トリガーの粒度・秘匿情報の扱い・送信基盤の自由度、それぞれに天井があります。督促・通知のように「頻度が命」で「事故らせたくない」用途になった時点で、その天井が窮屈になった、というのが私の場合の実感でした。
まとめ
| 観点 | GAS | 本格実装(pg_cron+Notion API) |
|---|---|---|
| 実行時間 | 1回あたり最大6分(無料アカウント) | インフラの実行時間制約に依存(GASの天井なし) |
| 実行間隔 | API上は分単位指定可だが、実発火はGoogle側のスケジューリングに委ねられブレる | pg_cronで毎分 |
| 秘匿情報 | PropertiesServiceは暗号化なし | Supabase Vault等での管理 |
| 双方向書き戻し | Webアプリ化すれば可能だが実質フレームワーク自作 | 署名付きトークン検証で実装 |
| 到達性 | Googleアカウントの送信quotaに依存、独自ドメイン運用不可 | 独自ドメイン+SPF/DKIM/DMARC |
GASは「まず動くものを作って、自分の課題が本当にそこにあるか確かめる」ためのプロトタイピングとしては今でも良い選択だと思っています。私自身、最初の一歩はGASでした。ただ、頻度と事故防止と到達性を同時に求め始めた瞬間から、別のインフラが要る、というのがこの遍歴で得た実感です。
このGAS→本格実装への発展は、Notionの期日・ステータスを監視して社外の相手にもメールでリマインドする個人開発ツール Kapsel の実装をもとにしています。開発の背景はこちらにまとめています。