February 20, 2026 · 18 min read

AI Agent for Notion: Automate Databases, Docs & Project Management

Notion is where your team's knowledge lives. But it's static — pages collect dust, databases go stale, and nobody reads the wiki. An AI agent turns Notion into a living, self-organizing system.

Why Notion Needs an AI Agent

Notion's built-in AI is fine for summarizing a page or fixing grammar. But it can't:

That's what a custom AI agent does. It treats Notion as both a knowledge source (reading from it) and an action target (writing to it).

💡 The ROI

Teams using Notion AI agents report saving 5-10 hours/week on manual data entry, page organization, and information retrieval. A single database enrichment agent can replace 2-3 hours of daily research work.

7 Notion Agent Automations

Automation 1

Knowledge Base Q&A

Index all your Notion pages into a vector database. Ask questions in natural language via Slack, a web interface, or even inside Notion itself. Get answers with source links back to the exact Notion page.

Automation 2

Database Auto-Enrichment

Add a company to your CRM database with just the name. The agent fills in: website, industry, employee count, funding, description, LinkedIn URL — all pulled from the web automatically.

Automation 3

Meeting Notes → Action Items

Paste raw meeting notes into a Notion page. The agent extracts action items, creates linked tasks in your project database, assigns owners, and sets due dates.

Automation 4

Content Calendar Management

The agent manages your content calendar database: suggests topics based on gaps, generates outlines, drafts content, and moves items through your workflow (Idea → Draft → Review → Published).

Automation 5

Workspace Cleanup & Organization

Finds pages not updated in 90+ days. Identifies duplicate content. Suggests merges, archives, or updates. Keeps your workspace lean instead of becoming a digital graveyard.

Automation 6

Weekly Report Generator

Every Friday, the agent queries your project databases, pulls completed tasks, calculates metrics, and creates a formatted weekly report page. Zero manual work.

Automation 7

External Data Sync

Keep Notion databases in sync with external sources: GitHub issues → Notion project tracker, HubSpot deals → Notion CRM, Google Analytics → Notion metrics dashboard. Bi-directional when needed.

Notion API Deep Dive

Everything your agent does flows through the Notion API. Here are the key operations:

import { Client } from "@notionhq/client";

const notion = new Client({ auth: process.env.NOTION_API_KEY });

// ─── READ OPERATIONS ───

// Search across entire workspace
async function searchNotion(query) {
  const response = await notion.search({
    query,
    filter: { property: "object", value: "page" },
    sort: { direction: "descending", timestamp: "last_edited_time" },
    page_size: 20,
  });
  return response.results;
}

// Get all items from a database
async function queryDatabase(databaseId, filter = undefined) {
  const pages = [];
  let cursor = undefined;

  while (true) {
    const response = await notion.databases.query({
      database_id: databaseId,
      filter,
      start_cursor: cursor,
      page_size: 100,
    });
    pages.push(...response.results);
    if (!response.has_more) break;
    cursor = response.next_cursor;
  }
  return pages;
}

// Get page content as blocks
async function getPageContent(pageId) {
  const blocks = [];
  let cursor = undefined;

  while (true) {
    const response = await notion.blocks.children.list({
      block_id: pageId,
      start_cursor: cursor,
      page_size: 100,
    });
    blocks.push(...response.results);
    if (!response.has_more) break;
    cursor = response.next_cursor;
  }
  return blocks;
}

// Convert blocks to readable text
function blocksToText(blocks) {
  return blocks.map(block => {
    const type = block.type;
    const content = block[type];
    if (!content) return "";

    if (content.rich_text) {
      return content.rich_text.map(t => t.plain_text).join("");
    }
    if (type === "code") {
      return `\`\`\`${content.language}\n${
        content.rich_text.map(t => t.plain_text).join("")
      }\n\`\`\``;
    }
    return "";
  }).filter(Boolean).join("\n");
}

// ─── WRITE OPERATIONS ───

// Create a new page in a database
async function createDatabaseItem(databaseId, properties) {
  return await notion.pages.create({
    parent: { database_id: databaseId },
    properties,
  });
}

// Update page properties
async function updatePage(pageId, properties) {
  return await notion.pages.update({
    page_id: pageId,
    properties,
  });
}

// Add content blocks to a page
async function appendBlocks(pageId, blocks) {
  return await notion.blocks.children.append({
    block_id: pageId,
    children: blocks,
  });
}

// Helper: create a text block
function textBlock(content, type = "paragraph") {
  return {
    type,
    [type]: {
      rich_text: [{ type: "text", text: { content } }],
    },
  };
}
⚠️ API rate limits

