AI 에이전트를 만들어 Notion에 내 일일 스탠드업을 자동으로 작성하게 했어요

발행: (2026년 3월 16일 오후 12:06 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to


나는 매일 Notion에 스탠드업을 자동으로 작성해 주는 AI 에이전트를 만들었다

소개

매일 아침 팀 채널에 스탠드업 업데이트를 올리는 것이 일상이지만, 실제로는 “오늘 무엇을 했는가?”, “어떤 문제가 있었는가?” 같은 질문에 답하는 데 몇 분 정도밖에 걸리지 않는다.
그럼에도 불구하고 이 작업을 매일 반복하면 생산성이 떨어지고, 중요한 작업에 집중할 시간이 줄어든다.

이 글에서는 OpenAI GPT‑4, LangChain, 그리고 Notion API를 활용해, 내가 하루 동안 한 일을 자동으로 정리하고 Notion 페이지에 기록해 주는 AI 에이전트를 만드는 과정을 소개한다.

사용한 기술 스택

기술역할
Python메인 스크립트 언어
LangChainLLM(대형 언어 모델)과 툴(예: 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=xxxxxxxxxxxxxxxxxxxx

Tip: GitHub 레포에 .env 파일을 커밋하지 말고, GitHub Secrets 혹은 로컬 환경 변수로 관리한다.

2️⃣ Notion 데이터베이스 준비

  1. Notion에서 “Daily Standup” 이라는 데이터베이스를 만든다.
  2. 최소한 다음 속성을 포함한다.
    • Date (Date) – 스탠드업 날짜
    • Content (Text) – AI가 생성한 스탠드업 본문

데이터베이스 ID는 URL에서 확인할 수 있다. 예: https://www.notion.so/yourworkspace/xxxxxxxxxxxxxxxxxxxx?v=...xxxxxxxxxxxxxxxxxxxx 가 바로 ID다.

3️⃣ LangChain 에이전트 구현

main.py 에서는 크게 두 가지 작업을 수행한다.

  1. 프롬프트 생성 – 오늘 내가 한 일을 요약하도록 GPT‑4에 요청한다.
  2. 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>&1

GitHub 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% 이상을 달성했다.

마무리 및 향후 개선점

  1. 작업 자동 수집 – 현재는 수동으로 bullet point를 입력하고 있지만, Git 커밋 로그, JIRA 티켓, 혹은 로컬 타임 트래킹 툴과 연동하면 완전 자동화가 가능하다.
  2. 다중 언어 지원 – 프롬프트에 언어 옵션을 추가해 영어·한국어·일본어 등으로 스탠드업을 생성할 수 있다.
  3. 피드백 루프 – 팀원이 스탠드업을 검토하고 수정하면, 그 피드백을 모델에 전달해 다음 생성에 반영하도록 학습시킬 수 있다.

이 프로젝트가 도움이 되었나요?
GitHub에 코드를 공개했으니 자유롭게 포크하고, 여러분만의 워크플로에 맞게 커스터마이징해 보세요! 🚀

매일 하는 일

  1. Fetch tasks – Notion 작업 데이터베이스를 읽어 어제 완료된 작업과 오늘 진행 중인 작업을 찾습니다.
  2. Generate summary – Claude를 사용해 간결한 Yesterday / Today / Blockers 요약을 생성합니다.
  3. Write page – Notion MCP 서버를 통해 아름답게 포맷된 스탠드‑업 페이지를 자동으로 Notion 작업 공간에 작성합니다.
  4. 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 서버를 로컬 서브프로세스로 실행하면 전체 프로젝트가 원활하게 작동합니다.

0 조회
Back to Blog

관련 글

더 보기 »

트라비고

Gemini와 함께 말하는 속도만큼 빠르게 여행하세요! 라이브 에이전트가 몰입형 스토리텔링 및 3D 내비게이션과 만나는 곳. 이 프로젝트는 Gemini Live Ag...에 진입하기 위해 만들어졌습니다.