Notion API Integration
Integrates with the Notion API to programmatically create and update pages, query databases, manipulate blocks, perform searches, and manage comments using notion-client (the Python SDK for the official Notion API).
TL;DR for Code Generation
- Use
notion_client.Clientwith an integration token from the Notion Integrations dashboard - Always call
databases.query()with typedfilterandsortsdicts — Notion requires structured queries - Use
pages.update()withpropertiesdict for partial page updates - Paginate with
has_more/next_cursor— Notion returns max 100 results per page - Append block children via
blocks.children.append(), not through page update - Handle
notion_client.errors.APIResponseErrorfor permission, validation, and rate-limit errors
When to Use
Use this skill when:
- Creating, reading, updating, or archiving Notion pages
- Querying Notion databases with filters (text, date, select, multi-select, formula)
- Appending or modifying page content (blocks: paragraphs, headings, lists, code, embeds)
- Searching across pages and databases with full-text search
- Adding or listing page comments and discussions
- Synchronizing Notion databases with external data sources (CRM, spreadsheets, APIs)
- Building self-updating documentation portals backed by Notion
When NOT to Use
- High-frequency real-time sync (Notion has rate limits — use webhook alternatives or queue updates)
- File storage (upload files to a dedicated store and embed links in Notion)
- Large-scale ETL (Notion query performance degrades beyond 10K+ records per database)
- Replacing a traditional relational database (Notion is designed for flexible content, not ACID compliance)
- Anonymous API access — Notion requires an integration token or OAuth for every request
Core Workflow
Create an Integration — Go to
https://www.notion.so/my-integrationsand create a new internal integration. Copy the "Internal Integration Secret" token. Checkpoint: Verify the integration is shared with the target page or database (the "Connections" menu in Notion).Initialize the Client —
client = Client(auth=os.environ["NOTION_TOKEN"]). The client wrapshttpxand handles authentication. Checkpoint: Callclient.search(query="")with an empty query to list accessible pages.Query a Database — Use
client.databases.query(database_id=..., filter=..., sorts=...). Build filters using the Notion filter structure:{"property": "Status", "select": {"equals": "Done"}}. Checkpoint: Verify the response containsresultsand parsehas_morefor pagination.Create or Update a Page — Create with
client.pages.create(parent=..., properties=...). Update withclient.pages.update(page_id=..., properties=...). Map Python types to Notion property types. Checkpoint: Confirm the page URL is accessible in the browser.Manipulate Blocks — Fetch blocks with
client.blocks.children.list(block_id=...). Append withclient.blocks.children.append(block_id=..., children=[...]). Each child is a typed dict. Checkpoint: Verify the new block renders at the correct position in the page.Handle Errors — Wrap in
try/except APIResponseError. Checkerror.codeforunauthorized(401),restricted_resource(403),conflict_error(409),rate_limited(429). Checkpoint: Log theerror.cursorif available for debugging.
Implementation Patterns
Pattern 1: Query a Database with Filters
import os
from notion_client import Client
notion = Client(auth=os.environ["NOTION_TOKEN"])
def query_completed_tasks(database_id: str, days: int = 7) -> list[dict]:
"""Fetch tasks completed within the last N days from a Notion database."""
from datetime import datetime, timedelta
cutoff = datetime.now() - timedelta(days=days)
results = notion.databases.query(
database_id=database_id,
filter={
"and": [
{"property": "Status", "select": {"equals": "Done"}},
{
"property": "Completed Date",
"date": {"on_or_after": cutoff.strftime("%Y-%m-%d")},
},
]
},
sorts=[{"property": "Completed Date", "direction": "descending"}],
)
return results.get("results", [])
Pattern 2: Create a Page in a Database
def create_task_page(
database_id: str,
title: str,
status: str = "To Do",
priority: str | None = None,
) -> dict:
"""Add a new task page to a Notion database."""
properties = {
"Title": {"title": [{"text": {"content": title}}]},
"Status": {"select": {"name": status}},
}
if priority:
properties["Priority"] = {"select": {"name": priority}}
return notion.pages.create(
parent={"database_id": database_id},
properties=properties,
)
task = create_task_page("db123abc", "Fix login timeout", "In Progress", "High")
print(f"Created page: {task['url']}")
Pattern 3: Append Blocks to a Page
def append_code_block(page_id: str, code: str, language: str = "python") -> dict:
"""Append a code block to an existing Notion page."""
return notion.blocks.children.append(
block_id=page_id,
children=[
{
"object": "block",
"type": "code",
"code": {
"rich_text": [{"type": "text", "text": {"content": code}}],
"language": language,
},
}
],
)
code_snippet = 'print("Hello, Notion!")\ndef deploy():\n pass'
append_code_block("page456xyz", code_snippet, "python")
Pattern 4: BAD vs GOOD — Property Updates
# ❌ BAD — replaces entire properties dict, can delete other fields
notion.pages.update(page_id="abc", properties={"Status": {"select": {"name": "Done"}}})
# ✅ GOOD — fetches current, merges status update only
def update_page_status(page_id: str, status: str) -> dict:
"""Update only the Status property of a page."""
return notion.pages.update(
page_id=page_id,
properties={"Status": {"select": {"name": status}}},
)
Pattern 5: BAD vs GOOD — Pagination
# ❌ BAD — assumes single page, misses data
results = notion.databases.query(database_id="db123")
all_pages = results["results"]
# ✅ GOOD — paginates through all results
def query_all(database_id: str, **kwargs) -> list[dict]:
"""Paginate through all database results."""
all_results = []
start_cursor = None
while True:
response = notion.databases.query(
database_id=database_id,
start_cursor=start_cursor,
**kwargs,
)
all_results.extend(response.get("results", []))
if not response.get("has_more"):
break
start_cursor = response.get("next_cursor")
return all_results
Constraints
MUST DO
- Use environment variables or secret managers for the Notion integration token
- Always paginate database queries — Notion caps at 100 items per response
- Share each integration with the specific pages/databases it needs (least privilege)
- Validate property names and types match the database schema before writing
- Use
rich_textarray structure (not a plain string) for text properties
MUST NOT DO
- Hardcode page IDs or database IDs — use configuration or environment variables
- Poll Notion for changes (no webhook API exists yet — use a sync layer or poll sparingly)
- Assume property values always exist — check for
Nonebefore accessing - Use the token in client-side code where it can be extracted
Output Template
Every integration function should expose:
- Client Initialization —
Client(auth=token)with token from environment - Query or Command — Typed database query or page update structure
- Pagination —
has_more/next_cursorloop for list endpoints - Data Transformation — Map Notion property types to Python types
- Error Handling —
try/except APIResponseErrorwitherror.codedifferentiation
Related Skills
| Skill | Purpose | |