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.
What's Inside
- Why Notion Needs an AI Agent
- 7 Notion Agent Automations
- Notion API Deep Dive
- Build: Knowledge Base Q&A Agent
- Build: Database Enrichment Agent
- Build: Content Generation Agent
- Build: Workspace Cleanup Agent
- Production System Prompt
- Tool Comparison: 6 Notion AI Tools
- Build Your Notion Agent in 60 Minutes
- 6 Mistakes That Break Notion Agents
Why Notion Needs an AI Agent
Notion's built-in AI is fine for summarizing a page or fixing grammar. But it can't:
- Automatically enrich your CRM database with company info from the web
- Watch for stale pages and flag or archive them
- Generate weekly reports by pulling data from multiple databases
- Answer questions across your entire workspace (not just one page)
- Create structured pages from templates based on external triggers
- Keep databases in sync with external tools (Jira, HubSpot, Airtable)
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).
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
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.
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.
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.
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).
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.
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.
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 } }],
},
};
}
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
}
}
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 — €29Tool 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
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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 — €29What'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