LangGraphでNotion Botを完全自動化する方法
はじめに
ルミナイR&Dチームの宮脇彰梧です。
現在はマルチモーダルAIの研究を行う大学院生として、
生成AIやAIエージェントの技術を実践的に探求しています。
本記事では、
LangGraph × OpenRouter(gpt-oss:20b) × Notion APIによる「自動分類・DB移動 Notion Bot」
を構築します。
特に今回は、実用レベルで便利な
- Notion DB のページ一覧を CLI で表示
- 番号で選ぶだけで処理開始
- 分類 → 自動 DB 移動 がワンステップ
という「実務向け UI」を備えている点が特徴です。
概要
LangGraph の状態管理 × OpenRouter LLM を活用し、
Notion ページを分類 → DB に自動移動するBotを構築する。
CLIでページ選択後、分類と整理が完全自動で進む。
1. なぜこのテーマを選んだのか
Notion は強力ですが、「情報整理」を人間がやる必要があります。
- 会議メモ
- TODO
- 研究ノート
- アイデア
- 日報
これらが“未分類DB”に溜まり、
あとで分類するのが面倒…という問題は多くの現場で起きています。
そこで今回のテーマは:
Notion に書く → Bot が分類 → 自動で適切なDBへ移動
という「整理不要のワークフロー」を作る。
LangGraph はこの用途に最適で、
- Node:LLM分類 / Notion更新
- State:page_id, content, category
- Graph構造:分類 → 更新
- エラー時の再実行も容易
というメリットがあります。
2. 関連調査
LangGraph が効く理由
| 概念 | 内容 |
|---|---|
| State | Bot の内部記憶(page_id / content / category) |
| Node | LLMまたはAPIを呼ぶ処理単位 |
| Edge | 「分類 → 更新」のような順序定義 |
| Pregelモデル | エラーに強く、ステップ実行が明確 |
今回のような「処理フローがはっきりしているタスク」では強力です。
OpenRouter × gpt-oss:20b を採用した理由
- 高速 & 安価
- テキスト分類には20Bで十分
- Function Calling不要
- LangChain 互換で LangGraph に統合しやすい
今回のタスク(分類)には 最適なモデル選択です。
Notion API の役割
- DB内のページ一覧取得
- ページ本文の抽出
- ページの親DBを切り替える(実質的に移動)
- タイトルなどプロパティ更新
Bot が Notion 上で“手作業”を代行する仕組みです。
3. 実装(やってみた)
今回実装した構成:
CLI で Notion DB のページを選択
→ ページ本文を API で抽出
→ LLM がカテゴリ分類
→ Notion API で DB 移動
3.1 実装した完全版コード
(説明が長いので、記事本編では GitHub へ掲載する想定)
👇 このコードが Bot の全機能を実装した最新版です:
import os
from typing import Optional, TypedDict, Dict, List
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
import requests
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph
# =========================
# 0. 環境変数ロード
# =========================
load_dotenv()
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
NOTION_TOKEN = os.getenv("NOTION_TOKEN")
if not OPENROUTER_API_KEY:
raise RuntimeError("OPENROUTER_API_KEY が .env に設定されていません。")
if not NOTION_TOKEN:
raise RuntimeError("NOTION_TOKEN が .env に設定されていません。")
# Notion DB ID(.env)
NOTION_DB_MEETING = os.getenv("NOTION_DB_MEETING")
NOTION_DB_WORK = os.getenv("NOTION_DB_WORK")
# =========================
# 1. LangGraph State 定義
# =========================
class BotState(TypedDict):
content: Optional[str]
category: Optional[str]
page_id: Optional[str]
result_message: Optional[str]
# =========================
# 2. OpenRouter (gpt-oss:20b) 設定
# =========================
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
api_key=OPENROUTER_API_KEY,
base_url="https://openrouter.ai/api/v1",
temperature=0.7,
)
# =========================
# 3. Notion API Utility 関数
# =========================
def fetch_page_list(database_id: str) -> List[Dict]:
"""特定DB内のページ一覧を取得して返す"""
url = f"https://api.notion.com/v1/databases/{database_id}/query"
headers = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
}
resp = requests.post(url, json={}, headers=headers)
results = resp.json().get("results", [])
pages = []
for page in results:
title_prop = page["properties"].get("Name", {}).get("title", [])
title = title_prop[0]["plain_text"] if title_prop else "(無題)"
pages.append({
"title": title,
"page_id": page["id"],
})
return pages
def fetch_page_content(page_id: str) -> str:
"""ページ本文を抽出してテキスト化"""
url = f"https://api.notion.com/v1/blocks/{page_id}/children"
headers = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": "2022-06-28",
}
resp = requests.get(url, headers=headers)
results = resp.json().get("results", [])
texts = []
for blk in results:
if blk["type"] == "paragraph":
for t in blk["paragraph"]["rich_text"]:
texts.append(t.get("plain_text", ""))
return "\n".join(texts)
# =========================
# 4. CLI:ページ選択機能
# =========================
def select_page_interactively(database_id: str) -> Dict:
"""DB内ページ一覧を CLI で選択 → page_id + content を返す"""
print("\n=== Notion ページ一覧 ===")
pages = fetch_page_list(database_id)
if not pages:
print("⚠️ ページがありません")
return None
for i, p in enumerate(pages):
print(f"{i}: {p['title']}")
idx = int(input("\n処理したいページ番号を入力してください: "))
selected = pages[idx]
page_id = selected["page_id"]
content = fetch_page_content(page_id)
print(f"\n▶ 選択ページ: {selected['title']} ({page_id})")
print(f"内容:\n{content}")
return {"page_id": page_id, "content": content}
# =========================
# 5. LangGraph ノード定義
# =========================
def classify_node(state: BotState) -> Dict:
content = state.get("content", "")
prompt = f"""
あなたは日本語テキストの分類器です。
次のメモを、以下のカテゴリのいずれか1つに分類してください。
候補カテゴリ:
- 会議メモ
- 仕事
出力はカテゴリ名のみ。
メモ本文:
{content}
"""
res = llm.invoke(prompt)
category = res.content.strip()
allowed = ["会議メモ", "仕事"]
if category not in allowed:
category = "アイデア"
print(f"[classify_node] → {category}")
return {"category": category}
def update_notion_node(state: BotState) -> Dict:
page_id = state.get("page_id")
category = state.get("category", "アイデア")
database_map = {
"会議メモ": NOTION_DB_MEETING,
"仕事": NOTION_DB_WORK
}
db_id = database_map.get(category)
payload = {
"parent": {"database_id": db_id},
"properties": {
"Name": {"title": [{"text": {"content": f"{category}|Auto-Sorted"}}]}
}
}
headers = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
}
resp = requests.patch(
f"https://api.notion.com/v1/pages/{page_id}",
json=payload,
headers=headers,
)
if not resp.ok:
raise RuntimeError(f"Notion API Error: {resp.text}")
msg = f"Notion page {page_id} updated to {category}"
print(f"[update_notion_node] {msg}")
return {"result_message": msg}
# =========================
# 6. LangGraph グラフ構築
# =========================
workflow = StateGraph(BotState)
workflow.add_node("classify", classify_node)
workflow.add_node("update_notion", update_notion_node)
workflow.add_edge("classify", "update_notion")
workflow.set_entry_point("classify")
agent = workflow.compile()
# =========================
# 7. FastAPI Webhook
# =========================
app = FastAPI(title="LangGraph Notion Bot")
class WebhookPayload(BaseModel):
page_id: str
content: str
@app.post("/webhook")
def handle_webhook(payload: WebhookPayload):
initial_state: BotState = {
"page_id": payload.page_id,
"content": payload.content,
"category": None,
"result_message": None,
}
result = agent.invoke(initial_state)
return {"status": "ok", "result": result}
# =========================
# 8. CLI 実行
# =========================
def debug_run():
print("\n=== DB を選択 ===")
print("0: 会議メモDB")
print("1: 仕事DB")
choice = int(input("番号を入力: "))
DB_MAP = {
0: NOTION_DB_MEETING,
1: NOTION_DB_WORK,
}
database_id = DB_MAP.get(choice)
selected = select_page_interactively(database_id)
# 🆕 ここが重要!!
if not selected:
print("❌ 選択されたDBにページがないため、処理を終了します。")
return
initial_state: BotState = {
"page_id": selected["page_id"],
"content": selected["content"],
"category": None,
"result_message": None,
}
print("\n=== LangGraph 実行 ===")
result_state = agent.invoke(initial_state)
print("\n=== 結果 ===")
for k, v in result_state.items():
print(f"{k}: {v}")
if __name__ == "__main__":
debug_run()
4. 動かしてみた(実行ログ付き)
ここでは、実際にあなたの環境で動作した
“リアルなログ” をそのまま載せて解説します。
4.1 DB を選択
=== DB を選択 ===
0: 会議メモDB
1: 仕事DB
番号を入力: 1
「仕事DB」 内のページを対象とします。
4.2 ページ一覧の表示
=== Notion ページ一覧 ===
0: 出張
1: コード編集
Notion API から DB 内のページ名が一覧で表示されました。
今回は「出張」を選択。
4.3 ページ内容の取得
▶ 選択ページ: 出張 (...)
内容:
来週、カナダに出張.
fetch_page_content() により paragraph ブロックを抽出し、
テキストとして整形されていることがわかります。
4.4 LangGraph の分類処理
=== LangGraph 実行 ===
[classify_node] → 仕事
LLM が文章を読んで判断:
「来週、カナダに出張 → 仕事カテゴリ」
非常に自然な分類結果です。
4.5 Notion ページの自動移動
[update_notion_node] Notion page ... updated to 仕事
Notion API による DB 移動も成功。
タイトルも自動的に:
仕事|Auto-Sorted
という形式に変更されます。
4.6 最終 State の確認
=== 結果 ===
content: 来週、カナダに出張.
category: 仕事
page_id: <略>
result_message: Notion page ... updated to 仕事
Bot の処理のすべてが明示的に記録されています。
5. 考察
今回の実装は、LangGraph の “状態管理 × ノード指向設計” が最も活きる例です。
特に効果的だった点:
-
ページ選択UI(CLI)
→ 開発者が即テストできる
→ 外部サービス不要 -
LLM分類のシンプルさ
→ gpt-oss-20b で十分な精度
→ コストを抑えて実用化しやすい -
Notion API の DB移動操作が自動化できる点
→ Notion 内の情報整理が「書けば終わり」になる
また、分類対象の文章が自然言語であれば
高確率で正しいカテゴリを選択してくれる。
今回の出張メモも、モデルは正しく “仕事” と判断した。
6. まとめ
- LangGraph により「分類 → 更新」が堅牢に自動化できる
- OpenRouter の 20B モデルがコスパよく実用的
- Notion API の組み合わせで業務整理の大部分が自動化可能
- CLI 選択方式が開発・個人利用に最適
- 実行ログから確認できる通り、分類精度も実用レベル
執筆:宮脇 彰梧(ルミナイ株式会社 / Lluminai)
【現在採用強化中です!】
- AIエンジニア
- PM/PdM
- 戦略投資コンサルタント
▼代表とのカジュアル面談URL
Discussion