Claude-code-plugins-plus onenote-hello-world
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/onenote-pack/skills/onenote-hello-world" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-onenote-hello-world && rm -rf "$T"
plugins/saas-packs/onenote-pack/skills/onenote-hello-world/SKILL.mdOneNote Hello World
Overview
Create your first OneNote notebook, section, and page through the Graph API. The critical pitfall this skill addresses: OneNote pages require strict XHTML (not regular HTML). Missing closing tags, unsupported attributes, or table features like
rowspan/colspan cause silent content corruption where the API returns 200 OK but the page renders incorrectly or with missing content.
This skill walks through the full creation chain — notebook, section, page — with correct XHTML, then reads back the content to demonstrate that output HTML differs from input HTML.
Prerequisites
- Completed
— you have a workingonenote-install-auth
(Python) orGraphServiceClient
(TypeScript)Client - Azure AD app with
permission scopeNotes.ReadWrite - Node.js 18+ or Python 3.10+
Instructions
Step 1: Create a Notebook
// TypeScript — create a new notebook const notebook = await client.api("/me/onenote/notebooks").post({ displayName: "Dev Integration Test" }); console.log(`Notebook created: ${notebook.displayName} (${notebook.id})`); // Save notebook.id — you need it for creating sections
# Python — create a new notebook from msgraph.generated.models.notebook import Notebook request_body = Notebook(display_name="Dev Integration Test") notebook = await client.me.onenote.notebooks.post(request_body) print(f"Notebook created: {notebook.display_name} ({notebook.id})")
Naming rules: Notebook names must be unique per user. If a notebook with the same name exists, you get a 400 error with code
20117. Use a timestamp suffix for test notebooks: f"Test-{datetime.now().isoformat()}".
Step 2: Create a Section
// TypeScript — create a section inside the notebook const section = await client .api(`/me/onenote/notebooks/${notebook.id}/sections`) .post({ displayName: "Getting Started" }); console.log(`Section created: ${section.displayName} (${section.id})`);
# Python — create a section from msgraph.generated.models.onenote_section import OnenoteSection section_body = OnenoteSection(display_name="Getting Started") section = await client.me.onenote.notebooks.by_notebook_id( notebook.id ).sections.post(section_body) print(f"Section created: {section.display_name} ({section.id})")
Step 3: Create a Page with Correct XHTML
This is where most integrations break. OneNote requires XHTML — every tag must close, the document must be UTF-8, and several HTML features are silently dropped.
VALID XHTML (this works):
<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Sprint Planning — March 2026</title> <meta name="created" content="2026-03-23T10:00:00-05:00" /> </head> <body> <h1>Sprint Planning Notes</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 CI pipeline</li> </ul> <h2>Decisions</h2> <p>Approved migration to delegated auth. Deadline: <strong>April 15</strong>.</p> <table> <tr> <td>Task</td> <td>Owner</td> <td>Status</td> </tr> <tr> <td>Auth migration</td> <td>Alice</td> <td>In progress</td> </tr> </table> <br /> <p><em>Next meeting: March 30, 2026</em></p> </body> </html>
INVALID HTML (common mistakes that cause silent failures):
<!-- WRONG: unclosed tags — content after <br> may be lost --> <p>Line one<br>Line two</p> <!-- CORRECT: self-closing tags --> <p>Line one<br />Line two</p> <!-- WRONG: rowspan/colspan — silently dropped, table layout breaks --> <td rowspan="2">Merged cell</td> <!-- CORRECT: use separate rows, no merge attributes --> <td>Row 1</td> <!-- WRONG: <img> without self-close --> <img src="https://example.com/chart.png" alt="Chart"> <!-- CORRECT: self-closing img --> <img src="https://example.com/chart.png" alt="Chart" /> <!-- WRONG: style attributes with unsupported CSS — silently ignored --> <p style="display: flex; gap: 8px;">Content</p> <!-- CORRECT: only supported inline styles --> <p style="color: #333; font-size: 14pt;">Content</p>
Send the page:
// TypeScript — create page with XHTML content const xhtml = `<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head><title>Hello from Graph API</title></head> <body> <h1>Hello World</h1> <p>Created via Microsoft Graph API at ${new Date().toISOString()}</p> <ul> <li data-tag="to-do">First task</li> <li data-tag="to-do">Second task</li> </ul> </body> </html>`; const page = await client .api(`/me/onenote/sections/${section.id}/pages`) .header("Content-Type", "text/html") .post(xhtml); console.log(`Page created: ${page.title} (${page.id})`);
# Python — create page via raw HTTP (SDK page creation uses HTML body) import httpx headers = { "Authorization": f"Bearer {token}", "Content-Type": "text/html", } xhtml = """<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head><title>Hello from Graph API</title></head> <body> <h1>Hello World</h1> <p>Created via Microsoft Graph API</p> <ul> <li data-tag="to-do">First task</li> </ul> </body> </html>""" resp = httpx.post( f"https://graph.microsoft.com/v1.0/me/onenote/sections/{section.id}/pages", headers=headers, content=xhtml, ) resp.raise_for_status() page = resp.json() print(f"Page created: {page['title']} ({page['id']})")
Step 4: Read Back Page Content
The HTML you get back from
GET /pages/{id}/content is NOT the same as what you sent. Graph normalizes the HTML, adds data-id attributes, wraps content in div elements, and may reorder attributes.
// TypeScript — read page content back // Note: small delay needed — page indexing is async await new Promise((r) => setTimeout(r, 2000)); const content = await client .api(`/me/onenote/pages/${page.id}/content`) .get(); // content is an HTML string — not the same as what you sent // Graph adds: data-id attributes, absolute positioning, div wrappers console.log("Page HTML (first 500 chars):", content.substring(0, 500));
# Python — read page content import asyncio await asyncio.sleep(2) # Page indexing is async resp = httpx.get( f"https://graph.microsoft.com/v1.0/me/onenote/pages/{page['id']}/content", headers={"Authorization": f"Bearer {token}"}, ) print("Output HTML (first 500 chars):", resp.text[:500]) # Notice: output HTML has data-id attrs, absolute positions, normalized structure
Valid data-tag Values for Checklists
| data-tag value | Renders as |
|---|---|
| Unchecked checkbox |
| Checked checkbox |
| Star icon |
| Question mark icon |
| Red exclamation |
| Bookmark icon |
| Definition marker |
| Yellow highlight |
Output
After completing these steps you will have:
- A new OneNote notebook with a section and page
- A page with correctly formatted XHTML content including checklists
- Understanding of input vs output HTML differences
- Knowledge of XHTML rules that prevent silent content corruption
Error Handling
| Error | Code | Root Cause | Solution |
|---|---|---|---|
| Duplicate notebook name | 400 () | Notebook with same exists | Append timestamp or check existence first |
| Invalid HTML | 400 | Malformed XHTML — unclosed tags, bad encoding | Validate XHTML before sending; use XML parser |
| Section not found | 404 | Notebook ID or section ID is wrong | Re-fetch notebook, verify ID matches |
| Empty page content | 200 (empty body) | Page created but content >4MB | Check payload size before POST |
| Missing title | 400 | tag missing from | Always include |
| Content encoding error | 400 | Non-UTF-8 characters in HTML | Ensure UTF-8 encoding, strip BOM markers |
Examples
Minimal valid page (smallest possible):
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Minimal Page</title></head> <body><p>Content here</p></body> </html>
Page with image from URL:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Page with Image</title></head> <body> <h1>Architecture Diagram</h1> <img src="https://example.com/diagram.png" alt="System architecture" /> <p>Figure 1: Current system architecture</p> </body> </html>
Resources
- OneNote Create Pages
- Input/Output HTML Reference
- Note Tags Reference
- Images and Files in OneNote
- Graph Explorer
- OneNote API Overview
Next Steps
- Use
to add retry logic and rate limit handlingonenote-sdk-patterns - See
when page creation returns unexpected errorsonenote-common-errors - See
to set up mock responses for rapid iterationonenote-local-dev-loop