When people say they want an AI integrated with Notion, they usually mean one of three very different things:
- “I want the AI to read my Notion pages and answer questions”
- “I want the AI to write structured data into my Notion databases”
- “I want Notion to be the AI’s brain — where it stores what it knows about me”
These are not the same integration. They have different auth paths, different API patterns, and different failure modes. This guide covers all three, plus the quirks Notion’s rich-text JSON model throws at you once you get past the basic API call.
Understanding Notion’s Auth Model First
Before any code, a critical decision: internal integration token vs. public OAuth.
Internal Integration Token
An internal token is what you create at notion.so/my-integrations. It works only within your workspace. For personal or team use, this is almost always the right choice.
How to get one:
- Go to notion.so/my-integrations
- Click New integration
- Name it (e.g., “OpenClaw”)
- Select your workspace
- Under Capabilities, enable:
- Read content ✓
- Update content ✓
- Insert content ✓
- Click Submit
- Copy the Internal Integration Secret — this is your API key
Critical step most people miss: The integration doesn’t automatically have access to your pages. You have to share each page or database with the integration explicitly.
In Notion: Open any page → Click ... in the top-right → Connections → find your integration → Confirm.
If you skip this, every API call returns a 404 even though your credentials are valid. It’s not an auth error — it’s a permissions error that looks like a 404.
Public OAuth (When You Actually Need It)
Public OAuth is for applications other people will use. You’re not building a SaaS — you’re setting up OpenClaw for yourself. Use internal tokens unless you have a specific reason not to.
The exception: if you want OpenClaw to access workspaces across multiple Notion accounts (like if you manage client workspaces separately), then you need OAuth. The setup involves creating a public integration, getting client ID and secret, and handling the OAuth callback URL. For personal use, this is overkill.
Setting Up the Notion Skill in OpenClaw
Once you have your internal integration token:
# In your OpenClaw config
notion_api_key: "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Or via environment variable:
export NOTION_API_KEY="secret_xxxx..."
The OpenClaw Notion skill will use this to authenticate all API calls. Verify the connection:
@openclaw check notion connection
Expected response: “Notion connected. Found X databases in accessible pages.”
If you get “No databases found,” you haven’t shared any pages with the integration yet. Go back and share them.
Reading from Notion: Pages and Databases
Reading a Page
@openclaw read my "Product Roadmap" Notion page
Under the hood, OpenClaw queries the Notion API with:
GET /v1/pages/{page_id}
GET /v1/blocks/{page_id}/children
The page content comes back as an array of blocks. Each block has a type (paragraph, heading_1, bulleted_list_item, etc.) and the text content lives inside a rich_text array.
This is where Notion gets annoying: the rich_text array is not a flat string. It’s an array of objects with annotations:
"rich_text": [
{
"type": "text",
"text": { "content": "This is " },
"annotations": { "bold": false, "italic": false }
},
{
"type": "text",
"text": { "content": "bold" },
"annotations": { "bold": true }
},
{
"type": "text",
"text": { "content": " and this is not." },
"annotations": { "bold": false }
}
]
OpenClaw’s Notion skill flattens this automatically when reading, but if you’re building custom workflows, you’ll encounter this. Keep a mental model of Notion’s block tree: every piece of content is a block, blocks can have children, and text inside blocks is always a rich_text array.
Querying a Database
Databases are different from pages. A Notion database is a collection of pages with a schema — each page in the database has properties (columns in spreadsheet terms).
@openclaw query my "Tasks" database where Status = "In Progress"
OpenClaw translates this to a Notion filter:
POST /v1/databases/{database_id}/query
{
"filter": {
"property": "Status",
"status": { "equals": "In Progress" }
}
}
Schema-aware queries matter here. The filter format depends on the property type. A select property uses different filter syntax than a multi_select, a date, or a relation. OpenClaw’s skill reads the database schema first, then constructs the right filter type. If you’re writing custom skills, always call GET /v1/databases/{id} first to inspect the schema before querying.
Filtering and Sorting
@openclaw query "Projects" database where Priority = "High" sort by "Last edited" descending limit 10
Compound filters use and / or wrappers:
{
"filter": {
"and": [
{ "property": "Priority", "select": { "equals": "High" } },
{ "property": "Status", "select": { "does_not_equal": "Done" } }
]
},
"sorts": [
{ "timestamp": "last_edited_time", "direction": "descending" }
],
"page_size": 10
}
Writing to Notion: The Rich-Text JSON Problem
Reading from Notion is straightforward. Writing is where most people get stuck.
Creating a Page in a Database
@openclaw create a new page in "Tasks" database with title "Review Q3 metrics" status "To Do" priority "High" due "2026-06-01"
The API call looks like:
POST /v1/pages
{
"parent": { "database_id": "your-database-id" },
"properties": {
"Name": {
"title": [{ "text": { "content": "Review Q3 metrics" } }]
},
"Status": {
"status": { "name": "To Do" }
},
"Priority": {
"select": { "name": "High" }
},
"Due": {
"date": { "start": "2026-06-01" }
}
}
}
The most common error: Using the wrong property type. If your database has a Status column using Notion’s native Status type (introduced in 2022), it uses "status". If it’s a Select property named “Status,” it uses "select". They look identical in the Notion UI but have different API shapes. Always check GET /v1/databases/{id} to see the actual property types.
Writing Rich Page Content (Blocks)
Creating a database entry with just properties gives you a blank page body. To add actual content:
@openclaw create a meeting notes page in "Notes" database with today's date and add a summary of [meeting context]
To add content to a page:
PATCH /v1/blocks/{page_id}/children
{
"children": [
{
"type": "heading_2",
"heading_2": {
"rich_text": [{ "text": { "content": "Meeting Summary" } }]
}
},
{
"type": "paragraph",
"paragraph": {
"rich_text": [{ "text": { "content": "Discussed Q3 targets..." } }]
}
},
{
"type": "bulleted_list_item",
"bulleted_list_item": {
"rich_text": [{ "text": { "content": "Action item 1" } }]
}
}
]
}
The Rich-Text Quirks You’ll Hit
Quirk 1: Annotations don’t merge across text runs
If you want “Hello world” you need two separate objects in the rich_text array — one for “Hello ” and one for “world” with bold: true. You can’t do "Hello **world**" as a single string with markdown inside.
Quirk 2: Links go in the text object, not annotations
{
"text": {
"content": "Click here",
"link": { "url": "https://example.com" }
}
}
Not in annotations. Easy to confuse since styling is in annotations but links aren’t.
Quirk 3: 2,000 character limit per rich_text element Notion has a 2,000-character limit per individual text element in the rich_text array. For long content, you need to split it into multiple text objects within the array. OpenClaw handles this automatically, but custom skills need to account for it.
Quirk 4: 100 blocks per append request You can’t add unlimited blocks in one API call — Notion limits to 100 blocks per PATCH. For long documents, you need pagination. OpenClaw chunks large content automatically.
The “Second Brain” Recipe
This is the integration pattern most people actually want: using Notion as OpenClaw’s long-term memory and knowledge base.
Setup
Create a Notion database called “OpenClaw Memory” with these properties:
| Property | Type | Purpose |
|---|---|---|
| Title | Title | What this memory is about |
| Category | Select | Work, Personal, Project, Reference |
| Date | Date | When this was added |
| Source | URL | Where the information came from |
| Tags | Multi-select | For retrieval |
| Summary | Text | Short version for quick reads |
Share this database with your OpenClaw integration.
The Capture Workflow
@openclaw remember that the API rate limit for Notion is 3 requests per second, category: Reference, tags: Notion, API, limits
OpenClaw creates a new page in “OpenClaw Memory” with the content, auto-tags it, and sets the date.
The Retrieval Workflow
@openclaw what do I know about Notion rate limits?
OpenClaw queries the database filtered by tags, retrieves the matching pages, reads the content, and returns a synthesized answer with source links.
Meeting Notes → Notion Pipeline
@openclaw I just finished a meeting about [topic]. Here are the notes: [paste notes]
Save these to Notion, extract action items, create tasks for each one.
OpenClaw:
- Creates a page in “Meeting Notes” database
- Formats the raw notes with headings
- Extracts action items using AI
- Creates corresponding pages in “Tasks” database
- Links the task pages back to the meeting notes page using Relations
This last step — creating Relations between pages — is one of Notion’s more powerful features and worth setting up. A relation property on your Tasks database pointing to Meeting Notes lets you see which meeting generated which task.
Advanced: Wikis and Nested Pages
Notion wikis are just pages with pages inside them. The API treats them the same — there’s no special “wiki” type. You navigate the tree using:
GET /v1/blocks/{page_id}/children
This gives you all direct children of a page. Child pages appear as blocks of type child_page. To get the content of a child page, you recurse — fetch its ID, then fetch its children.
OpenClaw’s Notion skill handles this recursion, but there’s a practical limit: deep nesting (more than 3-4 levels) makes queries slow because each level requires an additional API call, and Notion’s rate limit is 3 requests per second.
For large wikis, the better pattern is to use the Notion search API:
POST /v1/search
{
"query": "your search term",
"filter": { "value": "page", "property": "object" }
}
This is faster than traversing the tree and doesn’t require knowing the page hierarchy.
Handling Notion’s Rate Limits
Notion’s API rate limit is 3 requests per second per integration. For most personal use this isn’t a problem. Where it bites:
- Importing large amounts of content
- Querying large databases (pagination requires multiple requests)
- Building complex pages with many blocks
OpenClaw implements automatic rate limit handling with exponential backoff. If you hit a 429 (rate limited), it waits and retries. You shouldn’t need to handle this manually, but if you’re writing custom skills, build in a 350ms delay between requests to stay under the limit.
Error Reference
| Error | Meaning | Fix |
|---|---|---|
| 404 on valid page ID | Integration not shared with page | Share the page with your integration |
| 400 on property write | Wrong property type in API call | Check database schema with GET /databases/{id} |
| 400 “rich_text exceeds limit” | Text block too long | Split into multiple rich_text elements |
| 429 | Rate limited | Wait and retry; reduce request frequency |
| 403 | Integration lacks capability | Enable capabilities in integration settings |
PaioClaw vs. DIY: Where the Friction Lives
The self-hosted integration above is functional for most use cases. Where it gets complex:
Schema changes: If you update your Notion database schema (rename a property, change a type), your OpenClaw skill breaks silently. Properties that don’t match the schema get ignored — no error, just missing data. PaioClaw’s Notion integration detects schema changes at runtime and adapts.
Multi-workspace setups: If you have work and personal Notion workspaces, you need two separate internal tokens and logic to route requests to the right workspace. PaioClaw handles this with workspace-aware routing.
OAuth for shared integrations: If you want to share an OpenClaw setup with a team and each person uses their own Notion workspace, you need OAuth — which requires hosting a callback URL, handling token refresh, and storing credentials securely. PaioClaw manages OAuth flows out of the box.
For a personal second-brain setup with a single workspace, self-hosted with an internal token works well. PaioClaw becomes worth considering when you hit multi-workspace complexity, need team sharing, or want the schema-change resilience without building it yourself.
PaioClaw starts free, with paid plans from $15/mo (Smart) and $25/mo (Genius).
Summary
The Notion integration has three layers: auth (internal token for personal use), reading (pages via block tree, databases via query API), and writing (properties for structured data, blocks for content). The rich-text JSON model is Notion’s biggest idiosyncrasy — once you internalize it, the rest of the API is logical.
The “second brain” recipe — Notion as OpenClaw’s long-term memory with capture and retrieval workflows — is the highest-value use case for most users. Set that up first, then layer in the meeting notes pipeline and task automation once the foundation is working.

