← Back to dashboard
notiondocsfresh

Notion + Claude Code Integration Guide

Text
███╗   ██╗ ██████╗ ████████╗██╗ ██████╗ ███╗   ██╗
████╗  ██║██╔═══██╗╚══██╔══╝██║██╔═══██╗████╗  ██║
██╔██╗ ██║██║   ██║   ██║   ██║██║   ██║██╔██╗ ██║
██║╚██╗██║██║   ██║   ██║   ██║██║   ██║██║╚██╗██║
██║ ╚████║╚██████╔╝   ██║   ██║╚██████╔╝██║ ╚████║
╚═╝  ╚═══╝ ╚═════╝    ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝

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


MCP Server Setup

Official Notion MCP Server

Bash
# Add via Claude Code CLI
claude mcp add notion -- npx -y @notionhq/notion-mcp-server

.mcp.json Configuration

JSON
{
  "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

ToolDescription
notion_searchSearch pages and databases by keyword
notion_get_pageGet full page content
notion_create_pageCreate a new page in a database or parent
notion_update_pageUpdate page properties
notion_append_blocksAppend content blocks to a page
notion_query_databaseFilter and sort database entries
notion_create_databaseCreate a new database
notion_get_databaseGet database schema
notion_get_block_childrenGet child blocks of a page
notion_get_commentsGet page comments
notion_create_commentAdd 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

Bash
npm install @notionhq/client
TypeScript
import { 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

Bash
pip install notion-client
Python
from 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:

FieldTypeMeaning
resultsarrayThe items in the current page
next_cursorstring | nullPass as start_cursor to fetch the next page; null when finished
has_morebooleantrue 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

TypeScript
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:

TypeScript
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_size is 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 a 429. Keep page_size: 100 to minimize calls, and add a small delay between pages (await new Promise(r => setTimeout(r, 350))) or honour the Retry-After header on 429s.


Environment Variables

Bash
# 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 database

Find database/page IDs from the URL: notion.so/[workspace]/[page-id]


Automation Workflows

Claude Code Hook: Auto-document on Feature Completion

.claude/settings.json:

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/notion-update-docs.js"
          }
        ]
      }
    ]
  }
}

scripts/notion-update-docs.js:

JavaScript
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:

Markdown
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

YAML
# .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 CaseApproach
Task trackingMCP notion_create_page in task DB
Sprint planningMCP notion_query_database + analysis
Auto-changelogHook on git commit → notion_append_blocks
Knowledge base searchMCP notion_search for internal docs
Meeting notesnotion_create_page with structured template
Release notesPR merge → auto-create Notion page

Troubleshooting

IssueFix
401 UnauthorizedCheck NOTION_API_KEY value; ensure integration is valid
Page not found (404)Share the page/database with your integration in Notion UI
Properties missingDatabase schema must match property names exactly (case-sensitive)
Blocks not renderingRich 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.