なぜExcel・Notionでは限界だったか

私たちの組織には現在、約90名のAIスタッフが所属しています。各部署がそれぞれのプロジェクトを抱え、日々数十件のタスクが生まれては消化されていきます。この規模のタスク管理を、私たちは当初Notionで行っていました。

Notionは柔軟なツールです。データベースビューを切り替えれば、ボード表示もテーブル表示も思いのままに変えられます。しかし、組織が30名を超えたあたりから深刻な問題が顕在化しました。

まず、API経由での自動操作が実用的な速度で動かなくなりました。AIスタッフがタスクの状態を更新するたびにNotion APIを叩くのですが、レートリミットに引っかかり、処理がキュー待ちになる場面が増えたのです。Excelに至っては、そもそも複数のエージェントが同時にファイルを開くこと自体が不可能でした。

次に、GTDの考え方に沿ったタスク管理を実現しようとしたとき、Notionのデータベース構造では表現しきれない要素が出てきました。タスクとプロジェクトの親子関係、タスク間の依存関係、委任先の追跡、ロック機構による排他制御。これらを全てNotionのリレーションとロールアップで表現しようとすると、設計が破綻するか、クエリが遅くなるかのどちらかでした。

私たちが求めていたのは、SQLで直接クエリを投げられるリレーショナルデータベースそのものだったのです。そこで選んだのが、Cloudflare D1でした。Cloudflare Workersの基盤上で稼働するサーバーレスSQLiteデータベースであり、私たちのAPIインフラと同じエコシステムに乗せられる点が決定打になりました。

GTDの考え方を参考にした独自タスク管理の全体像

ここで明確にしておきたいことがあります。本記事で紹介するシステムは、David Allen氏が提唱したGTD(Getting Things Done)メソッドそのものではありません。GTDの考え方——「頭の中の気になることを全て外部に出し、信頼できるシステムで管理する」「次の物理的なアクションを明確にする」という原則——を参考にしながら、私たちの組織に最適化した独自のタスク管理システムです。

GTDの原典であるDavid Allen著『Getting Things Done』(Penguin Books, 2015年改訂版)、および日本語訳『はじめてのGTD ストレスフリーの整理術』(田口 元 監訳、二見書房)は、タスク管理に取り組む全ての方に一読をお勧めします。本記事はこれらの書籍の内容を教授するものではなく、あくまで私たちの実装記録です。

GTD(R)はDavid Allen Companyの登録商標です。

私たちのシステムの全体像は次のとおりです。Cloudflare D1上にgtd-dbという名前のデータベースを構築し、gtd_tasksテーブルとgtd_projectsテーブルの2つで全てのタスクとプロジェクトを管理しています。2026年3月25日時点で、557件のタスクと41件のプロジェクトが格納されています。

このデータベースに書き込みを行えるのは、組織内で唯一PMQ-004(中川悠斗)だけです。他のスタッフや秘書(Claude Code)はSELECTクエリで参照することはできますが、INSERT・UPDATE・DELETEは全てPMQ-004を経由します。さらに、D1のデータを人間が読みやすいMarkdown形式に変換するgenerate-gtd.shスクリプトが存在し、GTD.mdファイルを自動生成します。このGTD.mdファイルへの手動編集はhookによって物理的にブロックされています。

この設計思想は「プロジェクトキックオフチェックリスト」で紹介しているプロジェクト立ち上げの原則とも通じています。単一の情報源(Single Source of Truth)を定め、それ以外の場所への書き込みを物理的に防ぐことで、情報の整合性を担保するのです。

D1テーブルスキーマ設計 -- gtd_tasksとgtd_projects

gtd_tasksテーブル

タスクテーブルは21カラムで構成されています。GTDの原則に基づき、各カラムがタスクの「いつ」「誰が」「何を」「どの優先度で」を構造的に表現します。

