onenote-core-workflow-a
'Full CRUD lifecycle for OneNote notebooks, section groups, sections,
Allowed Tools
Provided by Plugin
onenote-pack
Claude Code skill pack for OneNote (18 skills)
Installation
This skill is included in the onenote-pack plugin:
/plugin install onenote-pack@claude-code-plugins-plus
Click to copy
Instructions
OneNote — Full CRUD Lifecycle (Notebooks, Sections, Pages)
Overview
OneNote's hierarchy — Notebook, Section Group, Section, Page — maps cleanly to Graph API endpoints, but the implementation has sharp edges. Section groups created via API sometimes don't render in the desktop client. Page content must be strict XHTML with self-closing tags, and the HTML you send in differs from the HTML you get back. This skill covers the full create/read/update/delete lifecycle with production-safe patterns for every level of the hierarchy.
Key pain points addressed:
- Page content requires XHTML (all tags must close, UTF-8 encoded, no
rowspan/colspan) - Section groups support API nesting depths that the desktop app cannot render beyond two levels
- Output HTML from
GET /pages/{id}/contentcontains Graph-injecteddata-idattributes and rewritten image URLs that differ from your input HTML PATCHpage updates use a JSON array withtarget/action/content— not raw HTML
Prerequisites
- Azure app registration with delegated permissions:
Notes.ReadWriteorNotes.ReadWrite.All - App-only auth deprecated March 31, 2025 — use delegated auth only (DeviceCodeCredential or InteractiveBrowserCredential)
- Python:
pip install msgraph-sdk azure-identity - Node/TypeScript:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
Instructions
Step 1 — Authenticate with Delegated Credentials
TypeScript:
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { DeviceCodeCredential } from "@azure/identity";
const credential = new DeviceCodeCredential({
clientId: process.env.AZURE_CLIENT_ID!,
tenantId: process.env.AZURE_TENANT_ID!,
});
const scopes = ["Notes.ReadWrite"];
const authProvider = new TokenCredentialAuthenticationProvider(credential, { scopes });
const client = Client.initWithMiddleware({ authProvider });
Python:
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
credential = DeviceCodeCredential(
client_id=os.environ["AZURE_CLIENT_ID"],
tenant_id=os.environ["AZURE_TENANT_ID"],
)
scopes = ["Notes.ReadWrite"]
client = GraphServiceClient(credentials=credential, scopes=scopes)
Step 2 — Create a Notebook
const notebook = await client.api("/me/onenote/notebooks").post({
displayName: "Project Notes Q2 2026",
});
// notebook.id is the resource identifier for all child operations
console.log(`Created notebook: ${notebook.id}`);
Notebook names must be unique per user. Attempting to create a duplicate returns 400 Bad Request with code 20117.
Step 3 — Create Section Groups and Sections
// Create a section group (top-level organization)
const group = await client.api(
`/me/onenote/notebooks/${notebook.id}/sectionGroups`
).post({ displayName: "Engineering" });
// Create a section inside the group
const section = await client.api(
`/me/onenote/sectionGroups/${group.id}/sections`
).post({ displayName: "Sprint 1" });
// Create a section directly in the notebook (no group)
const standaloneSection = await client.api(
`/me/onenote/notebooks/${notebook.id}/sections`
).post({ displayName: "Quick Notes" });
> Gotcha: The API allows nesting section groups three or more levels deep, but the OneNote desktop app only renders two levels. The web app may show deeper nesting inconsistently. Stick to a maximum of two levels for cross-client compatibility.
Step 4 — Create a Page with XHTML Content
OneNote pages use strict XHTML. Every tag must close. Use data-tag attributes for checkboxes and note tags.
const htmlContent = `<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Sprint Planning - March 2026</title>
<meta name="created" content="2026-03-23T10:00:00-05:00" />
</head>
<body>
<h1>Sprint Planning</h1>
<p>Attendees: Alice, Bob, Charlie</p>
<h2>Action Items</h2>
<ul>
<li data-tag="to-do">Deploy feature X by Friday</li>
<li data-tag="to-do">Review PR #488</li>
<li data-tag="to-do:completed">Set up staging environment</li>
</ul>
<table>
<tr><td>Task</td><td>Owner</td><td>Due</td></tr>
<tr><td>API integration</td><td>Alice</td><td>March 28</td></tr>
</table>
<p>Next meeting: <time datetime="2026-03-30T10:00:00-05:00">March 30</time></p>
</body>
</html>`;
const page = await client.api(
`/me/onenote/sections/${section.id}/pages`
).header("Content-Type", "text/html").post(htmlContent);
console.log(`Page created: ${page.id} — "${page.title}"`);
XHTML rules that cause silent failures if violated:
- All tags must self-close or have closing tags (
, not) - No
rowspanorcolspanon— use separate rows instead tags must includealtattribute- Content must be UTF-8 encoded
Step 5 — Retrieve Page Content
// Metadata (title, timestamps, parent info) — fast, cacheable const metadata = await client.api(`/me/onenote/pages/${page.id}`).get(); // Full HTML content — separate endpoint, slower const content = await client.api(`/me/onenote/pages/${page.id}/content`).get(); // content is a ReadableStream — pipe or buffer it> Important: The HTML returned by
GET /contentdiffers from your input. Graph injectsdata-idattributes on every element, rewrites imagesrcURLs to Graph resource endpoints, and may restructure your table markup. Never diff input vs output HTML for change detection — comparelastModifiedDateTimeinstead.Step 6 — Update Page Content (PATCH)
Updates use a JSON array describing targeted changes, not raw HTML replacement:
await client.api(`/me/onenote/pages/${page.id}/content`).patch([ { target: "body", action: "append", content: "<p>Update: Feature X deployed successfully.</p>", }, { target: "#action-items", action: "replace", content: '<ul><li data-tag="to-do:completed">All items complete</li></ul>', }, ]);Valid
actionvalues:append,replace,delete,insert,prepend. Thetargetis a CSS selector matchingdata-idattributes from the output HTML — you mustGET /contentfirst to obtain valid targets.Step 7 — List and Filter Pages with OData
const pages = await client.api("/me/onenote/sections/{sectionId}/pages") .select("id,title,lastModifiedDateTime,createdDateTime") .top(25) .orderby("lastModifiedDateTime desc") .get(); for (const p of pages.value) { console.log(`${p.title} — Last modified: ${p.lastModifiedDateTime}`); }Step 8 — Delete a Page
await client.api(`/me/onenote/pages/${page.id}`).delete(); // Returns 204 No Content on success // Deleted pages may still appear in LIST results for up to 30 minutesOutput
Successful CRUD operations return:
- Create notebook/section/page:
201 Createdwith resource JSON (includesid,self,createdDateTime) - Get content:
200 OKwith XHTML stream - Patch:
204 No Contenton success - Delete:
204 No Contenton success
Error Handling
Status Cause Fix 400 Invalid XHTML, unclosed tags, duplicate notebook name Validate HTML before sending; check notebook name uniqueness 403 Missing Notes.ReadWritepermission, wrong tenantVerify Azure app permissions and consent status 404 Notebook/section/page deleted or wrong ID Confirm resource exists with a GETbefore mutation429 Rate limit hit (600/min per user) Read Retry-Afterheader, wait that many seconds507 Section page limit exceeded Archive old pages to a new section; see onenote-performance-tuningExamples
Python — Create notebook and page:
notebook = await client.me.onenote.notebooks.post( {"displayName": "Python Notebook"} ) sections = await client.me.onenote.notebooks.by_notebook_id( notebook.id ).sections.post({"displayName": "Notes"}) html = """<!DOCTYPE html> <html><head><title>Hello from Python</title></head> <body><p>Created via msgraph-sdk.</p></body></html>""" page = await client.me.onenote.sections.by_onenote_section_id( sections.id ).pages.post(html)TypeScript — Multipart page with embedded image:
const boundary = "MyPartBoundary"; const body = [ `--${boundary}`, 'Content-Disposition: form-data; name="Presentation"', "Content-Type: text/html", "", '<!DOCTYPE html><html><head><title>With Image</title></head>', '<body><p>See diagram:</p><img src="name:diagram" alt="Architecture" /></body></html>', `--${boundary}`, 'Content-Disposition: form-data; name="diagram"', "Content-Type: image/png", "", imageBuffer.toString("binary"), `--${boundary}--`, ].join("\r\n"); await client.api(`/me/onenote/sections/${sectionId}/pages`) .header("Content-Type", `multipart/form-data; boundary=${boundary}`) .post(body);Resources
- OneNote API Overview
- Create Pages
- Update Pages
- Input/Output HTML
- Note Tags
- Images & Files
- Azure App Registration
Next Steps
- See
onenote-core-workflow-bfor search, pagination, and cross-notebook queries - See
onenote-performance-tuningfor large notebook optimization and image upload limits - See
onenote-rate-limitsfor throttling patterns when doing bulk page creation
Ready to use onenote-pack?