.claude/skills/notion-integration/SKILL.md
Notion integration patterns for knowledge management. Covers API, databases, pages, and blocks. Use this skill when implementing Notion features or syncing sessions with databases.
npx skillsauth add NextSpark-js/nextspark notion-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Integration patterns for syncing development sessions with Notion databases and pages.
┌─────────────────────────────────────────────────────────────────┐
│ NOTION INTEGRATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Claude │────►│ REST API │────►│ Notion │ │
│ │ Session │◄────│ Client │◄────│ API │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Session │ │ Databases │ │
│ │ Files │ │ & Pages │ │
│ └─────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
{
"taskManager": {
"enabled": true,
"provider": "notion",
"syncWithSession": true,
"autoUpdateStatus": true,
"config": {
"integrationToken": "${NOTION_TOKEN}",
"tasksDatabaseId": "your-database-id",
"defaultWorkspace": "your-workspace-id"
}
}
}
# .env.local
NOTION_TOKEN=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// 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';
| 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 |
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;
}
// 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 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 page properties
await fetch(`${BASE_URL}/pages/${pageId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
properties: {
Status: {
select: { name: 'Done' }
},
Completed: {
checkbox: true
}
}
})
});
Notion pages consist of blocks. Common 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 |
// 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'
}
}
]
})
});
# In requirements.md
## Notion Task
- **Page ID:** abc123-def456-...
- **URL:** https://notion.so/workspace/Task-Name-abc123def456
- **Status:** In Progress
| Session Event | Notion Status | |---------------|---------------| | Session created | "In Progress" | | Blocked | "Blocked" | | Session closed (success) | "Done" | | Session closed (partial) | "In Review" |
// 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 })
});
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' } }
}
];
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 */;
}
| Type | Limit | |------|-------| | Requests | 3 requests/second average | | Burst | Short bursts allowed |
// 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');
}
| 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 |
{
"object": "error",
"status": 400,
"code": "validation_error",
"message": "body failed validation..."
}
// __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
});
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();
});
});
// BAD - Missing version header
fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
// GOOD - Include version
fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': '2022-06-28'
}
});
// 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
}
}
// 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
}
Before using Notion integration:
session-management - Session lifecyclescheduled-actions - Background task processingservice-layer - API implementation patternsdevelopment
Zod validation patterns for this Next.js application. Covers schema definition, API validation, form integration, error formatting, and type inference. Use this skill when implementing validation for APIs, forms, or entity schemas.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Test coverage metrics and registry system for this Next.js application. Covers FEATURE_REGISTRY, FLOW_REGISTRY, TAGS_REGISTRY, and coverage metrics interpretation. Use this skill when evaluating test coverage, identifying gaps, or planning testing priorities.
development
TanStack Query (React Query) patterns for data fetching in this Next.js application. Covers useQuery, useMutation, optimistic updates, cache invalidation, and anti-patterns. Use this skill when implementing data fetching or state management with server data.