I Built an AI Agent That Writes My Daily Standup in Notion Automatically
Source: Dev.to
What it does every day
- Fetch tasks – reads your Notion task database to find what was completed yesterday and what’s active today.
- Generate summary – creates a concise Yesterday / Today / Blockers summary using Claude.
- Write page – autonomously writes a beautifully formatted stand‑up page back into your Notion workspace via the Notion MCP server.
- Notify – posts a summary to Discord so your team stays in the loop.
No forms. No copy‑pasting. Just open Notion and your stand‑up is already there.
Architecture diagram
┌─────────────────────────────────────────────────────────────┐
│ 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 completed · 4 active · 1 blocker |
|---|---|
| ✅ Yesterday | Bullet points with direct links to completed tasks |
| 🔨 Today | Bullet points with direct links to active tasks |
| 🚧 Blockers | Red callout if blockers exist, gray if none |
Repository
GitHub:
The project is built in TypeScript using a ports‑and‑adapters architecture; the core domain has zero knowledge of Notion, Claude, or any external system.
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 webhookKey file – 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 is where a Claude Opus 4.6 agent takes over. Instead of hand‑coded Notion API calls, Claude autonomously navigates the workspace via MCP tools, deciding whether to create a new page or update an existing one, deleting stale blocks, and appending fresh content.
Run it with --verbose to watch Claude think in real time:
🔧 [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/...Why MCP matters
Traditional approach (manual glue code)
// 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 approach (Claude‑driven)
// 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',
},
})) { /* … */ }Benefits
| Feature | Traditional | MCP |
|---|---|---|
| Idempotency | Manual checks required | Claude checks if today’s page exists and updates it automatically |
| Resilience to API changes | Breaks on schema changes | Claude adapts its tool usage at runtime |
| Readability / Debugging | Lots of boilerplate | Verbose mode shows exactly what Claude is doing and why |
| Dry‑run / Verbose | Requires custom logging | Built‑in --dryRun and --verbose flags |
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 setupRunning the Stand‑up
mise run standupDry‑Run Mode
mise run standup -- --dry-run--dry-run previews the summary without touching Notion—perfect for testing.
MCP Server Configuration
The @notionhq/notion-mcp-server normally requires OAuth for the hosted version, but you can run it locally via stdio with an internal integration token—no OAuth needed.
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',
}),
},
},
},Key Insight: Spawning the MCP server as a local subprocess with the internal token in the headers makes the whole project work seamlessly.