Notion API allows 3 requests/second per integration. For bulk operations, add a 350ms delay between requests. Use batch reads where possible (query database returns up to 100 items per call). Cache frequently accessed pages.

Build: Knowledge Base Q&A Agent

The most powerful Notion agent — index your entire workspace and answer questions from it:

import Anthropic from "@anthropic-ai/sdk";
import { createClient } from "@supabase/supabase-js";

const anthropic = new Anthropic();
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// Step 1: Index all Notion pages
async function indexWorkspace() {
  const pages = await searchNotion(""); // Get all pages
  let indexed = 0;

  for (const page of pages) {
    const blocks = await getPageContent(page.id);
    const text = blocksToText(blocks);
    if (!text || text.length < 50) continue;

    // Get page title
    const title = page.properties?.title?.title?.[0]?.plain_text
      || page.properties?.Name?.title?.[0]?.plain_text
      || "Untitled";

    // Chunk the text (500 tokens per chunk, 50 token overlap)
    const chunks = chunkText(text, 500, 50);

    for (const chunk of chunks) {
      const embedding = await getEmbedding(chunk);
      await supabase.from("notion_docs").upsert({
        page_id: page.id,
        title,
        content: chunk,
        embedding,
        url: page.url,
        last_edited: page.last_edited_time,
      });
    }

    indexed++;
    await sleep(350); // Rate limiting
  }
  console.log(`Indexed ${indexed} pages`);
}

// Step 2: Answer questions
async function askNotion(question) {
  const embedding = await getEmbedding(question);

  // Search for relevant chunks
  const { data: docs } = await supabase.rpc("match_notion_docs", {
    query_embedding: embedding,
    match_threshold: 0.7,
    match_count: 5,
  });

  const context = docs.map(d =>
    `[${d.title}](${d.url}):\n${d.content}`
  ).join("\n\n---\n\n");

  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1000,
    system: `You answer questions based on the team's Notion workspace.
Only use the provided context. Cite sources as [Page Title](url).
If the answer isn't in the context, say so. Be concise.`,
    messages: [{
      role: "user",
      content: `Context from Notion:\n${context}\n\nQuestion: ${question}`,
    }],
  });

  return {
    answer: response.content[0].text,
    sources: docs.map(d => ({ title: d.title, url: d.url })),
  };
}

// Step 3: Keep index fresh (run daily)
async function updateIndex() {
  const yesterday = new Date(Date.now() - 86400000).toISOString();
  const updated = await notion.search({
    filter: { property: "object", value: "page" },
    sort: { direction: "descending", timestamp: "last_edited_time" },
  });

  const recentPages = updated.results.filter(
    p => p.last_edited_time > yesterday
  );

  for (const page of recentPages) {
    // Re-index only changed pages
    await indexPage(page);
    await sleep(350);
  }
  console.log(`Re-indexed ${recentPages.length} updated pages`);
}

Build: Database Enrichment Agent

This agent watches a Notion CRM database and auto-fills missing data:

async function enrichCRMDatabase(databaseId) {
  // Get all entries missing key data
  const entries = await queryDatabase(databaseId, {
    or: [
      { property: "Industry", select: { is_empty: true } },
      { property: "Employee Count", number: { is_empty: true } },
      { property: "Description", rich_text: { is_empty: true } },
    ],
  });

  console.log(`Found ${entries.length} entries to enrich`);

  for (const entry of entries) {
    const name = entry.properties.Name?.title?.[0]?.plain_text;
    const website = entry.properties.Website?.url;
    if (!name) continue;

    // Use Claude to research the company
    const response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 500,
      messages: [{
        role: "user",
        content: `Research this company and return JSON:
{
  "industry": "string",
  "employee_count": number or null,
  "description": "1-2 sentence description",
  "founded_year": number or null,
  "headquarters": "city, country"
}

Company: ${name}
Website: ${website || "unknown"}

Be factual. Use null if you're not confident.`,
      }],
    });

    const data = JSON.parse(response.content[0].text);

    // Update the Notion entry
    await updatePage(entry.id, {
      Industry: data.industry
        ? { select: { name: data.industry } }
        : undefined,
      "Employee Count": data.employee_count
        ? { number: data.employee_count }
        : undefined,
      Description: data.description
        ? { rich_text: [{ text: { content: data.description } }] }
        : undefined,
      Headquarters: data.headquarters
        ? { rich_text: [{ text: { content: data.headquarters } }] }
        : undefined,
    });

    console.log(`✅ Enriched: ${name}`);
    await sleep(1000); // Be gentle with APIs
  }
}
💡 Enrich with real data