CREATE TABLE gtd_tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'inbox',
  priority TEXT,
  context TEXT,
  timeslot TEXT,
  earliest TEXT,
  due_date TEXT,
  memo TEXT,
  mvp INTEGER DEFAULT 0,
  assignee TEXT,
  project_id TEXT,
  estimated_minutes INTEGER,
  energy_level TEXT,
  depends_on INTEGER,
  delegated_to TEXT,
  delegated_at TEXT,
  sort_order INTEGER DEFAULT 0,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
  done_at TEXT,
  locked_at TEXT,
  locked_by TEXT
);

設計のポイントをいくつか補足します。

statusカラムはGTDのリスト体系を反映しています。inboxは「まだ処理していない気になること」、next_actionは「次に取るべき具体的な行動」、waiting_forは「誰かに委任して返答を待っている状態」、somedayは「いつかやりたいがコミットしていない項目」、doneは「完了済み」です。GTDの原則では、この状態の分類が最も重要です。「コミット済み」と「未コミット」を混在させない——これはDavid Allen氏が繰り返し強調している点であり、私たちのstatusカラム設計の根幹にあります。

earliestdue_dateは意図的に分離しています。earliestは「この日以降に着手可能」という開始制約であり、due_dateは「この日までに完了すべき」という期限です。GTDの考え方では、カレンダーには日時が確定したものだけを載せるべきとされています。earliestが未来のタスクはまだ着手可能ではないので、日次のタスクリスト提示から除外する——この仕組みによって、今日やるべきことだけに集中できます。

project_idはgtd_projectsテーブルへの外部参照です。当初はmemoカラムに「A1」などとプロジェクト識別子を書き込んでいましたが、クエリの柔軟性と整合性を考慮して独立カラムに昇格させました。

gtd_projectsテーブル

プロジェクトテーブルは12カラムで構成されています。

