Notion Integration Skill
Integration patterns for syncing development sessions with Notion databases and pages.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ NOTION INTEGRATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Claude │────►│ REST API │────►│ Notion │ │
│ │ Session │◄────│ Client │◄────│ API │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Session │ │ Databases │ │
│ │ Files │ │ & Pages │ │
│ └─────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
When to Use This Skill
- Setting up Notion integration for the first time
- Syncing development sessions with Notion databases
- Creating database entries from Claude sessions
- Updating page properties and content
- Managing documentation in Notion
Configuration
.claude/config/workspace.json
{
"taskManager": {
"enabled": true,
"provider": "notion",
"syncWithSession": true,
"autoUpdateStatus": true,
"config": {
"integrationToken": "${NOTION_TOKEN}",
"tasksDatabaseId": "your-database-id",
"defaultWorkspace": "your-workspace-id"
}
}
}
Environment Variables
# .env.local
NOTION_TOKEN=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authentication
Integration Token
// Bearer token authentication
const headers = {
'Authorization': `Bearer ${process.env.NOTION_TOKEN}`,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28' // Required API version
};
// Base URL
const BASE_URL = 'https://api.notion.com/v1';
Integration Setup
- Go to https://www.notion.so/my-integrations
- Create new integration
- Copy the "Internal Integration Token"
- Share databases/pages with the integration
API Patterns
Common Endpoints
| Endpoint |
Method |
Description |
/users/me |
GET |
Get bot user |
/users |
GET |
List users |
/databases/{id} |
GET |
Get database |
/databases/{id}/query |
POST |
Query database |
/pages |
POST |
Create page |
/pages/{id} |
GET/PATCH |
Page operations |
/pages/{id}/properties/{id} |
GET |
Get property value |
/blocks/{id} |
GET/PATCH/DELETE |
Block operations |
/blocks/{id}/children |
GET/PATCH |
Block children |
/search |
POST |
Search pages/databases |
Database Entry (Page) Structure
interface NotionPage {
id: string;
object: 'page';
parent: {
type: 'database_id';
database_id: string;
};
properties: {
// Title property (required)
Name: {
title: Array<{
type: 'text';
text: { content: string };
}>;
};
// Select property
Status: {
select: {
name: string;
color: string;
} | null;
};
// Multi-select property
Tags: {
multi_select: Array<{
name: string;
color: string;
}>;
};
// Date property
'Due Date': {
date: {
start: string;
end: string | null;
} | null;
};
// Person property
Assignee: {
people: Array<{
id: string;
name: string;
}>;
};
// Checkbox property
Completed: {
checkbox: boolean;
};
// URL property
URL: {
url: string | null;
};
// Rich text property
Description: {
rich_text: Array<{
type: 'text';
text: { content: string };
}>;
};
};
url: string;
}
Database Operations
Query Database
// Query with filter and sort
const response = await fetch(`${BASE_URL}/databases/${databaseId}/query`, {
method: 'POST',
headers,
body: JSON.stringify({
filter: {
and: [
{
property: 'Status',
select: { equals: 'In Progress' }
},
{
property: 'Assignee',
people: { contains: userId }
}
]
},
sorts: [
{ property: 'Due Date', direction: 'ascending' }
],
page_size: 100
})
});
const { results, has_more, next_cursor } = await response.json();
Create Database Entry
// Create new page in database
const page = await fetch(`${BASE_URL}/pages`, {
method: 'POST',
headers,
body: JSON.stringify({
parent: { database_id: databaseId },
properties: {
Name: {
title: [{ text: { content: 'New Feature: User Auth' } }]
},
Status: {
select: { name: 'In Progress' }
},
Tags: {
multi_select: [
{ name: 'feature' },
{ name: 'backend' }
]
},
'Due Date': {
date: { start: '2024-01-15' }
},
Description: {
rich_text: [{ text: { content: 'Implement OAuth2 support' } }]
}
}
})
});
Update Properties
// Update page properties
await fetch(`${BASE_URL}/pages/${pageId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
properties: {
Status: {
select: { name: 'Done' }
},
Completed: {
checkbox: true
}
}
})
});
Block Operations
Notion pages consist of blocks. Common block types:
Block Types
| Type |
Description |
paragraph |
Text paragraph |
heading_1/2/3 |
Headings |
bulleted_list_item |
Bullet point |
numbered_list_item |
Numbered item |
to_do |
Checkbox item |
toggle |
Collapsible block |
code |
Code block |
callout |
Callout box |
quote |
Quote block |
divider |
Horizontal line |
table |
Table |
table_row |
Table row |
Adding Content to Page
// Append blocks to page
await fetch(`${BASE_URL}/blocks/${pageId}/children`, {
method: 'PATCH',
headers,
body: JSON.stringify({
children: [
{
object: 'block',
type: 'heading_2',
heading_2: {
rich_text: [{ type: 'text', text: { content: 'Session Progress' } }]
}
},
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: 'Phase: Backend Development' } }]
}
},
{
object: 'block',
type: 'to_do',
to_do: {
rich_text: [{ type: 'text', text: { content: 'API endpoints' } }],
checked: true
}
},
{
object: 'block',
type: 'to_do',
to_do: {
rich_text: [{ type: 'text', text: { content: 'Frontend components' } }],
checked: false
}
},
{
object: 'block',
type: 'code',
code: {
rich_text: [{ type: 'text', text: { content: 'npm run test' } }],
language: 'bash'
}
}
]
})
});
Session Sync Patterns
Linking Session to Database Entry
# In requirements.md
## Notion Task
- **Page ID:** abc123-def456-...
- **URL:** https://notion.so/workspace/Task-Name-abc123def456
- **Status:** In Progress
Status Mapping
| Session Event |
Notion Status |
| Session created |
"In Progress" |
| Blocked |
"Blocked" |
| Session closed (success) |
"Done" |
| Session closed (partial) |
"In Review" |
Progress Updates
// Update page with progress
const progressBlocks = [
{
object: 'block',
type: 'divider',
divider: {}
},
{
object: 'block',
type: 'heading_3',
heading_3: {
rich_text: [{
type: 'text',
text: { content: `Update: ${new Date().toISOString().split('T')[0]}` }
}]
}
},
{
object: 'block',
type: 'bulleted_list_item',
bulleted_list_item: {
rich_text: [{ type: 'text', text: { content: 'API endpoints completed' } }]
}
},
{
object: 'block',
type: 'bulleted_list_item',
bulleted_list_item: {
rich_text: [{ type: 'text', text: { content: 'Tests written and passing' } }]
}
}
];
await fetch(`${BASE_URL}/blocks/${pageId}/children`, {
method: 'PATCH',
headers,
body: JSON.stringify({ children: progressBlocks })
});
Rich Text Formatting
Text with Formatting
const richText = [
{ type: 'text', text: { content: 'Normal text, ' } },
{
type: 'text',
text: { content: 'bold text' },
annotations: { bold: true }
},
{ type: 'text', text: { content: ', ' } },
{
type: 'text',
text: { content: 'italic' },
annotations: { italic: true }
},
{ type: 'text', text: { content: ', ' } },
{
type: 'text',
text: { content: 'code' },
annotations: { code: true }
},
{ type: 'text', text: { content: ', and ' } },
{
type: 'text',
text: { content: 'link', link: { url: 'https://example.com' } }
}
];
Available Annotations
interface Annotations {
bold: boolean;
italic: boolean;
strikethrough: boolean;
underline: boolean;
code: boolean;
color: 'default' | 'gray' | 'brown' | 'orange' | 'yellow' |
'green' | 'blue' | 'purple' | 'pink' | 'red' |
'gray_background' | 'brown_background' | /* etc */;
}
Rate Limiting
Limits
| Type |
Limit |
| Requests |
3 requests/second average |
| Burst |
Short bursts allowed |
Handling Rate Limits
// Notion returns 429 with Retry-After header
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || '1';
await sleep(parseInt(retryAfter) * 1000);
// Retry request
}
// Implement exponential backoff
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
const delay = Math.pow(2, i) * 1000;
await sleep(delay);
}
throw new Error('Max retries exceeded');
}
Error Handling
Common Error Codes
| Code |
Description |
Resolution |
| 400 |
Invalid request |
Check body format |
| 401 |
Unauthorized |
Check token |
| 403 |
No access |
Share page with integration |
| 404 |
Not found |
Verify page/database ID |
| 409 |
Conflict |
Transaction conflict, retry |
| 429 |
Rate limited |
Wait and retry |
Error Response Format
{
"object": "error",
"status": 400,
"code": "validation_error",
"message": "body failed validation..."
}
Testing Patterns
Mock API Responses
// __mocks__/notion.ts
export const mockPage = {
id: "abc123-def456",
object: "page",
properties: {
Name: { title: [{ text: { content: "Test Task" } }] },
Status: { select: { name: "In Progress" } }
}
};
export const mockQueryDatabase = jest.fn().mockResolvedValue({
results: [mockPage],
has_more: false
});
Integration Tests
describe('Notion Integration', () => {
it('should query database', async () => {
const results = await notion.queryDatabase(databaseId, {
filter: { property: 'Status', select: { equals: 'In Progress' } }
});
expect(results.length).toBeGreaterThan(0);
});
it('should create page', async () => {
const page = await notion.createPage({
parent: { database_id: databaseId },
properties: {
Name: { title: [{ text: { content: 'Test' } }] }
}
});
expect(page.id).toBeDefined();
});
});
Anti-Patterns
DON'T: Forget Notion-Version Header
// BAD - Missing version header
fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
// GOOD - Include version
fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': '2022-06-28'
}
});
DON'T: Assume Integration Has Access
// BAD - Assume access exists
const page = await notion.getPage(pageId);
// GOOD - Handle access errors
try {
const page = await notion.getPage(pageId);
} catch (error) {
if (error.code === 'object_not_found') {
console.error('Page not shared with integration');
// Guide user to share the page
}
}
DON'T: Send Empty Arrays
// BAD - Empty arrays can cause errors
properties: {
Tags: { multi_select: [] } // Remove tags
}
// GOOD - Omit property or use null where supported
properties: {
// Don't include Tags to keep existing values
}
Checklist
Before using Notion integration:
Related Skills
session-management - Session lifecycle
scheduled-actions - Background task processing
service-layer - API implementation patterns