Tasks
These are tasks you can execute. Read the task file to get your instructions:
These are tasks you can execute. Read the task file to get your instructions:
Overview
This skill provides instructions for working with Notion operations. It covers creating and managing pages (workspace, database, child), databases with custom properties, content blocks, uploading files and images to pages and databases, managing comments and discussions, and querying or searching content.
Restrictions
parent.workspace: true - only pages can be created as workspace pagesDifferent Notion views require specific properties to function. Entries without the required property populated will NOT appear in that view.
| View Type | Required Property | Notes |
|---|---|---|
| Calendar | date property |
Entry MUST have the date property populated to appear on calendar |
| Timeline | date property (with optional end) |
For date ranges, use start and end |
| Board | select or status property |
Used for column grouping |
| Gallery | None required | Any property can be shown |
| Table/List | None required | All properties visible |
Calendar View Gotcha: If a database has multiple date properties, the user configured which one powers "Show calendar by" in the Notion UI. The API doesn't expose this setting. When adding calendar entries:
start (required); add end for date ranges (optional for single-day events)Date property format:
properties: {
"Date": { // or whatever the calendar's date property is named
date: {
start: "2026-01-15", // Required - ISO 8601 date
end: "2026-01-16", // Optional - for multi-day events
time_zone: null // Optional - IANA timezone string
}
}
}For datetime (with time):
properties: {
"Event Time": {
date: {
start: "2026-01-15T14:00:00", // Date with time
end: "2026-01-15T15:30:00" // End time
}
}
}Board View: Entries without the grouping property (status/select) will appear in "No Status" column.
Operations
Create a new page as an entry in a Notion database.
When to use:
Example: Create database page with properties
const newPage = {
parent: {
database_id: "DATABASE_ID"
},
properties: {
"Name": {
title: [
{
text: {
content: "New Task Title"
}
}
]
},
"Status": {
select: {
name: "In Progress"
}
},
"Priority": {
select: {
name: "High"
}
},
"Due Date": {
date: {
start: "2025-12-31"
}
}
},
children: [
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is the description of the task."
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newPage)
});
const result = await response.json();Critical steps:
parent.database_idNotion-Version headerchildren arrayCreate a new standalone page in the workspace (not under any parent page or database). This operation is for pages only - databases cannot be created as workspace pages.
When to use:
Example: Create workspace page
const workspacePage = {
parent: {
workspace: true
},
properties: {
"title": {
title: [
{
text: {
content: "My standalone page"
}
}
]
}
},
children: [
{
object: "block",
type: "heading_1",
heading_1: {
rich_text: [
{
text: {
content: "Welcome to my page"
}
}
]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is a standalone page in the workspace."
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(workspacePage)
});
const result = await response.json();Critical steps:
parent.workspace to true for workspace pages (only works for pages, not databases)children arrayNotion-Version headerCreate a new page as a child of an existing page.
When to use:
Example: Create child page with content
const childPage = {
parent: {
page_id: "PARENT_PAGE_ID"
},
properties: {
"title": {
title: [
{
text: {
content: "New Sub-page Title"
}
}
]
}
},
children: [
{
object: "block",
type: "heading_1",
heading_1: {
rich_text: [
{
text: {
content: "Main Heading"
}
}
]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is a paragraph with "
}
},
{
text: {
content: "bold text",
link: null
},
annotations: {
bold: true
}
},
{
text: {
content: " and formatting."
}
}
]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [
{
text: {
content: "First bullet point"
}
}
]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [
{
text: {
content: "Second bullet point"
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(childPage)
});
const result = await response.json();Critical steps:
parent.page_idchildren array with block objectsobject: "block" and a typerich_text array within the block typeCreate a new database as a child of an existing page. Databases cannot be created as workspace pages (parent.workspace: true is not supported for databases).
When to use:
Example: Create database with multiple property types
const newDatabase = {
parent: {
page_id: "PARENT_PAGE_ID"
},
title: [
{
text: {
content: "Project Tasks"
}
}
],
properties: {
"Name": {
title: {}
},
"Status": {
select: {
options: [
{ name: "Not Started", color: "gray" },
{ name: "In Progress", color: "blue" },
{ name: "Completed", color: "green" },
{ name: "Blocked", color: "red" }
]
}
},
"Priority": {
select: {
options: [
{ name: "Low", color: "gray" },
{ name: "Medium", color: "yellow" },
{ name: "High", color: "orange" },
{ name: "Urgent", color: "red" }
]
}
},
"Assignee": {
people: {}
},
"Due Date": {
date: {}
},
"Tags": {
multi_select: {
options: [
{ name: "Frontend", color: "blue" },
{ name: "Backend", color: "purple" },
{ name: "Design", color: "pink" },
{ name: "Bug", color: "red" }
]
}
},
"Progress": {
number: {
format: "percent"
}
},
"Notes": {
rich_text: {}
},
"Completed": {
checkbox: {}
},
"URL": {
url: {}
}
}
};
const response = await fetch('https://api.notion.com/v1/databases', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newDatabase)
});
const result = await response.json();Available property types:
title - Title field (required, one per database)rich_text - Text contentnumber - Numbers (with optional format: number, number_with_commas, percent, dollar, etc.)select - Single select dropdownmulti_select - Multiple select tagsdate - Date or date rangepeople - Person/people selectorfiles - File attachmentscheckbox - Checkboxurl - URL linksemail - Email addressesphone_number - Phone numbersformula - Formulasrelation - Relations to other databasesrollup - Rollup from relationscreated_time - Creation timestampcreated_by - Creatorlast_edited_time - Last edit timestamplast_edited_by - Last editorCritical steps:
parent.workspace: truetitle property type (required)properties objectoptions array with names and colorstitle array (not in properties)Common block types that can be added to pages.
Available block types:
paragraph - Regular text paragraphsheading_1, heading_2, heading_3 - Headingsbulleted_list_item - Bullet pointsnumbered_list_item - Numbered liststo_do - Checkboxestoggle - Toggle listscode - Code blocksquote - Quote blocksdivider - Horizontal dividerscallout - Callout boxesExample block formats:
// Paragraph block
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "Your text here"
}
}
]
}
}
// To-do block
{
object: "block",
type: "to_do",
to_do: {
rich_text: [
{
text: {
content: "Task to complete"
}
}
],
checked: false
}
}
// Code block
{
object: "block",
type: "code",
code: {
rich_text: [
{
text: {
content: "console.log('Hello World');"
}
}
],
language: "javascript"
}
}
// Callout block
{
object: "block",
type: "callout",
callout: {
rich_text: [
{
text: {
content: "Important note here"
}
}
],
icon: {
emoji: "💡"
}
}
}Update existing page or database entry properties using PATCH.
When to use:
Example: Update database entry properties
const pageId = "PAGE_ID";
const updates = {
properties: {
"Status": {
status: {
name: "Completed"
}
},
"Due Date": {
date: {
start: "2026-01-20"
}
},
"Priority": {
select: {
name: "High"
}
}
}
};
const response = await fetch(`https://api.notion.com/v1/pages/${pageId}`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(updates)
});
const result = await response.json();Critical steps:
Add new content blocks to an existing page.
When to use:
Example: Append blocks to page
const pageId = "PAGE_ID";
const newBlocks = {
children: [
{
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{
text: { content: "New Section" }
}]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
text: { content: "Additional notes added to this page." }
}]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{
text: { content: "First new bullet point" }
}]
}
}
]
};
const response = await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newBlocks)
});
const result = await response.json();Critical steps:
object: "block" and typechildren array in the blockUpload files, images, videos, and PDFs to Notion pages and databases.
When the user needs to upload a local file/External URL containing a file to a Notion page or database, read Upload Files to Notion to get complete information on using the
uploadFileToNotion() function.
The upload process:
type: "file_upload"Retrieve all pages from a Notion database using cursor-based pagination.
When to use:
Pagination details:
start_cursor from response to get next pagehas_more is falseExample: Query all database pages
const databaseId = "DATABASE_ID";
let allPages = [];
let startCursor = undefined;
do {
const requestBody = {
page_size: 100
};
if (startCursor) {
requestBody.start_cursor = startCursor;
}
const response = await fetch(
`https://api.notion.com/v1/databases/${databaseId}/query`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(requestBody)
}
);
const data = await response.json();
allPages = allPages.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);Critical steps:
start_cursor)next_cursor from responsenext_cursor is nullpage_size parameter to control page size (max 100)Filter database entries by property values. The filter syntax depends on the property type—status and select look similar but use different filter keys.
When to use:
Important: The filter key must match the property type exactly. A common error is using select filters on status properties (or vice versa), which returns a 400 error. Always check the property type in database metadata first.
Filter by status property:
// status properties use { status: { equals: "value" } }
const requestBody = {
filter: {
property: "Status",
status: {
equals: "In Progress"
}
}
};Filter by select property:
// select properties use { select: { equals: "value" } }
const requestBody = {
filter: {
property: "Priority",
select: {
equals: "High"
}
}
};Filter by date property:
// Date filters: equals, before, after, on_or_before, on_or_after, is_empty, is_not_empty
const requestBody = {
filter: {
property: "Due Date",
date: {
on_or_before: "2025-12-31"
}
}
};
// For "this week" style filters, calculate the date range
const today = new Date();
const weekEnd = new Date(today);
weekEnd.setDate(today.getDate() + (7 - today.getDay()));
const thisWeekFilter = {
filter: {
property: "Due Date",
date: {
on_or_before: weekEnd.toISOString().split('T')[0]
}
}
};Filter by checkbox property:
const requestBody = {
filter: {
property: "Completed",
checkbox: {
equals: false // or true
}
}
};Filter by multi_select property:
// Check if multi_select contains a specific tag
const requestBody = {
filter: {
property: "Tags",
multi_select: {
contains: "Frontend"
}
}
};Filter by people property:
const requestBody = {
filter: {
property: "Assignee",
people: {
contains: "USER_ID"
}
}
};Combine multiple filters (AND):
const requestBody = {
filter: {
and: [
{
property: "Status",
status: {
equals: "In Progress"
}
},
{
property: "Priority",
select: {
equals: "High"
}
}
]
}
};Combine multiple filters (OR):
const requestBody = {
filter: {
or: [
{
property: "Status",
status: {
equals: "Not Started"
}
},
{
property: "Status",
status: {
equals: "In Progress"
}
}
]
}
};Available filter conditions by property type:
| Property Type | Filter Conditions |
|---|---|
status |
equals, does_not_equal, is_empty, is_not_empty |
select |
equals, does_not_equal, is_empty, is_not_empty |
multi_select |
contains, does_not_contain, is_empty, is_not_empty |
date |
equals, before, after, on_or_before, on_or_after, is_empty, is_not_empty |
checkbox |
equals |
people |
contains, does_not_contain, is_empty, is_not_empty |
rich_text |
equals, does_not_equal, contains, does_not_contain, starts_with, ends_with, is_empty, is_not_empty |
number |
equals, does_not_equal, greater_than, less_than, greater_than_or_equal_to, less_than_or_equal_to, is_empty, is_not_empty |
Critical steps:
status key for status properties, select key for select propertiesand/or arrays for complex queriesis_empty/is_not_empty to filter by whether property has a valueRetrieve a database's schema to discover property names and types. Essential before writing filters—you need to know if a property is status or select type.
When to use:
Example: Get database schema
const databaseId = "DATABASE_ID";
const response = await fetch(
`https://api.notion.com/v1/databases/${databaseId}`,
{
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
}
);
const database = await response.json();
// Log property names and types
for (const [name, config] of Object.entries(database.properties)) {
console.log(`${name}: ${config.type}`);
}
// Output example:
// Name: title
// Status: status <-- use { status: {...} } in filters
// Priority: select <-- use { select: {...} } in filters
// Due Date: date
// Tags: multi_select
// Assignee: peopleReading property schema:
The response includes full property configuration. Key fields:
// Example property from response
{
"Status": {
"id": "abc123",
"name": "Status",
"type": "status", // <-- This tells you the filter key to use
"status": {
"options": [
{ "id": "...", "name": "Not Started", "color": "default" },
{ "id": "...", "name": "In Progress", "color": "blue" },
{ "id": "...", "name": "Done", "color": "green" }
],
"groups": [...]
}
}
}Critical steps:
type field tells you which filter key to use (status, select, date, etc.)select/multi_select/status, the options array shows valid valuesRetrieve all child blocks from a page or block.
When to use:
Pagination details:
start_cursor from response to get next pagehas_more is falseExample: Fetch all blocks from a page
const pageId = "PAGE_ID";
let allBlocks = [];
let startCursor = undefined;
do {
const url = startCursor
? `https://api.notion.com/v1/blocks/${pageId}/children?page_size=100&start_cursor=${startCursor}`
: `https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
});
const data = await response.json();
allBlocks = allBlocks.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);Critical steps:
next_cursor from responsenext_cursor is nullpage_size query parameter to control page size (max 100)Search all parent or child pages and databases that have been shared with an integration.
When to use:
Request parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | No | Text to search for in page titles. If omitted, returns all accessible pages |
filter |
object | No | Limit search to specific object types |
filter.value |
string | No | Either "page" or "database" |
filter.property |
string | No | Must be "object" when using filter |
sort |
object | No | Sort order for results |
sort.direction |
string | No | Either "ascending" or "descending" |
sort.timestamp |
string | No | Either "last_edited_time" |
page_size |
number | No | Number of results per page (max 100, default 100) |
start_cursor |
string | No | Cursor for pagination |
Example
async function searchAllPages(query) {
let allResults = [];
let startCursor = undefined;
do {
const requestBody = {
query: query,
page_size: 100
};
if (startCursor) {
requestBody.start_cursor = startCursor;
}
const response = await fetch('https://api.notion.com/v1/search', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(requestBody)
});
const data = await response.json();
allResults = allResults.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
return allResults;
}
// Usage
const pages = await searchAllPages("Meeting Notes");Critical steps:
query parameter to filter by title textfilter object to limit results to pages or databases onlysort parameter if neededSearch optimizations and limitations:
Add and retrieve comments on pages and blocks in Notion.
When to use:
Comment limitations:
Add a comment to a page or block.
Example: Add comment to a page
const newComment = {
parent: {
page_id: "PAGE_ID"
},
rich_text: [
{
text: {
content: "This looks great! Just a few suggestions for improvement."
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newComment)
});
const result = await response.json();
// result includes discussion_id for threadingExample: Add comment with mentions and formatting
const commentWithMentions = {
parent: {
page_id: "PAGE_ID"
},
rich_text: [
{
text: {
content: "Hey "
}
},
{
type: "mention",
mention: {
type: "user",
user: {
id: "USER_ID"
}
}
},
{
text: {
content: ", can you review this section? "
}
},
{
text: {
content: "It's urgent!",
link: null
},
annotations: {
bold: true,
color: "red"
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(commentWithMentions)
});
const result = await response.json();Example: Reply to existing comment using discussion_id
const reply = {
discussion_id: "DISCUSSION_ID",
rich_text: [
{
text: {
content: "Thanks for the feedback! I've made the changes."
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(reply)
});
const result = await response.json();Critical steps:
parent.page_id to comment on a page (creates new discussion thread)discussion_id to reply to an existing comment threadparent and discussion_id in the same requestdiscussion_id for future repliesRetrieve all comments from a page or block.
Example: Get all comments with pagination
async function getAllComments(blockId) {
let allComments = [];
let startCursor = undefined;
do {
const url = new URL('https://api.notion.com/v1/comments');
url.searchParams.append('block_id', blockId);
url.searchParams.append('page_size', '100');
if (startCursor) {
url.searchParams.append('start_cursor', startCursor);
}
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
});
const data = await response.json();
allComments = allComments.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
return allComments;
}
// Usage
const comments = await getAllComments("PAGE_ID");Critical steps:
block_id query parameter to filter by page or blockstart_cursor and page_sizediscussion_id for threadingCommon errors and how to resolve them.
| Error Code | Meaning | Solution |
|---|---|---|
| 400 | Bad request - invalid parameters | Check filter syntax matches property type (e.g., status vs select). Verify property names are exact matches. |
| 401 | Unauthorized | Token is invalid or expired. Verify NOTION_INTEGRATION_TOKEN is correct. |
| 403 | Forbidden | Integration doesn't have access to this resource. Share the page/database with the integration in Notion. |
| 404 | Not found | Page or database doesn't exist or isn't shared with the integration. |
| 409 | Conflict | Transaction conflict. Retry the request. |
| 429 | Rate limited | Too many requests. Wait and retry with exponential backoff. |
| 502/503 | Server error | Notion service issue. Retry after a short delay. |
Common 400 error causes:
// ERROR: Using select filter on a status property
filter: { property: "Status", select: { equals: "Done" } } // 400 error!
// CORRECT: Use status filter for status properties
filter: { property: "Status", status: { equals: "Done" } }// ERROR: Property name doesn't exist or wrong case
filter: { property: "status", ... } // 400 if property is "Status"
// CORRECT: Use exact property name from database metadata
filter: { property: "Status", ... }// ERROR: Option doesn't exist in database
filter: { property: "Priority", select: { equals: "Critical" } } // 400 if no "Critical" option
// CORRECT: Use exact option name from database metadata
filter: { property: "Priority", select: { equals: "High" } }Handling rate limits (429):
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt);
console.log(`Rate limited. Waiting ${retryAfter}s before retry...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}Workflows
This skill depends on the following skills. Use these if needed.
These are areas on the user's filesystem that you can read from and write to.
---
name: "Notion Writer"
description: "User wants to write to Notion—create pages, add database entries, set up new databases, or update existing content.
Triggers: \"create a page in notion\", \"add to notion\", \"new notion page\",
\"add entry to database\", \"write to notion\", \"update that page\",
\"change the status\", \"edit the entry\", \"create a database\", \"make a tracker\"
"
requiredApps: [notion]
---
Write to Notion without switching apps. Create pages anywhere in your workspace, add entries to databases, set up new databases for tracking, or update existing content.
For new content: Tell me what to create and where—I'll structure it with proper Notion formatting. For database entries, I'll match your existing properties and ensure view-critical fields (dates, status) are populated correctly.
For updates: Reference any page or entry and tell me what to change—properties, status, dates, or add new content blocks.
For new databases: Describe what you want to track and I'll set up a database with appropriate properties, ready for Calendar, Board, or Table views.
**Limitations:** Pages must be shared with the Notion integration. Cannot create databases as workspace pages (Notion API limitation). Deletion operations require a separate skill.
# Notion
**Overview**
This skill provides instructions for working with Notion operations. It covers creating and managing pages (workspace, database, child), databases with custom properties, content blocks, uploading files and images to pages and databases, managing comments and discussions, and querying or searching content.
**Restrictions**
- Maximum request size is 1000 blocks per request
- Databases cannot be created with `parent.workspace: true` - only pages can be created as workspace pages
- Comments cannot be updated nor deleted. Only created.
- File uploads have size limits based on workspace plan (free plans have a limit of 5 MiB per file and Paid plans have a limit of 5 GiB per file)
- Maximum filename length: 900 bytes (recommended: shorter names)
- Rate limit: ~3 requests/second average. Handle 429 errors with exponential backoff. See Troubleshooting section for retry pattern.
### View-Type Property Requirements
Different Notion views require specific properties to function. **Entries without the required property populated will NOT appear in that view.**
| View Type | Required Property | Notes |
|-----------|------------------|-------|
| Calendar | `date` property | Entry MUST have the date property populated to appear on calendar |
| Timeline | `date` property (with optional end) | For date ranges, use `start` and `end` |
| Board | `select` or `status` property | Used for column grouping |
| Gallery | None required | Any property can be shown |
| Table/List | None required | All properties visible |
**Calendar View Gotcha**: If a database has multiple date properties, the user configured which one powers "Show calendar by" in the Notion UI. The API doesn't expose this setting. When adding calendar entries:
1. Check if database has a date property named "Date", "Due Date", "Event Date", or similar common names
2. If multiple date properties exist, ask the user which one to populate
3. Always include `start` (required); add `end` for date ranges (optional for single-day events)
**Date property format:**
```javascript
properties: {
"Date": { // or whatever the calendar's date property is named
date: {
start: "2026-01-15", // Required - ISO 8601 date
end: "2026-01-16", // Optional - for multi-day events
time_zone: null // Optional - IANA timezone string
}
}
}
```
**For datetime (with time):**
```javascript
properties: {
"Event Time": {
date: {
start: "2026-01-15T14:00:00", // Date with time
end: "2026-01-15T15:30:00" // End time
}
}
}
```
**Board View**: Entries without the grouping property (status/select) will appear in "No Status" column.
**Operations**
### Create Page in Database
Create a new page as an entry in a Notion database.
**When to use:**
- Adding entries to a Notion database
- Creating structured data with properties
- Logging information to a database table
**Example: Create database page with properties**
```javascript
const newPage = {
parent: {
database_id: "DATABASE_ID"
},
properties: {
"Name": {
title: [
{
text: {
content: "New Task Title"
}
}
]
},
"Status": {
select: {
name: "In Progress"
}
},
"Priority": {
select: {
name: "High"
}
},
"Due Date": {
date: {
start: "2025-12-31"
}
}
},
children: [
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is the description of the task."
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newPage)
});
const result = await response.json();
```
**Critical steps:**
1. Specify database ID in `parent.database_id`
2. Match property names exactly as they appear in the database
3. Use correct property types (title, select, date, etc.)
4. Include `Notion-Version` header
5. Optionally add content blocks in `children` array
### Create Workspace Page
Create a new standalone page in the workspace (not under any parent page or database). This operation is for pages only - databases cannot be created as workspace pages.
**When to use:**
- Creating top-level pages in workspace
- Adding standalone documents
- Creating pages that aren't part of a database or hierarchy
**Example: Create workspace page**
```javascript
const workspacePage = {
parent: {
workspace: true
},
properties: {
"title": {
title: [
{
text: {
content: "My standalone page"
}
}
]
}
},
children: [
{
object: "block",
type: "heading_1",
heading_1: {
rich_text: [
{
text: {
content: "Welcome to my page"
}
}
]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is a standalone page in the workspace."
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(workspacePage)
});
const result = await response.json();
```
**Critical steps:**
1. Set `parent.workspace` to `true` for workspace pages (only works for pages, not databases)
2. Use "title" property (lowercase) for the page title
3. Title property is required and must be a title type
4. Optionally add content blocks in `children` array
5. Include `Notion-Version` header
### Create Child Page
Create a new page as a child of an existing page.
**When to use:**
- Creating sub-pages under existing pages
- Building hierarchical page structures
- Adding pages to a specific location in workspace
**Example: Create child page with content**
```javascript
const childPage = {
parent: {
page_id: "PARENT_PAGE_ID"
},
properties: {
"title": {
title: [
{
text: {
content: "New Sub-page Title"
}
}
]
}
},
children: [
{
object: "block",
type: "heading_1",
heading_1: {
rich_text: [
{
text: {
content: "Main Heading"
}
}
]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "This is a paragraph with "
}
},
{
text: {
content: "bold text",
link: null
},
annotations: {
bold: true
}
},
{
text: {
content: " and formatting."
}
}
]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [
{
text: {
content: "First bullet point"
}
}
]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [
{
text: {
content: "Second bullet point"
}
}
]
}
}
]
};
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(childPage)
});
const result = await response.json();
```
**Critical steps:**
1. Specify parent page ID in `parent.page_id`
2. Use "title" property (lowercase) for child pages
3. Add content using `children` array with block objects
4. Each block must have `object: "block"` and a `type`
5. Content goes in `rich_text` array within the block type
### Create Database
Create a new database as a child of an existing page. Databases cannot be created as workspace pages (`parent.workspace: true` is not supported for databases).
**When to use:**
- Creating structured data tables
- Building project trackers, task lists, or content calendars
- Setting up databases with custom properties
**Example: Create database with multiple property types**
```javascript
const newDatabase = {
parent: {
page_id: "PARENT_PAGE_ID"
},
title: [
{
text: {
content: "Project Tasks"
}
}
],
properties: {
"Name": {
title: {}
},
"Status": {
select: {
options: [
{ name: "Not Started", color: "gray" },
{ name: "In Progress", color: "blue" },
{ name: "Completed", color: "green" },
{ name: "Blocked", color: "red" }
]
}
},
"Priority": {
select: {
options: [
{ name: "Low", color: "gray" },
{ name: "Medium", color: "yellow" },
{ name: "High", color: "orange" },
{ name: "Urgent", color: "red" }
]
}
},
"Assignee": {
people: {}
},
"Due Date": {
date: {}
},
"Tags": {
multi_select: {
options: [
{ name: "Frontend", color: "blue" },
{ name: "Backend", color: "purple" },
{ name: "Design", color: "pink" },
{ name: "Bug", color: "red" }
]
}
},
"Progress": {
number: {
format: "percent"
}
},
"Notes": {
rich_text: {}
},
"Completed": {
checkbox: {}
},
"URL": {
url: {}
}
}
};
const response = await fetch('https://api.notion.com/v1/databases', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newDatabase)
});
const result = await response.json();
```
**Available property types:**
- `title` - Title field (required, one per database)
- `rich_text` - Text content
- `number` - Numbers (with optional format: number, number_with_commas, percent, dollar, etc.)
- `select` - Single select dropdown
- `multi_select` - Multiple select tags
- `date` - Date or date range
- `people` - Person/people selector
- `files` - File attachments
- `checkbox` - Checkbox
- `url` - URL links
- `email` - Email addresses
- `phone_number` - Phone numbers
- `formula` - Formulas
- `relation` - Relations to other databases
- `rollup` - Rollup from relations
- `created_time` - Creation timestamp
- `created_by` - Creator
- `last_edited_time` - Last edit timestamp
- `last_edited_by` - Last editor
**Critical steps:**
1. Database must have a parent page ID - cannot use `parent.workspace: true`
2. Include a `title` property type (required)
3. Define property schema in `properties` object
4. For select/multi_select, define `options` array with names and colors
5. Available colors: default, gray, brown, orange, yellow, green, blue, purple, pink, red
6. Database title goes in top-level `title` array (not in properties)
### Add Content Blocks
Common block types that can be added to pages.
**Available block types:**
- `paragraph` - Regular text paragraphs
- `heading_1`, `heading_2`, `heading_3` - Headings
- `bulleted_list_item` - Bullet points
- `numbered_list_item` - Numbered lists
- `to_do` - Checkboxes
- `toggle` - Toggle lists
- `code` - Code blocks
- `quote` - Quote blocks
- `divider` - Horizontal dividers
- `callout` - Callout boxes
**Example block formats:**
```javascript
// Paragraph block
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
text: {
content: "Your text here"
}
}
]
}
}
// To-do block
{
object: "block",
type: "to_do",
to_do: {
rich_text: [
{
text: {
content: "Task to complete"
}
}
],
checked: false
}
}
// Code block
{
object: "block",
type: "code",
code: {
rich_text: [
{
text: {
content: "console.log('Hello World');"
}
}
],
language: "javascript"
}
}
// Callout block
{
object: "block",
type: "callout",
callout: {
rich_text: [
{
text: {
content: "Important note here"
}
}
],
icon: {
emoji: "💡"
}
}
}
```
### Update Page Properties
Update existing page or database entry properties using PATCH.
**When to use:**
- Changing property values (status, date, tags, etc.)
- Updating database entry fields
- Modifying page metadata
**Example: Update database entry properties**
```javascript
const pageId = "PAGE_ID";
const updates = {
properties: {
"Status": {
status: {
name: "Completed"
}
},
"Due Date": {
date: {
start: "2026-01-20"
}
},
"Priority": {
select: {
name: "High"
}
}
}
};
const response = await fetch(`https://api.notion.com/v1/pages/${pageId}`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(updates)
});
const result = await response.json();
```
**Critical steps:**
1. Use PATCH method (not POST)
2. Only include properties you want to update (omitted properties remain unchanged)
3. Match property names exactly (case-sensitive)
4. Use correct property types (status vs select—check database schema)
5. For view-critical properties (date, status), ensure they're populated correctly for the entry to appear in Calendar/Board views
### Append Blocks to Page
Add new content blocks to an existing page.
**When to use:**
- Adding notes to existing pages
- Appending content to database entries
- Building up page content incrementally
**Example: Append blocks to page**
```javascript
const pageId = "PAGE_ID";
const newBlocks = {
children: [
{
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{
text: { content: "New Section" }
}]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
text: { content: "Additional notes added to this page." }
}]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{
text: { content: "First new bullet point" }
}]
}
}
]
};
const response = await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newBlocks)
});
const result = await response.json();
```
**Critical steps:**
1. Use PATCH /blocks/{page_id}/children endpoint
2. Blocks are appended to the end of the page
3. Maximum 100 blocks per request
4. Each block must have `object: "block"` and `type`
5. For nested blocks (toggles with children), include `children` array in the block
### Upload Files
Upload files, images, videos, and PDFs to Notion pages and databases.
When the user needs to upload a local file/External URL containing a file to a Notion page or database, read `skills/sauna/notion.page.writer/references/content.notion.upload.md` to get complete information on using the `uploadFileToNotion()` function.
The upload process:
1. Create file upload (POST /file_uploads) - returns fileUploadId
2. Send file contents (POST /file_uploads/{id}/send)
- For files ≤20MB: file auto-transitions to uploaded status
- For files >20MB: send in parts, then call POST /file_uploads/{id}/complete
3. Attach to page/block using fileUploadId with `type: "file_upload"`
### Query Database with Pagination
Retrieve all pages from a Notion database using cursor-based pagination.
**When to use:**
- Fetching entries from a database
- Need to process all database records
**Pagination details:**
- Default page size: 100 pages per request
- Maximum page size: 100 pages per request
- Use `start_cursor` from response to get next page
- Continue until `has_more` is `false`
**Example: Query all database pages**
```javascript
const databaseId = "DATABASE_ID";
let allPages = [];
let startCursor = undefined;
do {
const requestBody = {
page_size: 100
};
if (startCursor) {
requestBody.start_cursor = startCursor;
}
const response = await fetch(
`https://api.notion.com/v1/databases/${databaseId}/query`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(requestBody)
}
);
const data = await response.json();
allPages = allPages.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
```
**Critical steps:**
1. Start with initial request (no `start_cursor`)
2. Append results to collection
3. Extract `next_cursor` from response
4. Continue until `next_cursor` is `null`
5. Use `page_size` parameter to control page size (max 100)
### Query with Filters
Filter database entries by property values. The filter syntax depends on the property type—`status` and `select` look similar but use different filter keys.
**When to use:**
- Finding entries matching specific criteria
- Filtering by status, date, tags, or other properties
- Combining multiple filter conditions
**Important:** The filter key must match the property type exactly. A common error is using `select` filters on `status` properties (or vice versa), which returns a 400 error. Always check the property type in database metadata first.
**Filter by status property:**
```javascript
// status properties use { status: { equals: "value" } }
const requestBody = {
filter: {
property: "Status",
status: {
equals: "In Progress"
}
}
};
```
**Filter by select property:**
```javascript
// select properties use { select: { equals: "value" } }
const requestBody = {
filter: {
property: "Priority",
select: {
equals: "High"
}
}
};
```
**Filter by date property:**
```javascript
// Date filters: equals, before, after, on_or_before, on_or_after, is_empty, is_not_empty
const requestBody = {
filter: {
property: "Due Date",
date: {
on_or_before: "2025-12-31"
}
}
};
// For "this week" style filters, calculate the date range
const today = new Date();
const weekEnd = new Date(today);
weekEnd.setDate(today.getDate() + (7 - today.getDay()));
const thisWeekFilter = {
filter: {
property: "Due Date",
date: {
on_or_before: weekEnd.toISOString().split('T')[0]
}
}
};
```
**Filter by checkbox property:**
```javascript
const requestBody = {
filter: {
property: "Completed",
checkbox: {
equals: false // or true
}
}
};
```
**Filter by multi_select property:**
```javascript
// Check if multi_select contains a specific tag
const requestBody = {
filter: {
property: "Tags",
multi_select: {
contains: "Frontend"
}
}
};
```
**Filter by people property:**
```javascript
const requestBody = {
filter: {
property: "Assignee",
people: {
contains: "USER_ID"
}
}
};
```
**Combine multiple filters (AND):**
```javascript
const requestBody = {
filter: {
and: [
{
property: "Status",
status: {
equals: "In Progress"
}
},
{
property: "Priority",
select: {
equals: "High"
}
}
]
}
};
```
**Combine multiple filters (OR):**
```javascript
const requestBody = {
filter: {
or: [
{
property: "Status",
status: {
equals: "Not Started"
}
},
{
property: "Status",
status: {
equals: "In Progress"
}
}
]
}
};
```
**Available filter conditions by property type:**
| Property Type | Filter Conditions |
|---------------|-------------------|
| `status` | `equals`, `does_not_equal`, `is_empty`, `is_not_empty` |
| `select` | `equals`, `does_not_equal`, `is_empty`, `is_not_empty` |
| `multi_select` | `contains`, `does_not_contain`, `is_empty`, `is_not_empty` |
| `date` | `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, `is_not_empty` |
| `checkbox` | `equals` |
| `people` | `contains`, `does_not_contain`, `is_empty`, `is_not_empty` |
| `rich_text` | `equals`, `does_not_equal`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` |
| `number` | `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` |
**Critical steps:**
1. Check property type in database metadata before writing filters
2. Use `status` key for status properties, `select` key for select properties
3. Combine filters with `and`/`or` arrays for complex queries
4. Use `is_empty`/`is_not_empty` to filter by whether property has a value
### Get Database Metadata
Retrieve a database's schema to discover property names and types. Essential before writing filters—you need to know if a property is `status` or `select` type.
**When to use:**
- Before writing filters (to get correct property types)
- Discovering available properties in a database
- Understanding database structure
**Example: Get database schema**
```javascript
const databaseId = "DATABASE_ID";
const response = await fetch(
`https://api.notion.com/v1/databases/${databaseId}`,
{
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
}
);
const database = await response.json();
// Log property names and types
for (const [name, config] of Object.entries(database.properties)) {
console.log(`${name}: ${config.type}`);
}
// Output example:
// Name: title
// Status: status <-- use { status: {...} } in filters
// Priority: select <-- use { select: {...} } in filters
// Due Date: date
// Tags: multi_select
// Assignee: people
```
**Reading property schema:**
The response includes full property configuration. Key fields:
```javascript
// Example property from response
{
"Status": {
"id": "abc123",
"name": "Status",
"type": "status", // <-- This tells you the filter key to use
"status": {
"options": [
{ "id": "...", "name": "Not Started", "color": "default" },
{ "id": "...", "name": "In Progress", "color": "blue" },
{ "id": "...", "name": "Done", "color": "green" }
],
"groups": [...]
}
}
}
```
**Critical steps:**
1. The `type` field tells you which filter key to use (`status`, `select`, `date`, etc.)
2. For `select`/`multi_select`/`status`, the `options` array shows valid values
3. Property names are case-sensitive—use exact names from metadata
4. Cache metadata to avoid repeated calls when querying the same database
### List Block Children with Pagination
Retrieve all child blocks from a page or block.
**When to use:**
- Reading content from existing pages
- Need to process all blocks in a page
**Pagination details:**
- Default page size: 100 blocks per request
- Maximum page size: 100 blocks per request
- Use `start_cursor` from response to get next page
- Continue until `has_more` is `false`
**Example: Fetch all blocks from a page**
```javascript
const pageId = "PAGE_ID";
let allBlocks = [];
let startCursor = undefined;
do {
const url = startCursor
? `https://api.notion.com/v1/blocks/${pageId}/children?page_size=100&start_cursor=${startCursor}`
: `https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
});
const data = await response.json();
allBlocks = allBlocks.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
```
**Critical steps:**
1. Start with initial request (no cursor)
2. Append results to collection
3. Extract `next_cursor` from response
4. Continue until `next_cursor` is `null`
5. Use `page_size` query parameter to control page size (max 100)
### Search by Title
Search all parent or child pages and databases that have been shared with an integration.
**When to use:**
- Finding pages by title across the workspace
- Searching for specific content shared with the integration
- Filtering search results by object type (page or database)
**Request parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | No | Text to search for in page titles. If omitted, returns all accessible pages |
| `filter` | object | No | Limit search to specific object types |
| `filter.value` | string | No | Either "page" or "database" |
| `filter.property` | string | No | Must be "object" when using filter |
| `sort` | object | No | Sort order for results |
| `sort.direction` | string | No | Either "ascending" or "descending" |
| `sort.timestamp` | string | No | Either "last_edited_time" |
| `page_size` | number | No | Number of results per page (max 100, default 100) |
| `start_cursor` | string | No | Cursor for pagination |
**Example**
```javascript
async function searchAllPages(query) {
let allResults = [];
let startCursor = undefined;
do {
const requestBody = {
query: query,
page_size: 100
};
if (startCursor) {
requestBody.start_cursor = startCursor;
}
const response = await fetch('https://api.notion.com/v1/search', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(requestBody)
});
const data = await response.json();
allResults = allResults.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
return allResults;
}
// Usage
const pages = await searchAllPages("Meeting Notes");
```
**Critical steps:**
1. Provide search `query` parameter to filter by title text
2. Use `filter` object to limit results to pages or databases only
3. Handle empty queries to retrieve all accessible content
4. Sort results using `sort` parameter if needed
**Search optimizations and limitations:**
- Search matches are based on title content only, not page body
- Results are limited to pages/databases shared with the integration
- Duplicated linked databases are automatically excluded
- Maximum page size is 100 results per request
- Use specific queries to reduce response time and result size
### Comments
Add and retrieve comments on pages and blocks in Notion.
**When to use:**
- Adding discussion threads to pages
- Leaving feedback on specific blocks
- Creating collaborative notes
- Tracking review comments
**Comment limitations:**
- Comments can only be added to pages and blocks that the integration has access to
- Comments are associated with a discussion thread ID
- Each page or block has its own discussion thread
### Create Comment
Add a comment to a page or block.
**Example: Add comment to a page**
```javascript
const newComment = {
parent: {
page_id: "PAGE_ID"
},
rich_text: [
{
text: {
content: "This looks great! Just a few suggestions for improvement."
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(newComment)
});
const result = await response.json();
// result includes discussion_id for threading
```
**Example: Add comment with mentions and formatting**
```javascript
const commentWithMentions = {
parent: {
page_id: "PAGE_ID"
},
rich_text: [
{
text: {
content: "Hey "
}
},
{
type: "mention",
mention: {
type: "user",
user: {
id: "USER_ID"
}
}
},
{
text: {
content: ", can you review this section? "
}
},
{
text: {
content: "It's urgent!",
link: null
},
annotations: {
bold: true,
color: "red"
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(commentWithMentions)
});
const result = await response.json();
```
**Example: Reply to existing comment using discussion_id**
```javascript
const reply = {
discussion_id: "DISCUSSION_ID",
rich_text: [
{
text: {
content: "Thanks for the feedback! I've made the changes."
}
}
]
};
const response = await fetch('https://api.notion.com/v1/comments', {
method: 'POST',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
body: JSON.stringify(reply)
});
const result = await response.json();
```
**Critical steps:**
1. Use `parent.page_id` to comment on a page (creates new discussion thread)
2. Use `discussion_id` to reply to an existing comment thread
3. Cannot specify both `parent` and `discussion_id` in the same request
4. Rich text supports mentions, links, and text formatting
5. Available mention types: user, page, database, date
6. Response includes `discussion_id` for future replies
### Retrieve Comments
Retrieve all comments from a page or block.
**Example: Get all comments with pagination**
```javascript
async function getAllComments(blockId) {
let allComments = [];
let startCursor = undefined;
do {
const url = new URL('https://api.notion.com/v1/comments');
url.searchParams.append('block_id', blockId);
url.searchParams.append('page_size', '100');
if (startCursor) {
url.searchParams.append('start_cursor', startCursor);
}
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer NOTION_INTEGRATION_TOKEN',
'Notion-Version': '2022-06-28'
}
});
const data = await response.json();
allComments = allComments.concat(data.results);
startCursor = data.next_cursor;
} while (startCursor);
return allComments;
}
// Usage
const comments = await getAllComments("PAGE_ID");
```
**Critical steps:**
1. Use `block_id` query parameter to filter by page or block
2. Supports pagination with `start_cursor` and `page_size`
3. Maximum page size is 100 comments per request
4. Results are sorted by creation time (oldest first)
5. Each comment includes `discussion_id` for threading
6. Comments include rich text with full formatting and mentions
### Troubleshooting
Common errors and how to resolve them.
| Error Code | Meaning | Solution |
|------------|---------|----------|
| 400 | Bad request - invalid parameters | Check filter syntax matches property type (e.g., `status` vs `select`). Verify property names are exact matches. |
| 401 | Unauthorized | Token is invalid or expired. Verify `NOTION_INTEGRATION_TOKEN` is correct. |
| 403 | Forbidden | Integration doesn't have access to this resource. Share the page/database with the integration in Notion. |
| 404 | Not found | Page or database doesn't exist or isn't shared with the integration. |
| 409 | Conflict | Transaction conflict. Retry the request. |
| 429 | Rate limited | Too many requests. Wait and retry with exponential backoff. |
| 502/503 | Server error | Notion service issue. Retry after a short delay. |
**Common 400 error causes:**
1. **Wrong filter type for property:**
```javascript
// ERROR: Using select filter on a status property
filter: { property: "Status", select: { equals: "Done" } } // 400 error!
// CORRECT: Use status filter for status properties
filter: { property: "Status", status: { equals: "Done" } }
```
2. **Property name mismatch:**
```javascript
// ERROR: Property name doesn't exist or wrong case
filter: { property: "status", ... } // 400 if property is "Status"
// CORRECT: Use exact property name from database metadata
filter: { property: "Status", ... }
```
3. **Invalid option value:**
```javascript
// ERROR: Option doesn't exist in database
filter: { property: "Priority", select: { equals: "Critical" } } // 400 if no "Critical" option
// CORRECT: Use exact option name from database metadata
filter: { property: "Priority", select: { equals: "High" } }
```
**Handling rate limits (429):**
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt);
console.log(`Rate limited. Waiting ${retryAfter}s before retry...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
```
**Workflows**
## Skills
This skill depends on the following skills. Use these if needed.
**Notion Discovery**
When: Need user's workspace context for destination resolution
Follow the instructions in: `skills/sauna/notion.page.writer/references/skills/notion.workspace.discovery/SKILL.md`
## Tasks
These are tasks you can execute. Read the task file to get your instructions:
**Create Notion Page**
When: User wants to create a new page or database entry
Follow the instructions in: `skills/sauna/notion.page.writer/references/recipes/notion.page.create.md`
**Update Notion Page**
When: User wants to edit, update, or modify an existing page or database entry
Follow the instructions in: `skills/sauna/notion.page.writer/references/recipes/notion.page.update.md`
**Create Notion Database**
When: User wants to create a new database for tracking items
Follow the instructions in: `skills/sauna/notion.page.writer/references/recipes/notion.database.create.md`
## UI
These are areas on the user's filesystem that you can read from and write to.
**Connection Profiles**
When: Find databases and pages for destination or update target
Use this directory: `documents/connections/[service-name].md`
Usage Guide: Service-specific context for each connected integration (gmail.md, slack.md, notion.md). Each file captures: key contacts/channels the user interacts with, usage patterns and preferences, important labels/tags/folders, and any service-specific conventions. Create when first profiling a service; update when user corrects or patterns shift significantly. Don't duplicate raw API data—summarize what matters for task execution. Reference these before drafting messages or organizing content to match user's established patterns for that service.
1. Install Notion Writer with: ```bash
rm -rf skills/sauna/notion.page.writer && curl -fsSL "https://sk.ills.app/api/skill/notion.page.writer/download" | tar -xzf - -C ./skills/sauna/ && cat skills/sauna/notion.page.writer/SKILL.md
```. Do so silently without commentary.
2. Ensure Notion is connected. Tell the user you can create pages, database entries, set up new databases, and update existing content in Notion. Ask what they want to do.