notion-content-management
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".
Allowed Tools
Provided by Plugin
notion-pack
Claude Code skill pack for Notion (30 skills)
Installation
This skill is included in the notion-pack plugin:
/plugin install notion-pack@claude-code-plugins-plus
Click to copy
Instructions
Notion Content Management
Overview
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.
Prerequisites
- Completed
notion-install-authsetup NOTION_TOKENenvironment variable set- Target database or page shared with your integration (via Connections menu)
@notionhq/clientv2+ installed (TypeScript) ornotion-client(Python)
Instructions
Step 1: Create, Update, and Archive Pages
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);
}
Step 2: Compose Content with Block Types
Append blocks to an existing page. Each block type has its own shape:
async function appendBlocks(pageId: string) {
await notion.blocks.children.append({
block_id: pageId,
children: [
// Headings (heading_1, heading_2, heading_3)
{
heading_1: {
rich_text: [{ text: { content: 'Project Overview' } }],
is_toggleable: false,
},
},
// Paragraph with rich text formatting
{
paragraph: {
rich_text: [
{ text: { content: 'This is ' } },
{ text: { content: 'bold text' }, annotations: { bold: true } },
{ text: { content: ' and ' } },
{ text: { content: 'inline code' }, annotations: { code: true } },
{ text: { content: '. Visit ' } },
{
text: { content: 'our docs', link: { url: 'https://example.com' } },
annotations: { italic: true },
},
{ text: { content: '.' } },
],
},
},
// Bulleted list items
{
bulleted_list_item: {
rich_text: [{ text: { content: 'First bullet point' } }],
},
},
{
bulleted_list_item: {
rich_text: [{ text: { content: 'Second bullet point' } }],
},
},
// Numbered list items
{
numbered_list_item: {
rich_text: [{ text: { content: 'Step one' } }],
},
},
{
numbered_list_item: {
rich_text: [{ text: { content: 'Step two' } }],
},
},
// To-do items
{
to_do: {
rich_text: [{ text: { content: 'Review pull requests' } }],
checked: false,
},
},
{
to_do: {
rich_text: [{ text: { content: 'Update documentation' } }],
checked: true,
},
},
// Toggle block (collapsible)
{
toggle: {
rich_text: [{ text: { content: 'Click to expand details' } }],
children: [
{
paragraph: {
rich_text: [{ text: { content: 'Hidden content inside toggle.' } }],
},
},
],
},
},
// Code block
{
code: {
rich_text: [{ text: { content: 'const x = 42;\nconsole.log(x);' } }],
language: 'typescript',
caption: [{ text: { content: 'Example snippet' } }],
},
},
// Callout
{
callout: {
rich_text: [{ text: { content: 'Important: review before merging.' } }],
icon: { emoji: '⚠️' },
color: 'yellow_background',
},
},
// Quote
{
quote: {
rich_text: [{ text: { content: 'Ship early, ship often.' } }],
color: 'gray',
},
},
// Divider
{ divider: {} },
// Image (external URL)
{
image: {
external: { url: 'https://example.com/diagram.png' },
caption: [{ text: { content: 'System architecture diagram' } }],
},
},
// Table (3 columns x 2 rows)
{
table: {
table_width: 3,
has_column_header: true,
has_row_header: false,
children: [
{
table_row: {
cells: [
[{ text: { content: 'Feature' } }],
[{ text: { content: 'Status' } }],
[{ text: { content: 'Owner' } }],
],
},
},
{
table_row: {
cells: [
[{ text: { content: 'Auth' } }],
[{ text: { content: 'Done' } }],
[{ text: { content: 'Alice' } }],
],
},
},
],
},
},
],
});
console.log('Blocks appended to page:', pageId);
}
Step 3: Update and Delete Individual Blocks
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;
}
Output
- Created pages with typed properties, icons, covers, and initial block content
- Updated page properties and metadata
- Archived and restored pages
- Appended all common block types: headings, paragraphs, lists, to-dos, toggles, code, callouts, quotes, dividers, images, and tables
- Retrieved, updated, and deleted individual blocks
Error Handling
| Error | Cause | Solution |
|---|---|---|
validation_error (400) |
Wrong property type or name | Retrieve database schema with databases.retrieve() to confirm property names and types |
objectnotfound (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 |
Examples
Complete Page Builder
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;
}
Python Example
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)
Batch Block Append (Chunked for >100 Blocks)
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));
}
}
}
Resources
- Working with Page Content
- Create a Page
- Update Page Properties
- Append Block Children
- Block Type Reference
- Rich Text Object
- @notionhq/client npm
- notion-sdk-py GitHub
Next Steps
Proceed to notion-data-handling for database queries, filtering, sorting, and pagination patterns.