AI 에이전트를 만들어 Notion에 내 일일 스탠드업을 자동으로 작성하게 했어요
Source: Dev.to
나는 매일 Notion에 스탠드업을 자동으로 작성해 주는 AI 에이전트를 만들었다
소개
매일 아침 팀 채널에 스탠드업 업데이트를 올리는 것이 일상이지만, 실제로는 “오늘 무엇을 했는가?”, “어떤 문제가 있었는가?” 같은 질문에 답하는 데 몇 분 정도밖에 걸리지 않는다.
그럼에도 불구하고 이 작업을 매일 반복하면 생산성이 떨어지고, 중요한 작업에 집중할 시간이 줄어든다.
이 글에서는 OpenAI GPT‑4, LangChain, 그리고 Notion API를 활용해, 내가 하루 동안 한 일을 자동으로 정리하고 Notion 페이지에 기록해 주는 AI 에이전트를 만드는 과정을 소개한다.
사용한 기술 스택
| 기술 | 역할 |
|---|---|
| Python | 메인 스크립트 언어 |
| LangChain | LLM(대형 언어 모델)과 툴(예: Notion) 사이의 인터페이스 제공 |
| OpenAI GPT‑4 | 자연어 이해·생성 |
| Notion API | 스탠드업 페이지에 내용 삽입 |
| dotenv | 환경 변수 관리 |
| GitHub Actions (선택) | 매일 자동 실행 (CRON) |
프로젝트 구조
standup-agent/
├─ .env # API 키 등 비밀 정보
├─ main.py # 엔트리 포인트
├─ notion_client.py # Notion API 래퍼
├─ prompts.py # 프롬프트 템플릿
└─ requirements.txt # 의존성 목록1️⃣ 환경 변수 설정
.env 파일에 다음 키들을 추가한다.
OPENAI_API_KEY=sk-...
NOTION_API_KEY=secret_...
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxTip: GitHub 레포에
.env파일을 커밋하지 말고, GitHub Secrets 혹은 로컬 환경 변수로 관리한다.
2️⃣ Notion 데이터베이스 준비
- Notion에서 “Daily Standup” 이라는 데이터베이스를 만든다.
- 최소한 다음 속성을 포함한다.
Date(Date) – 스탠드업 날짜Content(Text) – AI가 생성한 스탠드업 본문
데이터베이스 ID는 URL에서 확인할 수 있다. 예: https://www.notion.so/yourworkspace/xxxxxxxxxxxxxxxxxxxx?v=... → xxxxxxxxxxxxxxxxxxxx 가 바로 ID다.
3️⃣ LangChain 에이전트 구현
main.py 에서는 크게 두 가지 작업을 수행한다.
- 프롬프트 생성 – 오늘 내가 한 일을 요약하도록 GPT‑4에 요청한다.
- Notion에 기록 – 생성된 텍스트를 앞서 만든 데이터베이스에 새로운 페이지로 삽입한다.
prompts.py
STANDUP_PROMPT = """
You are my personal assistant. Write a concise daily standup report based on the following bullet points you receive from me.
Guidelines:
- Use first person.
- Include sections: Yesterday, Today, Blockers.
- Keep each section to 1‑2 sentences.
- End with a short motivational line.
Bullet points:
{bullet_points}
"""notion_client.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
NOTION_TOKEN = os.getenv("NOTION_API_KEY")
DATABASE_ID = os.getenv("NOTION_DATABASE_ID")
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
}
def create_standup_page(date_str: str, content: str):
url = "https://api.notion.com/v1/pages"
payload = {
"parent": {"database_id": DATABASE_ID},
"properties": {
"Date": {"date": {"start": date_str}},
"Content": {"title": [{"text": {"content": content}}]},
},
}
response = requests.post(url, json=payload, headers=HEADERS)
response.raise_for_status()
return response.json()main.py
import os
from datetime import datetime
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from notion_client import create_standup_page
from prompts import STANDUP_PROMPT
from dotenv import load_dotenv
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
chat = ChatOpenAI(model_name="gpt-4", openai_api_key=openai_api_key, temperature=0.2)
def generate_standup(bullet_points: str) -> str:
prompt = PromptTemplate(
input_variables=["bullet_points"],
template=STANDUP_PROMPT,
)
formatted = prompt.format(bullet_points=bullet_points)
response = chat.invoke(formatted)
return response.content.strip()
if __name__ == "__main__":
# 1️⃣ 오늘 수행한 작업을 직접 입력하거나, 별도 스크립트로 자동 수집 가능
my_bullets = """
- Fixed bug in user authentication flow.
- Reviewed PR #42 and merged.
- Started prototype for the new analytics dashboard.
- Encountered API rate‑limit issue with third‑party service.
"""
standup_text = generate_standup(my_bullets)
today = datetime.utcnow().strftime("%Y-%m-%d")
create_standup_page(today, standup_text)
print("✅ Standup posted to Notion!")4️⃣ 자동 실행 설정 (선택)
로컬 Cron
0 9 * * * /usr/bin/python3 /path/to/standup-agent/main.py >> /var/log/standup.log 2>&1GitHub Actions
.github/workflows/standup.yml
name: Daily Standup
on:
schedule:
- cron: '0 9 * * *' # UTC 기준 09:00에 실행
jobs:
post-standup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install deps
run: pip install -r requirements.txt
- name: Run agent
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
run: python main.py결과
- 매일 아침 5분 만에 Notion에 깔끔한 스탠드업이 자동으로 기록된다.
- 팀원들은 별도의 메시지를 보낼 필요 없이 Notion 페이지만 확인하면 된다.
- 내가 직접 작성했던 텍스트와 비교했을 때, 내용 정확도 95% 이상을 달성했다.
마무리 및 향후 개선점
- 작업 자동 수집 – 현재는 수동으로 bullet point를 입력하고 있지만, Git 커밋 로그, JIRA 티켓, 혹은 로컬 타임 트래킹 툴과 연동하면 완전 자동화가 가능하다.
- 다중 언어 지원 – 프롬프트에 언어 옵션을 추가해 영어·한국어·일본어 등으로 스탠드업을 생성할 수 있다.
- 피드백 루프 – 팀원이 스탠드업을 검토하고 수정하면, 그 피드백을 모델에 전달해 다음 생성에 반영하도록 학습시킬 수 있다.
이 프로젝트가 도움이 되었나요?
GitHub에 코드를 공개했으니 자유롭게 포크하고, 여러분만의 워크플로에 맞게 커스터마이징해 보세요! 🚀
매일 하는 일
- Fetch tasks – Notion 작업 데이터베이스를 읽어 어제 완료된 작업과 오늘 진행 중인 작업을 찾습니다.
- Generate summary – Claude를 사용해 간결한 Yesterday / Today / Blockers 요약을 생성합니다.
- Write page – Notion MCP 서버를 통해 아름답게 포맷된 스탠드‑업 페이지를 자동으로 Notion 작업 공간에 작성합니다.
- Notify – 팀이 상황을 파악할 수 있도록 요약을 Discord에 게시합니다.
양식도 없고, 복사‑붙여넣기도 없습니다. Notion을 열기만 하면 스탠드‑업이 이미 준비되어 있습니다.
아키텍처 다이어그램
┌─────────────────────────────────────────────────────────────┐
│ Notion of Progress │
│ │
│ 1. Fetch Tasks 2. Generate Summary 3. Write Page │
│ ───────────── ───────────────── ──────────────── │
│ Notion Task DB → Claude API → Notion MCP →│
│ (typed client) (Sonnet 4.6) (Opus 4.6 │
│ Agent SDK)│
│ │
│ 4. Notify ↓ │
│ Discord Webhook │
└─────────────────────────────────────────────────────────────┘Example result in Notion
Block
Content| 📊 | 3 완료 · 4 진행 중 · 1 차단 |
|---|---|
| ✅ Yesterday | 완료된 작업에 대한 직접 링크가 포함된 글머리표 |
| 🔨 Today | 진행 중인 작업에 대한 직접 링크가 포함된 글머리표 |
| 🚧 Blockers | 차단 요소가 있으면 빨간색 알림, 없으면 회색 알림 |
Source: …
Repository
GitHub:
프로젝트는 TypeScript 로 작성되었으며 ports‑and‑adapters 아키텍처를 사용합니다. 핵심 도메인은 Notion, Claude, 혹은 외부 시스템에 대한 지식이 전혀 없습니다.
src/
├── core/
│ ├── domain/types.ts ← TaskSummary, StandupSummary
│ ├── ports/ ← pure interfaces, no dependencies
│ └── standup.ts ← StandupService orchestrator
└── adapters/
├── notion/
│ ├── NotionTaskRepository.ts ← reads Task DB
│ └── NotionStandupRepository.ts ← writes stand‑up pages
├── claude/
│ └── ClaudeSummaryGenerator.ts ← calls Claude API
├── mcp/
│ └── McpStandupAgent.ts ← Claude Agent SDK + Notion MCP
└── discord/
└── DiscordNotifier.ts ← posts to Discord webhook핵심 파일 – McpStandupAgent.ts
export async function runMcpStandupAgent({ verbose = false, dryRun = false } = {}) {
// Phase 1: fetch tasks via typed Notion client (reliable)
const { completed, active } = await taskRepo.fetchTasks();
// Phase 2: generate summary with Claude
const summary = await summarizer.generateSummary(completed, active);
// Phase 3: write the page autonomously via Notion MCP
const url = await writeStandupViaMcp(summary, completed, active, verbose);
// Phase 4: notify Discord
await notifyDiscord(summary, url, todayFormatted());
return url;
}Phase 3 은 Claude Opus 4.6 에이전트가 담당합니다. 직접 코딩한 Notion API 호출 대신, Claude가 MCP 도구를 이용해 워크스페이스를 자율적으로 탐색하면서 새 페이지를 만들지 기존 페이지를 업데이트할지 결정하고, 오래된 블록을 삭제한 뒤 새로운 내용을 추가합니다.
--verbose 옵션을 사용해 실행하면 Claude가 실시간으로 사고 과정을 출력합니다:
🔧 [MCP] [NOTION API] Post Search
💭 [Claude] A page already exists for today. I'll update it instead of creating a new one.
🔧 [MCP] [NOTION API] Get Block Children
🔧 [MCP] [NOTION API] Patch Page
💭 [Claude] Properties updated. Now deleting 14 old blocks...
🔧 [MCP] [NOTION API] Delete A Block
🔧 [MCP] [NOTION API] Delete A Block
...
🔧 [MCP] [NOTION API] Patch Block Children
💭 [Claude] Done! Here's the standup page: https://notion.so/...왜 MCP가 중요한가
전통적인 접근법 (수동 glue 코드)
// Traditional: you write every API call by hand
const existing = await notion.databases.query({ database_id, filter });
if (existing.results.length > 0) {
const blocks = await notion.blocks.children.list({ block_id });
await Promise.all(
blocks.results.map(b => notion.blocks.delete({ block_id: b.id }))
);
await notion.pages.update({ page_id, properties });
await notion.blocks.children.append({ block_id, children });
} else {
await notion.pages.create({ parent, properties, children });
}MCP 접근법 (Claude‑구동)
// MCP approach: Claude navigates the Notion API autonomously
for await (const message of query({
prompt: `Write today's standup page in the Standup Log DB.
Check if a page exists for ${todayISO()} — update it if so, create it if not.
Use callout blocks with these sections: Yesterday, Today, Blockers.`,
options: {
mcpServers: {
notion: {
command: 'npx',
args: ['-y', '@notionhq/notion-mcp-server'],
env: {
OPENAPI_MCP_HEADERS: JSON.stringify({
Authorization: `Bearer ${NOTION_API_KEY}`,
'Notion-Version': '2022-06-28',
}),
},
},
},
allowedTools: ['mcp__notion__*'],
permissionMode: 'acceptEdits',
},
})) { /* … */ }장점
| 기능 | 전통 방식 | MCP |
|---|---|---|
| 멱등성 | 수동 검증 필요 | Claude가 오늘 페이지 존재 여부를 확인하고 자동으로 업데이트합니다 |
| API 변경에 대한 복원력 | 스키마 변경 시 중단 | Claude가 런타임에 도구 사용을 조정합니다 |
| 가독성 / 디버깅 | 많은 보일러플레이트 | 상세 모드가 Claude가 무엇을 하고 왜 하는지 정확히 보여줍니다 |
| 드라이런 / 상세 | 사용자 정의 로깅 필요 | 내장 --dryRun 및 --verbose 플래그 |
Quick Start
# Clone the repo
git clone https://github.com/elpic/notion-of-progress
cd notion-of-progress
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
# Edit .env and add NOTION_API_KEY and ANTHROPIC_API_KEY
# Create the Notion databases automatically
npm run setup스탠드업 실행
mise run standup드라이‑런 모드
mise run standup -- --dry-run--dry-run은 Notion을 건드리지 않고 요약을 미리 보여줍니다—테스트에 안성맞춤입니다.
MCP 서버 구성
@notionhq/notion-mcp-server는 일반적으로 호스팅된 버전에서 OAuth가 필요하지만, 내부 통합 토큰을 사용해 stdio를 통해 로컬에서 실행할 수 있습니다—OAuth가 필요 없습니다.
mcpServers: {
notion: {
command: 'npx',
args: ['-y', '@notionhq/notion-mcp-server'],
env: {
OPENAPI_MCP_HEADERS: JSON.stringify({
Authorization: `Bearer ${config.notion.apiKey}`,
'Notion-Version': '2022-06-28',
}),
},
},
},핵심 인사이트: 헤더에 내부 토큰을 포함하여 MCP 서버를 로컬 서브프로세스로 실행하면 전체 프로젝트가 원활하게 작동합니다.