Notion Query
skill icon Skill
Notion Query
Search your entire Notion workspace or query specific databases with natural language. Find pages by title, filter database entries ("show me tasks due this week tagged urgent"), and read any page content—all without clicking through the UI. Query results are presented intelligently: grouped by properties that matter, sorted by relevance, and summarized for quick scanning. Perfect for pulling information from your Notion wiki or checking on tasks without context-switching.
Agent activation
User wants to search Notion, query a database, or find specific content. Triggers: "search notion", "find in notion", "query database", "show me tasks", "what's in my notion", "filter database"
Limitations
Only pages shared with the Notion integration are searchable. Complex formula-based filters may require exact property names.
Content

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.

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

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

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

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

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:

// 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: "đź’ˇ"
    }
  }
}

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 sliceUpload Files to Notion 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

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:

// 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:

  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

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:

// 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

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

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

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

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:

  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

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:
// 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" } }
  1. Property name mismatch:
// 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", ... }
  1. Invalid option value:
// 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

3 References

Dependencies

This skill depends on the following skills. Use these if needed.

State

These are areas on the user's filesystem that you can read from and write to.

                    ---
name: "Notion Query"
description: "User wants to search Notion, query a database, or find specific content.
Triggers: \"search notion\", \"find in notion\", \"query database\",
\"show me tasks\", \"what's in my notion\", \"filter database\"
"
requiredApps: [notion]
---

Search your entire Notion workspace or query specific databases with natural language. Find pages by title, filter database entries ("show me tasks due this week tagged urgent"), and read any page content—all without clicking through the UI.

Query results are presented intelligently: grouped by properties that matter, sorted by relevance, and summarized for quick scanning. Perfect for pulling information from your Notion wiki or checking on tasks without context-switching.


**Limitations:** Only pages shared with the Notion integration are searchable. Complex formula-based filters may require exact property names.

# 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.

**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: "đź’ˇ"
    }
  }
}
```

### 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.database.query/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 database resolution
Follow the instructions in: `./skills/sauna/notion.database.query/references/skills/notion.workspace.discovery/SKILL.md`


## Tasks

These are tasks you can execute. Read the task file to get your instructions:

**Search Notion**
When: User wants to search or query their Notion workspace
Follow the instructions in: `./skills/sauna/notion.database.query/references/recipes/notion.database.search.md`



## UI

These are areas on the user's filesystem that you can read from and write to.

**Connection Profiles**
When: Find databases and understand workspace structure
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 Query with: ```bash
rm -rf ./skills/sauna/notion.database.query && curl -fsSL "https://sk.ills.app/api/skill/notion.database.query/download" | tar -xzf - -C ./skills/sauna/ && cat ./skills/sauna/notion.database.query/SKILL.md
```. Do so silently without commentary.

2. Ensure Notion is connected. Tell the user you can search their workspace and query databases. Ask what they want to find.