For production use, combine Claude with real data APIs: Clearbit (company data), Hunter.io (email finding), Crunchbase (funding data), LinkedIn API (employee count). Use Claude to synthesize and fill gaps, not as the primary data source.

Build: Content Generation Agent

async function generateContentFromCalendar(databaseId) {
  // Get items in "Idea" status
  const ideas = await queryDatabase(databaseId, {
    property: "Status",
    select: { equals: "Idea" },
  });

  for (const idea of ideas) {
    const topic = idea.properties.Topic?.title?.[0]?.plain_text;
    const format = idea.properties.Format?.select?.name || "blog";
    const audience = idea.properties.Audience?.select?.name || "general";

    if (!topic) continue;

    // Generate outline
    const response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2000,
      messages: [{
        role: "user",
        content: `Create a detailed outline for a ${format} about:
"${topic}"

Target audience: ${audience}

Return a structured outline with:
- Hook / opening angle
- 5-7 main sections with key points
- CTA / closing
- SEO keywords to target (3-5)
- Estimated word count`,
      }],
    });

    const outline = response.content[0].text;

    // Add outline as content blocks on the Notion page
    await appendBlocks(idea.id, [
      textBlock("📝 AI-Generated Outline", "heading_2"),
      textBlock(outline),
      textBlock("---"),
      textBlock("⚠️ Review and edit this outline before writing.", "callout"),
    ]);

    // Update status to "Outlined"
    await updatePage(idea.id, {
      Status: { select: { name: "Outlined" } },
    });

    console.log(`📝 Outlined: ${topic}`);
    await sleep(500);
  }
}

Build: Workspace Cleanup Agent

async function cleanupWorkspace() {
  const allPages = await searchNotion("");
  const now = new Date();
  const staleThreshold = 90; // days
  const report = { stale: [], empty: [], duplicates: [] };

  for (const page of allPages) {
    const lastEdited = new Date(page.last_edited_time);
    const daysSinceEdit = (now - lastEdited) / (1000 * 60 * 60 * 24);
    const title = page.properties?.title?.title?.[0]?.plain_text
      || page.properties?.Name?.title?.[0]?.plain_text
      || "Untitled";

    // Flag stale pages
    if (daysSinceEdit > staleThreshold) {
      report.stale.push({
        title,
        url: page.url,
        days_stale: Math.round(daysSinceEdit),
      });
    }

    // Flag empty or near-empty pages
    const blocks = await getPageContent(page.id);
    const text = blocksToText(blocks);
    if (text.length < 20) {
      report.empty.push({ title, url: page.url });
    }

    await sleep(350);
  }

  // Find duplicate titles
  const titleMap = {};
  for (const page of allPages) {
    const title = page.properties?.title?.title?.[0]?.plain_text?.toLowerCase();
    if (!title) continue;
    if (titleMap[title]) {
      report.duplicates.push({
        title,
        pages: [titleMap[title], page.url],
      });
    } else {
      titleMap[title] = page.url;
    }
  }

  // Create a cleanup report page
  const reportPage = await notion.pages.create({
    parent: { page_id: WORKSPACE_ROOT_ID },
    properties: {
      title: { title: [{ text: { content:
        `🧹 Workspace Cleanup — ${now.toLocaleDateString()}`
      }}] },
    },
  });

  await appendBlocks(reportPage.id, [
    textBlock(`Found ${report.stale.length} stale pages, ` +
      `${report.empty.length} empty pages, ` +
      `${report.duplicates.length} potential duplicates.`),
    textBlock("Stale Pages (90+ days)", "heading_2"),
    ...report.stale.slice(0, 20).map(p =>
      textBlock(`• ${p.title} — ${p.days_stale} days since edit`)
    ),
    textBlock("Empty Pages", "heading_2"),
    ...report.empty.map(p => textBlock(`• ${p.title}`)),
    textBlock("Potential Duplicates", "heading_2"),
    ...report.duplicates.map(d => textBlock(`• "${d.title}"`)),
  ]);

  return report;
}

Production System Prompt

const NOTION_AGENT_PROMPT = `You are a Notion workspace assistant
for [COMPANY_NAME].

## Capabilities
- Search and read any page in the workspace
- Create new pages and database entries
- Update existing pages and properties
- Query databases with filters
- Generate content based on templates
- Organize and clean up the workspace

## Rules
1. NEVER delete pages without explicit confirmation
2. When creating pages, always use the team's existing templates
3. For database entries, match existing property formats exactly
4. When answering questions, always link to source pages
5. If asked to do something outside your capabilities, suggest
   alternatives (e.g., "I can't connect to Jira directly, but
   I can create a manual sync template")
6. Respect page permissions — don't share content from restricted
   pages in public channels

## Database Conventions
- Status values: Not Started → In Progress → Review → Done
- Date format: ISO 8601
- Tags: Use existing tags first, create new ones sparingly
- Assignees: Use @mentions with user IDs

## Content Standards
- Match the team's writing style and tone
- Use existing heading hierarchy
- Include relevant cross-links to other Notion pages
- Add a "Last updated by AI" callout when modifying pages

## Safety
- Always preview changes before applying to important pages
- Create a backup block before overwriting content
- Log all modifications with timestamps
- Rate limit: max 100 API calls per session
`;

