Notion + Claude Code Integration Guide
███╗ ██╗ ██████╗ ████████╗██╗ ██████╗ ███╗ ██╗
████╗ ██║██╔═══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
██╔██╗ ██║██║ ██║ ██║ ██║██║ ██║██╔██╗ ██║
██║╚██╗██║██║ ██║ ██║ ██║██║ ██║██║╚██╗██║
██║ ╚████║╚██████╔╝ ██║ ██║╚██████╔╝██║ ╚████║
╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝Notion + Claude Code Integration Guide
Focus: Automating documentation, project management, and knowledge base workflows from Claude Code using the official Notion MCP server and REST API.
Overview
Notion is an all-in-one workspace for docs, databases, wikis, and project management. The official @notionhq/notion-mcp-server lets Claude Code search pages, create and update content, query databases, manage tasks, and build automated documentation pipelines — turning Notion into a live integration layer for your development workflow.
Here is the big picture — your integration token unlocks a clean path from Claude Code to your workspace:
Official Documentation
| Resource | URL |
|---|---|
| Notion Developers | https://developers.notion.com |
| Notion API Reference | https://developers.notion.com/reference |
| Notion MCP Server | https://github.com/makenotion/notion-mcp-server |
| JavaScript Client | https://github.com/makenotion/notion-sdk-js |
| Integration Guide | https://developers.notion.com/docs/getting-started |
MCP Server Setup
Official Notion MCP Server
# Add via Claude Code CLI
claude mcp add notion -- npx -y @notionhq/notion-mcp-server.mcp.json Configuration
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ${NOTION_API_KEY}\", \"Notion-Version\": \"2022-06-28\"}"
}
}
}
}Get your Notion API key by creating an integration at: https://www.notion.so/my-integrations
Available MCP Tools
| Tool | Description |
|---|---|
notion_search | Search pages and databases by keyword |
notion_get_page | Get full page content |
notion_create_page | Create a new page in a database or parent |
notion_update_page | Update page properties |
notion_append_blocks | Append content blocks to a page |
notion_query_database | Filter and sort database entries |
notion_create_database | Create a new database |
notion_get_database | Get database schema |
notion_get_block_children | Get child blocks of a page |
notion_get_comments | Get page comments |
notion_create_comment | Add a comment to a page |
REST API Integration
You are just a few calls away from a working task flow — here is the typical search, query, and create lifecycle:
JavaScript / TypeScript Client
npm install @notionhq/clientimport { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// Search for pages
const search = await notion.search({
query: "API Design",
filter: { property: "object", value: "page" },
});
// Query a database (e.g., task tracker)
const tasks = await notion.databases.query({
database_id: process.env.NOTION_TASKS_DB_ID!,
filter: {
and: [
{ property: "Status", select: { equals: "In Progress" } },
{ property: "Assignee", people: { contains: "me" } },
],
},
sorts: [{ property: "Due Date", direction: "ascending" }],
});
// Create a new page (row in database)
const newTask = await notion.pages.create({
parent: { database_id: process.env.NOTION_TASKS_DB_ID! },
properties: {
Name: { title: [{ text: { content: "Fix auth middleware" } }] },
Status: { select: { name: "Todo" } },
Priority: { select: { name: "High" } },
"Due Date": { date: { start: "2025-05-20" } },
},
});
// Append content to a page
await notion.blocks.children.append({
block_id: newTask.id,
children: [
{
object: "block",
type: "heading_2",
heading_2: { rich_text: [{ text: { content: "Implementation Notes" } }] },
},
{
object: "block",
type: "paragraph",
paragraph: { rich_text: [{ text: { content: "Use JWT with RS256 signing." } }] },
},
{
object: "block",
type: "code",
code: {
language: "typescript",
rich_text: [{ text: { content: "const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });" } }],
},
},
],
});Python Client
pip install notion-clientfrom notion_client import Client
import os
notion = Client(auth=os.environ["NOTION_API_KEY"])
# Query database
results = notion.databases.query(
database_id=os.environ["NOTION_TASKS_DB_ID"],
filter={"property": "Status", "select": {"equals": "Done"}},
)
for page in results["results"]:
title = page["properties"]["Name"]["title"][0]["text"]["content"]
print(f"Completed: {title}")Pagination — querying large databases
Every Notion list/query endpoint (databases.query, search, blocks.children.list, users.list, comments.list) is paginated. A single call returns at most one page, so a databases.query against a 500-row task tracker will silently give you back only the first slice unless you follow the cursor. Skip this and your "all tasks" report quietly drops everyone past row 100.
The response shape is the same across endpoints:
| Field | Type | Meaning |
|---|---|---|
results | array | The items in the current page |
next_cursor | string | null | Pass as start_cursor to fetch the next page; null when finished |
has_more | boolean | true if another page exists |
Request side: send start_cursor to resume from a cursor, and page_size to control items per page (max 100, default 100). Omit start_cursor for the first page.
Here is the cursor loop — keep going while the server says there is more:
Copy-paste TS loop — fetch every row
import { Client } from "@notionhq/client";
import type { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
async function queryAllRows(databaseId: string) {
const rows: PageObjectResponse[] = [];
let cursor: string | undefined = undefined; // undefined → first page
do {
const response = await notion.databases.query({
database_id: databaseId,
filter: { property: "Status", select: { equals: "In Progress" } },
sorts: [{ property: "Due Date", direction: "ascending" }],
start_cursor: cursor,
page_size: 100, // max allowed; fewer round-trips
});
rows.push(...(response.results as PageObjectResponse[]));
cursor = response.next_cursor ?? undefined; // null → stop
} while (cursor !== undefined);
return rows;
}
const allTasks = await queryAllRows(process.env.NOTION_TASKS_DB_ID!);
console.log(`Fetched ${allTasks.length} tasks across all pages`);Helpers — let the SDK drive the cursor
The official client ships two pagination helpers so you never touch next_cursor by hand:
import { Client, iteratePaginatedAPI, collectPaginatedAPI } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const database_id = process.env.NOTION_TASKS_DB_ID!;
// Stream one row at a time — memory-efficient for huge databases
for await (const row of iteratePaginatedAPI(notion.databases.query, { database_id })) {
console.log(row.id);
}
// Collect everything into one array — only when the dataset fits in memory
const allRows = await collectPaginatedAPI(notion.databases.query, { database_id });
console.log(`Fetched ${allRows.length} rows`);Gotcha — page_size and rate limits.
page_sizeis capped at 100; asking for more is ignored, so a large database always needs multiple round-trips. Notion throttles at roughly 3 requests/second, so a deep paginated pull can trip a429. Keeppage_size: 100to minimize calls, and add a small delay between pages (await new Promise(r => setTimeout(r, 350))) or honour theRetry-Afterheader on 429s.
Environment Variables
# Required
NOTION_API_KEY=secret_... # From notion.so/my-integrations
# Optional — IDs for commonly used databases/pages
NOTION_TASKS_DB_ID=... # Your task tracker database ID
NOTION_DOCS_PAGE_ID=... # Your docs root page ID
NOTION_SPRINT_DB_ID=... # Sprint planning databaseFind database/page IDs from the URL: notion.so/[workspace]/[page-id]
Automation Workflows
Claude Code Hook: Auto-document on Feature Completion
.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node scripts/notion-update-docs.js"
}
]
}
]
}
}scripts/notion-update-docs.js:
import { Client } from "@notionhq/client";
import { execFileSync } from "child_process";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// Get last commit message
const commitMsg = execFileSync("git", ["log", "-1", "--pretty=%B"]).toString().trim();
if (!commitMsg.startsWith("feat:")) process.exit(0);
// Append to changelog page
await notion.blocks.children.append({
block_id: process.env.NOTION_CHANGELOG_PAGE_ID,
children: [{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{
text: {
content: `[${new Date().toISOString().slice(0, 10)}] ${commitMsg}`,
},
}],
},
}],
});
console.log("Changelog updated in Notion");Slash Command: Create Task in Notion
.claude/commands/notion-task.md:
Create a new task in the Notion task database for: $ARGUMENTS
Use the Notion MCP tool `notion_create_page` to add a row to the tasks database with:
- Name: $ARGUMENTS
- Status: Todo
- Priority: Medium
- Assignee: (leave blank)
- Source: "Claude Code"
Report the URL of the created Notion page.Usage: /project:notion-task "Refactor the authentication module"
CI/CD: Auto-update Notion Sprint Board
# .github/workflows/notion-update.yml
name: Update Notion on Deploy
on:
workflow_run:
workflows: ["Deploy to Production"]
types: [completed]
jobs:
update-notion:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm install @notionhq/client
- name: Update Notion
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
NOTION_SPRINT_DB_ID: ${{ secrets.NOTION_SPRINT_DB_ID }}
run: |
node -e "
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
notion.pages.create({
parent: { database_id: process.env.NOTION_SPRINT_DB_ID },
properties: {
Name: { title: [{ text: { content: 'Deployed: ${{ github.sha }}' } }] },
Status: { select: { name: 'Released' } },
Date: { date: { start: new Date().toISOString().slice(0,10) } }
}
}).then(() => console.log('Notion updated'));
"Common Use Cases
| Use Case | Approach |
|---|---|
| Task tracking | MCP notion_create_page in task DB |
| Sprint planning | MCP notion_query_database + analysis |
| Auto-changelog | Hook on git commit → notion_append_blocks |
| Knowledge base search | MCP notion_search for internal docs |
| Meeting notes | notion_create_page with structured template |
| Release notes | PR merge → auto-create Notion page |
Troubleshooting
| Issue | Fix |
|---|---|
| 401 Unauthorized | Check NOTION_API_KEY value; ensure integration is valid |
| Page not found (404) | Share the page/database with your integration in Notion UI |
| Properties missing | Database schema must match property names exactly (case-sensitive) |
| Blocks not rendering | Rich text must be an array; use [{ text: { content: "..." } }] |
| Rate limit (429) | Notion allows ~3 req/sec; add await new Promise(r => setTimeout(r, 350)) between calls |
Setup tip: For the integration to access a database, go to the database in Notion →
...menu → Connections → add your integration.