From notion-pack
Searches Notion workspaces and retrieves pages, databases, and block content using Notion API. For database queries with filters/sorts/pagination and recursive block extraction.
How this skill is triggered — by the user, by Claude, or both
Slash command
/notion-pack:notion-search-retrieveThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Search across a Notion workspace, query databases with compound filters, retrieve individual pages, and extract nested block content. Covers the full read path: workspace-level search, database queries with filter/sort/pagination, page retrieval, and recursive block tree traversal.
Search across a Notion workspace, query databases with compound filters, retrieve individual pages, and extract nested block content. Covers the full read path: workspace-level search, database queries with filter/sort/pagination, page retrieval, and recursive block tree traversal.
@notionhq/client installed (npm install @notionhq/client)notion-install-auth setupCall notion.search() to find pages and databases. The integration only sees content explicitly shared with it.
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
// Search for pages matching a query
const searchResults = await notion.search({
query: 'meeting notes',
filter: {
property: 'object',
value: 'page', // 'page' or 'database'
},
sort: {
direction: 'descending',
timestamp: 'last_edited_time',
},
page_size: 20,
});
for (const result of searchResults.results) {
if (result.object === 'page' && 'properties' in result) {
const titleProp = Object.values(result.properties)
.find(p => p.type === 'title');
const title = titleProp?.type === 'title'
? titleProp.title.map(t => t.plain_text).join('')
: 'Untitled';
console.log(`${title} (${result.id})`);
}
}
An empty query string returns all shared content. Results are eventually consistent — newly shared pages may take a few seconds to appear in the index.
Call notion.databases.query() for structured queries. Filters support compound and/or logic. See filter-operators.md for every property type and operator.
// Single filter
const activeItems = await notion.databases.query({
database_id: 'your-database-id',
filter: {
property: 'Status',
select: { equals: 'Active' },
},
sorts: [
{ property: 'Priority', direction: 'descending' },
],
page_size: 50,
});
// Compound filter with AND
const highPriorityActive = await notion.databases.query({
database_id: 'your-database-id',
filter: {
and: [
{ property: 'Status', select: { equals: 'Active' } },
{ property: 'Priority', number: { greater_than: 3 } },
],
},
});
Notion uses cursor-based pagination. All list endpoints return has_more and next_cursor. Call notion.pages.retrieve() for a single page, then notion.blocks.children.list() to read its content recursively.
import type {
PageObjectResponse,
BlockObjectResponse,
} from '@notionhq/client/build/src/api-endpoints';
// Paginate through all database results
async function queryAllPages(databaseId: string): Promise<PageObjectResponse[]> {
const pages: PageObjectResponse[] = [];
let cursor: string | undefined = undefined;
do {
const response = await notion.databases.query({
database_id: databaseId,
start_cursor: cursor,
page_size: 100,
});
for (const page of response.results) {
if ('properties' in page) {
pages.push(page as PageObjectResponse);
}
}
cursor = response.has_more ? response.next_cursor! : undefined;
} while (cursor);
return pages;
}
// Retrieve a single page and extract typed property values
async function getPage(pageId: string) {
const page = await notion.pages.retrieve({ page_id: pageId });
if (!('properties' in page)) throw new Error('Partial page object');
return page as PageObjectResponse;
}
function extractProperties(page: PageObjectResponse) {
const result: Record<string, any> = {};
for (const [name, prop] of Object.entries(page.properties)) {
switch (prop.type) {
case 'title':
result[name] = prop.title.map(t => t.plain_text).join(''); break;
case 'rich_text':
result[name] = prop.rich_text.map(t => t.plain_text).join(''); break;
case 'number': result[name] = prop.number; break;
case 'select': result[name] = prop.select?.name ?? null; break;
case 'multi_select':
result[name] = prop.multi_select.map(s => s.name); break;
case 'date':
result[name] = prop.date ? { start: prop.date.start, end: prop.date.end } : null; break;
case 'people':
result[name] = prop.people.map(p => ('name' in p ? p.name : p.id)); break;
case 'checkbox': result[name] = prop.checkbox; break;
case 'url': result[name] = prop.url; break;
case 'email': result[name] = prop.email; break;
case 'phone_number': result[name] = prop.phone_number; break;
case 'status': result[name] = prop.status?.name ?? null; break;
case 'relation': result[name] = prop.relation.map(r => r.id); break;
case 'formula': result[name] = prop.formula; break;
case 'rollup': result[name] = prop.rollup; break;
default: result[name] = `[${prop.type}]`;
}
}
return result;
}
// Recursively fetch all blocks (page content)
async function getPageContent(
blockId: string, depth = 0, maxDepth = 3
): Promise<BlockObjectResponse[]> {
const blocks: BlockObjectResponse[] = [];
let cursor: string | undefined = undefined;
do {
const response = await notion.blocks.children.list({
block_id: blockId,
start_cursor: cursor,
page_size: 100,
});
for (const block of response.results) {
if (!('type' in block)) continue;
const b = block as BlockObjectResponse;
blocks.push(b);
if (b.has_children && depth < maxDepth) {
blocks.push(...await getPageContent(b.id, depth + 1, maxDepth));
}
}
cursor = response.has_more ? response.next_cursor! : undefined;
} while (cursor);
return blocks;
}
function blockToText(block: BlockObjectResponse): string {
const content = (block as any)[block.type];
if (!content?.rich_text) return '';
return content.rich_text.map((t: any) => t.plain_text).join('');
}
| Issue | Cause | Solution |
|---|---|---|
| Could not find database | Database not shared with integration | Open database in Notion, click Share, add the integration |
| Could not find page | Page not shared or deleted | Verify page is shared; check archived status |
| Empty search results | Integration not connected | Share parent page/database with integration; wait for indexing |
| validation_error on filter | Wrong operator for property type | Check filter-operators.md |
| HTTP 429 rate_limited | Too many requests | Back off using Retry-After header; use page_size: 100 |
| Missing properties | Partial page object | Check 'properties' in page before casting to PageObjectResponse |
| Incomplete page content | Not recursing child blocks | Check has_children and recurse; increase maxDepth |
See examples.md for complete patterns including database export, full-text page dump, and compound filter variations.
For creating and updating pages, see notion-core-workflow-a. For PII handling and GDPR compliance, see notion-data-handling. For real-time sync via webhooks, see notion-webhooks-events.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin notion-packQuery Notion databases with filters/sorts, create/update pages with typed properties, and retrieve content using Notion API.
Interact with Notion workspaces via official API CLI: manage pages, databases, blocks, users, comments, and search. For AI agents or developers automating Notion tasks.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.