Want all 13 agent templates ready to deploy?

The AI Employee Playbook includes Notion agents, Slack bots, email assistants, and 10 more — with complete code and system prompts.

Get the Playbook — €29

Tool Comparison: 6 Notion AI Tools

Tool Best For Price Custom Logic
Notion AI (native) In-page summarization, writing help $10/user/mo ❌ None
Zapier + Notion Simple triggers (new item → action) Free / $20/mo ⚠️ Limited
Make (Integromat) Complex multi-step workflows Free / $9/mo ⚠️ Moderate
n8n + Notion Self-hosted automation Free (self-host) ✅ Full
Bardeen Browser-based Notion automation Free / $10/mo ⚠️ Some
Custom Agent (this guide) Full control, AI-powered logic ~$5-15/mo (API) ✅ Full

Our recommendation: Use Notion AI for in-page writing help. Use Zapier/Make for simple triggers. Build a custom agent when you need AI-powered enrichment, cross-database intelligence, or complex multi-step workflows that no-code tools can't handle.

Build Your Notion Agent in 60 Minutes

Step 1 (5 min)

Create a Notion Integration

Go to notion.so/my-integrations. Create a new integration. Copy the API key. Share the pages/databases you want the agent to access with the integration.

Step 2 (10 min)

Set Up the Project

npm init -y && npm install @notionhq/client @anthropic-ai/sdk @supabase/supabase-js. Copy the API helper functions from the "Notion API Deep Dive" section above. Set environment variables.

Step 3 (20 min)

Index Your Workspace

Run the indexWorkspace function to crawl all shared pages. This creates embeddings and stores them in Supabase. For a small workspace (< 100 pages), this takes about 5 minutes.

Step 4 (15 min)

Build the Interface

For a quick start, create a simple CLI: node ask.js "What's our refund policy?". For team use, connect to Slack (see our Slack Agent guide) or build a simple web UI with Next.js.

Step 5 (10 min)

Add Automation

Set up a cron job for nightly re-indexing and database enrichment. Use GitHub Actions (free) or a simple setInterval in your Node.js process. Add the cleanup agent on a weekly schedule.

6 Mistakes That Break Notion Agents

Mistake 1

Not sharing pages with the integration

The #1 reason Notion agents "don't work." You must explicitly share each page or database with your integration. The API can only access pages that have been shared. Use a top-level page and share that — child pages inherit access.

Mistake 2

Ignoring pagination

The Notion API returns max 100 items per request. If your database has 500 entries, you need to paginate using start_cursor and has_more. The code examples above handle this — don't skip it.

Mistake 3

Not handling nested blocks

Notion pages have nested content (toggle blocks, columns, synced blocks). The blocks API only returns top-level blocks by default. You need to recursively fetch children for a complete page extraction.

Mistake 4

Overwriting without backups

When your agent updates a page, always create a backup first. Append a "Previous version" toggle block before replacing content. Notion has version history, but it's easier to have an in-page backup.

Mistake 5

Stale index

If you index once and never update, answers become wrong fast. Set up incremental re-indexing: check last_edited_time against your last index run. Only re-index changed pages. Run nightly minimum.

Mistake 6

Wrong property types in updates

Notion's API is strict about property types. A "Select" property needs { select: { name: "value" } }, not just "value". A "Number" needs { number: 42 }. Get the database schema first and match types exactly, or your updates will silently fail.

Turn your Notion into an intelligent workspace

The AI Employee Playbook includes the complete Notion agent setup with enrichment, Q&A, and cleanup — plus 12 other production-ready agent templates.

Get the Playbook — €29

What's Next

Start with the Knowledge Base Q&A agent — it's the highest ROI and easiest to build. Once that's working, add database enrichment for your CRM or content calendar. Then set up the cleanup agent on a weekly cron.

The goal isn't to automate everything in Notion. It's to automate the tedious parts — data entry, organization, information retrieval — so your team can focus on the work that actually matters: thinking, creating, and deciding.

Want the complete agent-building system?

The AI Employee Playbook covers the 3-file framework, memory systems, autonomy rules, and real production examples.

Get the Playbook — €29