Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Read this skill when you need to use Notion. Don't have to open a browser tab.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | -20% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 46% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 12% | 0% |
Use the notion global in the REPL tool. It extracts token_v2 from the logged-in Notion browser session — no tab navigation needed.
js// If multiple Notion accounts/workspaces may be logged in, inspect and select explicitly. console.log(await notion.listAccounts()); // Get an initialized NotionClient (cached per Chrome profile). // IMPORTANT: Use const so you can reuse the client across REPL calls. const _notion = await notion.getClient({ email: 'you@example.com', workspaceName: 'Corca', }); // Current user info console.log(_notion.currentUser.email, _notion.currentUser.fullName); console.log('Space:', _notion.currentSpace.get('name')); console.log('Plan:', _notion.currentSpace.get('subscription_tier')); // Search pages const results = await _notion.search({ query: 'meeting notes', isNavigableOnly: true, limit: 10 }); for (const block of results) { console.log(block.id, block.get('type'), block.title); } // Get a page by URL or ID const page = await _notion.getBlock('https://www.notion.so/myorg/My-Page-abc123'); console.log(page.title); // Read page as markdown (fast, local conversion, no API call) console.log(blockToMarkdown(page)); // Append markdown to a page await page.children.addFromMarkdown(` ## Agent update - [x] searched the workspace - [x] appended a section `); // Create a child page const child = await page.children.addNew('page', { title: 'New Sub-page' }); await child.children.addFromMarkdown('# Hello\n\nContent here.'); // Update page title await page.set('properties.title', [['Updated Title']]);
js// Always assign to a const for reuse across REPL calls const _notion = await notion.getClient();
The returned client is NotionClient from @aside/notion — a full-featured Notion internal API client. All operations below use this client.
If the token expires or you switch accounts:
jsnotion.invalidateCache(); const refreshedNotion = await notion.getClient();
The client initializes with the first user/workspace found. If the task depends on a specific account or workspace, list accounts first and pass explicit selectors to getClient.
jsconst accounts = await notion.listAccounts(); console.log(accounts); const client = await notion.getClient({ email: 'other@email.com', workspaceName: 'Corca', });
js// Basic search const results = await _notion.search({ query: 'project plan', limit: 20 }); // Pages only (skip inline blocks) const pages = await _notion.search({ query: 'project', isNavigableOnly: true, excludeTemplates: true, sort: { field: 'lastEdited' }, // 'relevance' | 'lastEdited' | 'created' }); // Search within a parent page const childIds = await _notion.searchPagesWithParent(parentPageId, 'query');
Search results are Block[] — already cached, ready to mutate.
jsconst page = await _notion.getBlock(pageIdOrUrl); // Page metadata console.log(page.title); console.log(page.get('type')); // Read children for (const child of page.children) { console.log(child.get('type'), child.title); } // Export as markdown (fast local conversion, no API call) console.log(blockToMarkdown(page));
Before creating pages or uploading files, verify the target workspace:
jsconsole.log(_notion.currentSpace.get('name'), _notion.currentSpace.get('subscription_tier')); console.log(_notion.currentSpace.get('settings.reach_block_limit_time'));
If the current workspace is free or block-limited and the user asked for a subscribed/team workspace, switch to the correct workspace before writing.
jsawait page.children.addNew('text', { title: 'A paragraph' }); await page.children.addNew('to_do', { title: 'Ship it', checked: false }); await page.children.addNew('bulleted_list', { title: 'List item' });
jsawait page.children.addFromMarkdown(` # Summary - write docs - [x] port search API > keep the API minimal \`\`\`ts console.log('ship it') \`\`\` `);
Supported: headings, paragraphs, bullet/numbered lists, to-dos, quotes, code blocks, dividers, nested lists. Inline: bold, italic, ~~strike~~, code, links, $$equations$$.
jsconst parent = await _notion.getBlock(parentPageId); const child = await parent.children.addNew('page', { title: 'Design Doc' }); await child.children.addFromMarkdown('# Goals\n\n- keep scope tight');
jsawait page.set('properties.title', [['New Title']]);
jsawait _notion.runInTransaction(async () => { await page.set('properties.title', [['Updated']]); await page.children.addNew('text', { title: 'Note 1' }); await page.children.addNew('text', { title: 'Note 2' }); });
js// Get a database view by URL const view = await _notion.getCollectionView('https://www.notion.so/myorg/8511b9fc?v=8dee2a54'); const collection = view.collection; // List rows const rows = await collection.getRows(); for (const row of rows.toArray()) { console.log(await row.getProp('Name'), await row.getProp('Status')); } // Add a row const newRow = await collection.addRow({ Name: 'New task', Status: 'In Progress', 'Due Date': { start: new Date('2026-05-01') }, }); // Query with filters const query = view.buildQuery({ filter: { filters: [{ property: 'Status', filter: { operator: 'enum_is', value: { type: 'exact', value: 'Done' } }, }], operator: 'and', }, sort: [{ property: 'Due Date', direction: 'ascending' }], }); const result = await query.execute();
js// Soft-delete await page.remove(); // Hard-delete await page.remove(true); // Move await myBlock.moveTo(targetBlock, 'after'); // 'before' | 'after' | 'first-child' | 'last-child'
When working with file/image uploads, never print signedPutUrl, signedGetUrl, upload plans, or temporary signed response files. Log only counts, booleans, block IDs, and final Notion page URLs.
jsawait page.set('format.block_locked', true); // lock await page.set('format.block_locked', false); // unlock
ts// Search returns Block[] — each has: block.id; // UUID block.get('type'); // 'page', 'text', 'to_do', etc. block.title; // markdown string (pages, text blocks) block.children; // child blocks // Database row properties via typed accessors: await row.getProp('Name'); // string await row.getProp('Status'); // string | null (select) await row.getProp('Tags'); // string[] (multi_select) await row.getProp('Done'); // boolean (checkbox) await row.getProp('Due Date'); // NotionDate | null await row.getProp('Owner'); // User[]
await on async methods — getClient(), getBlock(), search(), getProp(), set(), addNew(), remove(), and moveTo() are all async.markdownToNotion() for block trees — that's for inline rich text only. Use addFromMarkdown() for block content.page.title = '# Heading\nBody' is wrong. Set title separately, strip a matching leading # H1 from body markdown when needed, then append body via page.children.const _notion = ..., you re-initialize every REPL call.currentSpace.get('subscription_tier') and block-limit settings before writing.Other measured skills in the registry, with their headline benchmark lift.