From notion-pack
Create, update, archive, and compose Notion pages and block content. Use when building pages programmatically, appending rich content blocks, updating page properties, or managing page lifecycle (archive/restore). Trigger with phrases like "notion create page", "notion add blocks", "notion update page", "notion archive page", "notion content", "notion block types", "notion rich text".
How this skill is triggered — by the user, by Claude, or both
Slash command
/notion-pack:notion-content-managementThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Complete guide to creating, updating, archiving, and composing Notion pages and block content using the `@notionhq/client` SDK. Covers page lifecycle, all common block types, rich text formatting, and bulk content operations.
Complete guide to creating, updating, archiving, and composing Notion pages and block content using the @notionhq/client SDK. Covers page lifecycle, all common block types, rich text formatting, and bulk content operations.
notion-install-auth setupNOTION_TOKEN environment variable set@notionhq/client v2+ installed (TypeScript) or notion-client (Python)Create a page in a database with typed properties and initial block content:
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
// Create a page with properties and inline content
async function createPage(databaseId: string) {
const page = await notion.pages.create({
parent: { database_id: databaseId },
icon: { emoji: '📄' },
cover: {
external: { url: 'https://images.unsplash.com/photo-cover-id' },
},
properties: {
// Title property (required for database pages)
Name: {
title: [{ text: { content: 'Q1 Sprint Retrospective' } }],
},
Status: {
select: { name: 'In Progress' },
},
Priority: {
select: { name: 'High' },
},
Tags: {
multi_select: [{ name: 'Engineering' }, { name: 'Sprint' }],
},
'Due Date': {
date: { start: '2026-04-01', end: '2026-04-05' },
},
Assignee: {
people: [{ id: 'user-uuid-here' }],
},
Effort: {
number: 8,
},
Done: {
checkbox: false,
},
URL: {
url: 'https://example.com/sprint-board',
},
},
// Initial page body (block children)
children: [
{
heading_2: {
rich_text: [{ text: { content: 'Summary' } }],
},
},
{
paragraph: {
rich_text: [{ text: { content: 'This page tracks the Q1 sprint retrospective.' } }],
},
},
],
});
console.log('Created page:', page.id);
return page;
}
Update page properties after creation:
async function updatePageProperties(pageId: string) {
const updated = await notion.pages.update({
page_id: pageId,
properties: {
Status: { select: { name: 'Done' } },
Done: { checkbox: true },
// Clear a property by setting to null
'Due Date': { date: null },
},
// Update icon/cover
icon: { emoji: '✅' },
});
console.log('Updated page:', updated.id);
return updated;
}
Archive and restore pages:
// Archive (soft-delete)
async function archivePage(pageId: string) {
await notion.pages.update({ page_id: pageId, archived: true });
console.log('Archived page:', pageId);
}
// Restore from archive
async function restorePage(pageId: string) {
await notion.pages.update({ page_id: pageId, archived: false });
console.log('Restored page:', pageId);
}
Append blocks to an existing page with blocks.children.append. Each block type has its own payload shape — headings, paragraphs (with rich text annotations), bulleted/numbered lists, to-dos, toggles, code blocks, callouts, quotes, dividers, images, and tables are all supported. The full catalog with the exact shape for every block type is in the block type catalog.
The minimal pattern:
await notion.blocks.children.append({
block_id: pageId,
children: [
{ heading_2: { rich_text: [{ text: { content: 'Notes' } }] } },
{
paragraph: {
rich_text: [
{ text: { content: 'Plain and ' } },
{ text: { content: 'bold' }, annotations: { bold: true } },
],
},
},
{ to_do: { rich_text: [{ text: { content: 'Review PRs' } }], checked: false } },
],
});
Retrieve, modify, and remove specific blocks:
// List all child blocks of a page
async function listBlocks(pageId: string) {
const blocks: any[] = [];
let cursor: string | undefined;
do {
const response = await notion.blocks.children.list({
block_id: pageId,
start_cursor: cursor,
page_size: 100,
});
blocks.push(...response.results);
cursor = response.has_more ? response.next_cursor! : undefined;
} while (cursor);
return blocks;
}
// Update a specific block's content
async function updateBlock(blockId: string) {
await notion.blocks.update({
block_id: blockId,
paragraph: {
rich_text: [
{ text: { content: 'Updated paragraph content with ' } },
{ text: { content: 'new formatting' }, annotations: { bold: true, color: 'red' } },
],
},
});
console.log('Block updated:', blockId);
}
// Update a to-do block's checked state
async function toggleTodo(blockId: string, checked: boolean) {
await notion.blocks.update({
block_id: blockId,
to_do: {
checked,
},
});
}
// Delete a block (moves to trash, recoverable for 30 days)
async function deleteBlock(blockId: string) {
await notion.blocks.delete({ block_id: blockId });
console.log('Deleted block:', blockId);
}
// Retrieve a single block by ID
async function getBlock(blockId: string) {
const block = await notion.blocks.retrieve({ block_id: blockId });
console.log('Block type:', block.type, 'Has children:', block.has_children);
return block;
}
| Error | Cause | Solution |
|---|---|---|
validation_error (400) | Wrong property type or name | Retrieve database schema with databases.retrieve() to confirm property names and types |
object_not_found (404) | Page/block not shared with integration | Open the page in Notion, click ... > Connections > add your integration |
unauthorized (401) | Invalid or expired token | Regenerate at notion.so/my-integrations and update NOTION_TOKEN |
rate_limited (429) | Over 3 requests/second | Implement exponential backoff; read Retry-After header |
conflict_error (409) | Concurrent edit to same block | Retry with fresh block data from blocks.retrieve() |
body too large (413) | Over 100 blocks in one append | Batch into chunks of 100 blocks per blocks.children.append call |
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
async function buildMeetingNotes(databaseId: string) {
// 1. Create the page
const page = await notion.pages.create({
parent: { database_id: databaseId },
icon: { emoji: '📝' },
properties: {
Name: { title: [{ text: { content: `Standup ${new Date().toISOString().slice(0, 10)}` } }] },
Status: { select: { name: 'In Progress' } },
Tags: { multi_select: [{ name: 'Standup' }, { name: 'Daily' }] },
},
});
// 2. Append structured content
await notion.blocks.children.append({
block_id: page.id,
children: [
{ heading_2: { rich_text: [{ text: { content: 'Yesterday' } }] } },
{ bulleted_list_item: { rich_text: [{ text: { content: 'Completed auth integration' } }] } },
{ bulleted_list_item: { rich_text: [{ text: { content: 'Fixed rate-limit retry logic' } }] } },
{ heading_2: { rich_text: [{ text: { content: 'Today' } }] } },
{ to_do: { rich_text: [{ text: { content: 'Build content management module' } }], checked: false } },
{ to_do: { rich_text: [{ text: { content: 'Write integration tests' } }], checked: false } },
{ heading_2: { rich_text: [{ text: { content: 'Blockers' } }] } },
{
callout: {
rich_text: [{ text: { content: 'Waiting on API key for staging environment.' } }],
icon: { emoji: '🚧' },
color: 'red_background',
},
},
],
});
console.log('Meeting notes page:', `https://notion.so/${page.id.replace(/-/g, '')}`);
return page;
}
import os
from notion_client import Client
notion = Client(auth=os.environ["NOTION_TOKEN"])
# Create a page
page = notion.pages.create(
parent={"database_id": "your-database-id"},
properties={
"Name": {"title": [{"text": {"content": "Python Page"}}]},
"Status": {"select": {"name": "Draft"}},
"Tags": {"multi_select": [{"name": "API"}, {"name": "Python"}]},
},
)
print(f"Created: {page['id']}")
# Update properties
notion.pages.update(
page_id=page["id"],
properties={
"Status": {"select": {"name": "Done"}},
},
)
# Append blocks
notion.blocks.children.append(
block_id=page["id"],
children=[
{"heading_2": {"rich_text": [{"text": {"content": "Notes"}}]}},
{
"paragraph": {
"rich_text": [
{"text": {"content": "Created via "}},
{"text": {"content": "Python SDK"}, "annotations": {"bold": True}},
]
}
},
{
"code": {
"rich_text": [{"text": {"content": "print('hello notion')"}}],
"language": "python",
}
},
{"divider": {}},
{
"to_do": {
"rich_text": [{"text": {"content": "Review and publish"}}],
"checked": False,
}
},
],
)
# Archive the page
notion.pages.update(page_id=page["id"], archived=True)
async function appendBlocksChunked(
pageId: string,
blocks: any[],
chunkSize = 100,
) {
for (let i = 0; i < blocks.length; i += chunkSize) {
const chunk = blocks.slice(i, i + chunkSize);
await notion.blocks.children.append({
block_id: pageId,
children: chunk,
});
// Respect rate limits between chunks
if (i + chunkSize < blocks.length) {
await new Promise((r) => setTimeout(r, 350));
}
}
}
Proceed to notion-data-handling for database queries, filtering, sorting, and pagination patterns.
npx claudepluginhub pw00kt/fuzzy-sniffle --plugin notion-packGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.