I Built an AI Agent That Writes My Daily Standup in Notion Automatically

Published: (March 15, 2026 at 11:06 PM EDT)
5 min read
Source: Dev.to

Source: Dev.to

What it does every day

  1. Fetch tasks – reads your Notion task database to find what was completed yesterday and what’s active today.
  2. Generate summary – creates a concise Yesterday / Today / Blockers summary using Claude.
  3. Write page – autonomously writes a beautifully formatted stand‑up page back into your Notion workspace via the Notion MCP server.
  4. 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
YesterdayBullet points with direct links to completed tasks
🔨 TodayBullet points with direct links to active tasks
🚧 BlockersRed 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 webhook

Key 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

FeatureTraditionalMCP
IdempotencyManual checks requiredClaude checks if today’s page exists and updates it automatically
Resilience to API changesBreaks on schema changesClaude adapts its tool usage at runtime
Readability / DebuggingLots of boilerplateVerbose mode shows exactly what Claude is doing and why
Dry‑run / VerboseRequires custom loggingBuilt‑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 setup

Running the Stand‑up

mise run standup

Dry‑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.

0 views
Back to Blog

Related posts

Read more »

Travigo

Travel as fast as you speak with Gemini! Where live agents meet immersive storytelling & 3D navigation. This project was created for entering the Gemini Live Ag...

Micro games

Hey Gamers! 👾 As part of the Rapid Games Prototyping module, we are tasked with reviewing a peer's game. The challenge is to analyse a prototype built in just...