CREATE TABLE gtd_projects (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  pl_code TEXT,
  pl_name TEXT,
  target TEXT,
  goal TEXT,
  deadline TEXT,
  description TEXT,
  status TEXT NOT NULL DEFAULT 'active',
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

idがTEXT型であることに注目してください。「A1」「B3」「C12」のように、カテゴリを示すアルファベット+連番という形式を採用しています。これは人間が口頭で「A1のタスクを確認して」と指示できる利便性を重視した設計判断です。自動採番のINTEGERでは「プロジェクト37番」のように直感的に把握しにくいため、あえてTEXT型のIDを選びました。

pl_codepl_nameはプロジェクトリーダーの情報です。タスクのassigneeとは別に、プロジェクト全体の責任者を明示することで、「このプロジェクトの進捗を誰に聞けばいいか」が即座にわかります。

2つのテーブルの関係は、gtd_tasks.project_idgtd_projects.idを参照するシンプルな1対多の構造です。1つのプロジェクトに複数のタスクがぶら下がり、1つのタスクは必ず1つのプロジェクトに所属します。この制約により、「プロジェクトに紐づかない浮遊タスク」が発生しません。GTDでは「2ステップ以上の成果物はプロジェクトとして管理する」とされていますが、私たちはさらに踏み込んで、単発タスクであっても必ずプロジェクトに紐づけるルールを採用しています。

タスクライフサイクル -- inboxからdoneまで

タスクは必ずinboxから始まります。これはGTDの「まず全てを収集する」という原則を反映しています。誰かが「これをやったほうがいい」と思いついた時点で、そのタスクは即座にinboxに投入されます。

inboxに入ったタスクは、次のいずれかの状態に遷移します。

inbox → next_action: 「次にやるべき具体的な行動」として認定されたタスクです。タスク名は「~を確認する」「~に連絡する」のように、物理的な行動として記述します。「マーケティングを改善する」のような曖昧な表現は許容しません。曖昧なものはプロジェクトとして登録し、最初のnext_actionを切り出します。

inbox → someday: いつかやりたいが、今はコミットしないタスクです。週次レビューの際に再評価し、状況が変わればnext_actionに昇格させます。

inbox → waiting_for: 自分では行動できず、外部の返答を待っている状態です。delegated_toカラムに委任先を記録し、delegated_atに委任日を記録します。これにより「2週間前に○○に頼んだ件、返事が来ていない」というフォローアップが構造的に可能になります。

next_action → done: タスクが完了した状態です。done_atに完了日時が自動記録されます。

この状態遷移は全てSQLのUPDATE文で表現されます。

-- inboxからnext_actionへの昇格
UPDATE gtd_tasks
SET status = 'next_action',
    updated_at = datetime('now')
WHERE id = 123 AND status = 'inbox';

-- タスクの完了
UPDATE gtd_tasks
SET status = 'done',
    done_at = datetime('now'),
    updated_at = datetime('now')
WHERE id = 123;

実運用では、1日あたり平均10~15件のタスクがstatusを遷移しています。done状態のタスクは削除せず、履歴として永続的に保持します。「先月のプロジェクトAで何件のタスクを消化したか」というレトロスペクティブを、SQLの集計クエリ一発で実行できることが、データベース化の大きな利点です。KPTなどの振り返りフレームワークとも相性が良く、定量的なデータに基づいた改善サイクルを回せるようになりました。

ロック機構による排他制御

私たちの組織では、複数のAIスタッフが同時にタスクを操作する可能性があります。たとえば、PMQ-004がタスクAのstatusをnext_actionに更新しようとしている最中に、別のスタッフが同じタスクAの内容を変更しようとする——このような競合を防ぐために、ロック機構を実装しました。

gtd_tasksテーブルの末尾にあるlocked_atlocked_byの2カラムがその仕組みです。

-- ロックの取得
UPDATE gtd_tasks
SET locked_at = datetime('now'),
    locked_by = 'PMQ-004'
WHERE id = 123
  AND (locked_at IS NULL
       OR locked_at < datetime('now', '-5 minutes'));

-- ロックの解放
UPDATE gtd_tasks
SET locked_at = NULL,
    locked_by = NULL
WHERE id = 123 AND locked_by = 'PMQ-004';

ロックの有効期限は5分です。5分を超えたロックは自動的に「期限切れ」と見なされ、他のプロセスが上書き取得できます。これは、処理中にエラーが発生してロックが解放されないまま残る「デッドロック」を防ぐための安全弁です。

一般的なWebアプリケーションでは、データベース自体が提供するトランザクション機能やロック機構を使うのが定石です。しかしD1はSQLiteベースであり、また私たちの操作は全てCloudflare Workers経由のHTTPリクエストで行われるため、リクエストをまたいだトランザクションを維持できません。そこで「アプリケーションレベルのロック」として、テーブル内にロック情報を持たせる方式を採用しました。

この方式には限界もあります。厳密な意味での排他制御ではなく、ロック取得のUPDATE文自体が競合する可能性はゼロではありません。しかし、私たちの運用では書き込み窓口をPMQ-004に一本化しているため、実質的に同時書き込みが発生しない設計になっています。ロック機構は「万が一」の安全策として機能しており、運用開始以来、ロック競合は一度も発生していません。

GTD.md自動生成 -- D1からMarkdownへ

データベースに格納されたタスクとプロジェクトの情報は、人間やAIスタッフが日常的に参照するにはSQLクエリを叩く必要があります。しかし、全スタッフがSQLを書けるわけではありませんし、タスクの全体像を俯瞰するには表形式のMarkdownのほうが遥かに読みやすいのが実情です。

そこで作成したのがgenerate-gtd.shというシェルスクリプトです。このスクリプトは内部でNode.jsのスクリプト(generate-gtd.js)を呼び出し、D1に対してSELECTクエリを実行し、結果をMarkdownテーブルとして整形してGTD.mdファイルに出力します。

処理の流れは以下のとおりです。

  1. wrangler d1 executeコマンドでgtd-dbに接続し、アクティブなプロジェクト一覧を取得
  2. 各プロジェクトに紐づくタスクを、statusごとにグルーピングして取得
  3. プロジェクト単位でMarkdownのセクションを生成し、タスクをテーブル形式で出力
  4. 完成したMarkdownをGTD.mdに書き出す

生成されるGTD.mdの構造は、プロジェクトごとにH2見出しが付き、その配下にタスク一覧がテーブルで並ぶ形式です。statusによって「Next Actions」「Waiting For」「Someday/Maybe」のセクションに分類されます。

このスクリプトによって、D1が「唯一の正」であるという原則を維持しながら、日常的な参照にはMarkdownという読みやすい形式を提供するという二重の利便性を実現しています。「プロジェクトマネジメント完全ガイド」で述べている「情報の見える化」の具体的な実装例でもあります。

ポイントは、この変換が一方通行であることです。D1 → GTD.mdの方向にしかデータは流れません。GTD.mdを手動で編集しても、D1には反映されません。むしろ、手動編集はGTD.mdとD1の乖離を生むだけなので、物理的にブロックする必要がありました。

hookによる手動編集ブロック

GTD.mdが自動生成ファイルであるにもかかわらず、人間やAIが「ちょっとこの行を修正しよう」と手動編集してしまうリスクは常に存在します。特にClaude CodeのようなAIアシスタントは、ファイルの内容を見て「ここを直せばいい」と判断し、EditツールやWriteツールで直接書き換えてしまう可能性があります。

これを防ぐために、Claude CodeのPreToolUse hookを活用しました。block-gtd-edit.shというシェルスクリプトがhookとして登録されており、Claude CodeがEditまたはWriteツールを使おうとしたとき、対象ファイルパスに「GTD.md」が含まれていれば、exit code 2を返して操作を拒否します。

if [[ ("$tool_name" == "Edit" || "$tool_name" == "Write") \
      && "$file_path" == *"GTD.md"* ]]; then
  echo "GTD.mdは自動生成ファイルです。直接編集できません。" >&2
  exit 2
fi

この仕組みにより、Claude Codeが「GTD.mdを編集しようとしました」→「hookによってブロックされました」→「D1に対して変更を行う必要があります」という正しいワークフローに自動的に誘導されます。

hookの設計思想は「AIに正しい行動を教える」のではなく、「間違った行動を物理的に不可能にする」というものです。私たちはAIスタッフの運用で多くの失敗を経験してきましたが、その教訓の一つが「ルールを守らせるのではなく、ルール違反が不可能な仕組みを作る」ということでした。hookはまさにその思想の具体的な実装です。

PMQ-004を唯一の書き込み窓口にした設計

データベースへの書き込み権限を単一のスタッフに集約する——これは一見、ボトルネックを自ら作り出しているように見えるかもしれません。しかし私たちの運用では、これが最も安全かつ効率的な設計でした。

書き込みを集約する以前は、秘書(Claude Code)が直接D1にINSERTやUPDATEを実行していました。しかし、複数のセッションが同時に走る私たちの環境では、以下の問題が発生しました。

1つ目は、同じタスクに対して異なるセッションが矛盾するUPDATEを発行するケースです。セッションAが「このタスクはdone」とマークした直後に、セッションBが古い情報に基づいて「このタスクのmemoを更新」と実行し、done状態が上書きされる——このような事態が実際に起きました。

2つ目は、タスクの登録ルールが統一されない問題です。あるセッションはpriorityに「A」と書き、別のセッションは「high」と書く。contextの表記ゆれも頻発しました。

PMQ-004を唯一の書き込み窓口にすることで、これらの問題は解消されました。全ての書き込みリクエストはPMQ-004を経由するため、ロック取得→書き込み→ロック解放というプロセスが直列化されます。PMQ-004は書き込みルール(statusの遷移ルール、priority表記の統一など)を内蔵しており、ルールに反する書き込みは拒否します。

秘書を含む他の全スタッフは、参照クエリ(SELECT)のみ直接実行可能です。タスクの登録、ステータス変更、プロジェクト管理の全てが「PMQ-004を呼ぶ」というシンプルな行動に帰結します。この設計はCloudflare Workersの設計ガイドで解説しているマイクロサービス的な責務分離の思想とも共通しています。

この設計にはトレードオフもあります。PMQ-004に障害が発生した場合、タスクの書き込みが全て停止します。現時点ではPMQ-004の可用性で問題が発生したことはありませんが、将来的にはフェイルオーバー機構を検討する余地があります。

データベース化して得た5つの恩恵

NotionからD1へ移行して5日間が経過した時点で、以下の5つの恩恵を実感しています。

恩恵1: SQLによる横断分析が一瞬で完了する

「先週完了したタスクの件数は?」「プロジェクトAの残タスクは何件?」「someday状態が3ヶ月以上放置されているタスクは?」——これらの質問に対して、SQLクエリ一発で回答が得られます。

-- 先週完了したタスクの集計
SELECT project_id, COUNT(*) as completed
FROM gtd_tasks
WHERE status = 'done'
  AND done_at >= datetime('now', '-7 days')
GROUP BY project_id
ORDER BY completed DESC;

Notionでは、フィルタを手動で設定し、ビューを切り替え、場合によっては複数のデータベースを横断してリレーションを辿る必要がありました。D1ではJOIN一つで完結します。

恩恵2: 自動化との親和性が飛躍的に向上した

WBSの分解結果を自動的にgtd_tasksに投入する、完了タスクの統計を日次レポートに含める、特定条件のタスクをSlackに通知する——こうした自動化が、全てSQL + Cloudflare Workersの組み合わせで実現できます。Notion APIのレートリミットを気にする必要はもうありません。

恩恵3: データの整合性が構造的に保証される

NOT NULL制約、DEFAULT値、UNIQUE制約。リレーショナルデータベースの基本機能が、データの品質を自動的に守ります。「statusが空のタスク」「同じslugのプロジェクトが重複登録される」といった事態は、スキーマレベルで防止されています。

恩恵4: 監査証跡としての価値

全タスクのcreated_atupdated_atdone_atが記録されているため、タスクのライフサイクルを後から完全にトレースできます。「このタスクは何日間next_action状態だったか」「完了までの平均リードタイムは何日か」といった分析が可能です。これはアジャイルの振り返りやプロセス改善に不可欠なデータです。

恩恵5: コストがほぼゼロ

Cloudflare D1の無料枠は、1日あたり読み取り500万行・書き込み10万行です。私たちの運用規模(557タスク・41プロジェクト)では、無料枠の1%にも達しません。Notionの有料プランと比較して、ランニングコストが文字通りゼロになりました。サーバーレスであるため、サーバーの管理も不要です。

まとめ

タスク管理をデータベース化するというアプローチは、一見すると大げさに思えるかもしれません。しかし、組織が一定の規模を超え、複数のエージェントが同時にタスクを操作する環境では、リレーショナルデータベースの恩恵は絶大です。

私たちの設計を振り返ると、技術的に高度なことをしているわけではありません。SQLiteベースのD1に2つのテーブルを作り、書き込み窓口を一本化し、Markdownへの自動変換とhookによる編集ブロックを設定しただけです。しかし、このシンプルな設計が、557件のタスクと41プロジェクトを破綻なく管理する基盤になっています。

David Allen氏の言葉を借りれば、GTDの本質は「信頼できるシステムに全てを預けることで、頭を空にし、目の前のタスクに集中する」ことです。D1はまさにその「信頼できるシステム」として機能しています。

今後の課題としては、コンテキスト(contextカラム)の正規化、タスク履歴テーブルの新設、週次レビューの自動化などが挙げられます。これらの改善は、GTDの考え方をさらに深くシステムに組み込んでいく過程であり、「完成」のない継続的な取り組みです。

参考文献