|
|
(4) User context mismatch — querying another user's notes without delegation |
Use /users/{userId}/onen
Full CRUD lifecycle for OneNote notebooks, section groups, sections, and pages via Graph API.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
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}/content contains Graph-injected data-id attributes and rewritten image URLs that differ from your input HTML
PATCH page updates use a JSON array with target/action/content — not raw HTML
Prerequisites
- Azure app registration with delegated permissions:
Notes.ReadWrite or Notes.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",
}
Search, query, and paginate OneNote content with OData filters and client-side search patterns.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote — Search, Query, and Pagination
Overview
OneNote's dedicated search endpoint was deprecated in April 2024. The replacement — OData $filter queries on page listings — cannot search page body content, cannot search across all notebooks in a single call, and sometimes returns deleted pages in results. Pagination via @odata.nextLink is unreliable: the link is sometimes omitted even when more results exist. This skill provides production-tested patterns for content discovery, cross-notebook queries, and safe pagination with guard rails.
Key pain points addressed:
- The
$search parameter on /me/onenote/pages is deprecated — use $filter on metadata fields only
- No single endpoint searches across all notebooks — you must iterate notebooks and their sections
- Deleted pages continue appearing in
GET /sections/{id}/pages results for up to 30 minutes
@odata.nextLink may be absent even when $top items were returned (Graph bug with OneNote)
Prerequisites
- Azure app registration with delegated permissions:
Notes.Read or Notes.ReadWrite
- App-only auth deprecated March 31, 2025 — use delegated auth only
- Python:
pip install msgraph-sdk azure-identity
- Node/TypeScript:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
- Optional for client-side search:
npm install fuse.js or pip install thefuzz
Instructions
Step 1 — Query Pages with OData Filters
OData $filter works on page metadata fields — not body content. Supported fields: title, createdDateTime, lastModifiedDateTime.
import { Client } from "@microsoft/microsoft-graph-client";
// Filter by title substring
const results = await client.api("/me/onenote/pages")
.filter("contains(title, 'sprint planning')")
.select("id,title,lastModifiedDateTime,parentSection")
.top(20)
.orderby("lastModifiedDateTime desc")
.get();
// Filter by date range
const recentPages = await client.api("/me/onenote/pages")
.filter("lastModifiedDateTime ge 2026-03-01T00:00:00Z")
.select("id,title,lastModifiedDateTime")
.top(50)
.orderby("lastModifiedDateTime desc")
.get();
> Warning: $search was deprecated April 2024. Using it returns 400 Bad Request on most tenants. Use $filter with contains() on title, or implement client-side search on fetched content.
Step 2 — Cross-Notebook Search Pattern
There is no single Graph endpoint that searches page content across all note
Optimize costs and API usage for OneNote Graph API integrations with caching and batching strategies.
ReadWriteEditGrep
OneNote Cost Tuning
Overview
OneNote Graph API calls have no per-request cost — they are included in every Microsoft 365 E3/E5/Business license. However, rate limits create an effective ceiling that functions like a cost constraint: 600 requests per user per 60 seconds and 10,000 requests per app per 10 minutes at the tenant level. Exceeding these limits returns 429 errors with Retry-After headers, degrading user experience the same way budget overruns degrade service. This skill covers the practical optimization strategies that keep you well under those ceilings: metadata caching, JSON batch requests, delta sync, payload minimization with $select/$expand, and content deduplication. A naive integration that polls every user's notebooks every minute burns through the tenant limit in under 10 minutes. An optimized one handles thousands of users within the same budget.
Prerequisites
- Microsoft 365 license (E3/E5/Business) — OneNote API is included, no additional billing
- Azure AD app registration with delegated permissions
- Python:
pip install msgraph-sdk azure-identity or Node: npm install @microsoft/microsoft-graph-client @azure/identity
- Understanding of HTTP caching headers (ETag, If-None-Match)
Instructions
Licensing Model and True Cost
| Component |
Cost |
| OneNote API calls |
Included in M365 license (no per-call charge) |
| Rate limit: per user |
600 requests / 60 seconds |
| Rate limit: per tenant |
10,000 requests / 10 minutes |
| Retry-After penalty |
Blocked for N seconds (header value) |
| Graph metered billing |
Optional; extends limits for high-volume apps |
The real cost is operational: every 429 response adds latency, retry logic consumes compute, and throttled users see failures. Optimization is about reliability, not billing.
Strategy 1: Cache Metadata Aggressively
Notebook and section metadata changes rarely (names, IDs, hierarchy). Cache it locally and refresh on a schedule, not per-request:
interface CachedMetadata {
notebooks: any[];
sections: Map<string, any[]>; // notebookId -> sections
fetchedAt: number;
ttlMs: number;
}
class MetadataCache {
private cache: CachedMetadata = {
notebooks: [],
sections: new Map(),
fetchedAt: 0,
ttlMs: 15 * 60 * 1000, // 15 minutes — notebooks/sections rarely change
};
isStale(): boolean {
return Date.now() - this.cache.fetchedAt > this.cache.ttlMs;
}
async getNotebooks(client: any): Promise<any[]> {
if (!this.isStale() && this.cache.notebooks.length > 0) {
return this.cache.notebooks; // 0 API calls
}
con
Generate comprehensive diagnostic bundles for OneNote Graph API issues with request tracing and token analysis.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Debug Bundle
Overview
When OneNote API calls fail, you need the request-id response header (Microsoft support requires it), decoded JWT token claims (to verify granted scopes), the x-ms-ags-diagnostic header (internal Graph routing info), and the full error body — all correlated to a single request. This skill generates structured diagnostic bundles for self-diagnosis and Microsoft support ticket filing.
Prerequisites
- Node.js 18+ or Python 3.10+
- An existing OneNote integration with Graph API calls
@microsoft/microsoft-graph-client or msgraph-sdk installed
- Access token available (for token inspection)
Instructions
TypeScript Diagnostic Middleware
Intercept all Graph API calls and capture diagnostics automatically:
// src/debug/diagnostic-middleware.ts
interface DiagnosticEntry {
timestamp: string; method: string; url: string; status: number;
requestId: string | null; agsDiagnostic: string | null;
retryAfter: string | null; duration_ms: number; error: any | null;
}
const diagnosticLog: DiagnosticEntry[] = [];
export function createDiagnosticMiddleware() {
return {
execute: async (context: any) => {
const start = Date.now();
const { url, method } = context.request || { url: "unknown", method: "GET" };
try {
await context.next();
const h = context.response?.headers;
diagnosticLog.push({
timestamp: new Date().toISOString(), method, url,
status: context.response?.status || 0,
requestId: h?.get?.("request-id") || null,
agsDiagnostic: h?.get?.("x-ms-ags-diagnostic") || null,
retryAfter: h?.get?.("retry-after") || null,
duration_ms: Date.now() - start, error: null,
});
} catch (err: any) {
diagnosticLog.push({
timestamp: new Date().toISOString(), method, url,
status: err?.statusCode || 0,
requestId: err?.headers?.["request-id"] || null,
agsDiagnostic: err?.headers?.["x-ms-ags-diagnostic"] || null,
retryAfter: err?.headers?.["retry-after"] || null,
duration_ms: Date.now() - start,
error: { code: err?.code, message: err?.message, body: err?.body },
});
throw err;
}
},
};
}
export function getDiagnosticLog(): DiagnosticEntry[] { return [...diagnosticLog]; }
export function clearDiagnosticLog(): void { diagnosticLog.length = 0; }
Token Claims Inspection (No External Libraries)
Decode a JWT access token to inspect scopes and expiry using only built-in Base64:
// src/debug/token-inspector.ts
export function decodeTokenClaims(accessToken: string): Record<string, any> {
const part
Deploy OneNote integrations with MSAL token persistence, health checks, and container best practices.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Deploy Integration
Overview
Deploying OneNote integrations into containers breaks local development assumptions: MSAL token caches vanish on restart, health checks must validate Graph API connectivity (not just HTTP 200), and graceful shutdown must flush token state. This skill provides production-ready Dockerfile, Docker Compose, and Kubernetes manifests with MSAL token persistence, health/readiness probes that verify actual Graph reachability, and SIGTERM handling.
Prerequisites
- Docker 24+ and Docker Compose v2
- Node.js 20 LTS or Python 3.11+
- Azure AD app registration with delegated permissions (
Notes.Read, Notes.ReadWrite)
- Redis (recommended for multi-replica) or persistent volume for token cache
Instructions
Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json src/ ./
RUN npm run build
FROM node:20-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
RUN mkdir -p /app/.cache/msal && chown -R node:node /app/.cache
USER node
ENV NODE_ENV=production MSAL_CACHE_DIR=/app/.cache/msal
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -sf http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/index.js"]
MSAL Token Cache Persistence
File-based (single replica):
// src/auth/token-cache.ts
import { readFile, writeFile, mkdir } from "fs/promises";
import { existsSync } from "fs";
import path from "path";
const CACHE_DIR = process.env.MSAL_CACHE_DIR || "/app/.cache/msal";
const CACHE_FILE = path.join(CACHE_DIR, "token-cache.json");
export async function loadCache(): Promise<string | null> {
try {
if (existsSync(CACHE_FILE)) return await readFile(CACHE_FILE, "utf-8");
} catch (err) { console.error("Failed to load token cache:", err); }
return null;
}
export async function saveCache(contents: string): Promise<void> {
await mkdir(CACHE_DIR, { recursive: true });
await writeFile(CACHE_FILE, contents, { mode: 0o600 });
}
Redis-based (multi-replica):
// src/auth/redis-cache.ts
import { createClient, RedisClientType } from "redis";
const CACHE_KEY = "msal:onenote:token-cache";
let redis: RedisClientType;
export async function initRedisCache(): Promise<void> {
redis = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
redis.on("error", (err
Create your first OneNote notebook, section, and page with correct XHTML content.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote 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
onenote-install-auth — you have a working GraphServiceClient (Python) or Client (TypeScript)
- Azure AD app with
Notes.ReadWrite permission scope
- 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):
<
Install and configure OneNote SDK/API authentication with delegated auth (MSAL).
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Install & Auth
Overview
Set up Microsoft Graph API authentication for OneNote using delegated credentials via MSAL. This skill walks through Azure AD app registration, SDK installation, permission scope selection, token caching, and connection verification for both Python and TypeScript.
BREAKING CHANGE (March 31, 2025): App-only authentication (ClientSecretCredential) was deprecated for OneNote APIs. All integrations MUST use delegated auth — DeviceCodeCredential or InteractiveBrowserCredential. If your existing code uses ClientSecretCredential with OneNote endpoints, it will receive 403 Forbidden on every call. This skill provides the correct migration path.
Prerequisites
- Azure account with permission to register applications (Azure AD admin or Application Developer role)
- Node.js 18+ or Python 3.10+
- Access to Azure Portal App Registrations
- A OneNote account (personal Microsoft account or Microsoft 365 work/school account)
Instructions
Step 1: Register an Azure AD Application
- Navigate to Azure Portal > App Registrations
- Click New registration
- Set the Name (e.g.,
onenote-integration-dev)
- Under Supported account types, choose:
- Single tenant — only your organization (most restrictive, recommended for internal tools)
- Multi-tenant — any Azure AD directory (needed if serving multiple orgs)
- Multi-tenant + personal — includes personal Microsoft accounts (needed if targeting consumer OneNote)
- Under Redirect URI, select Public client/native and set URI to
http://localhost
- Click Register and note the Application (client) ID and Directory (tenant) ID
Step 2: Configure API Permissions
- In your app registration, go to API permissions > Add a permission > Microsoft Graph > Delegated permissions
- Add the appropriate scope:
| Scope |
Use Case |
Notes.Read |
Read-only access to user's notebooks |
Notes.ReadWrite |
Read and write to user's notebooks |
Notes.ReadWrite.All |
Read/write all notebooks the user can access (including shared) |
Notes.Read.All |
Read all notebooks the user can access (including shared) |
- Click Grant admin consent if you h
Set up a local development loop for OneNote integrations with mock Graph API responses.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Local Dev Loop
Overview
Testing OneNote integrations typically requires Azure AD credentials and live Graph API calls, which means authentication friction on every dev session and risk of hitting the 600 req/60s rate limit during rapid iteration. This skill sets up a local development loop with mock Graph responses so you can develop and test OneNote features without Azure credentials, without rate limits, and with instant feedback.
The mock layer intercepts HTTP calls to graph.microsoft.com and returns realistic fixture data, including the XHTML output format that differs from input format. You can switch between mock and live Graph with a single environment variable.
Prerequisites
- Node.js 18+ or Python 3.10+
- Familiarity with your project's test framework (vitest/jest for Node, pytest for Python)
- Optional: completed
onenote-install-auth for live mode switching
Instructions
Step 1: Project Structure
my-onenote-app/
├── .env # GRAPH_MODE=mock or GRAPH_MODE=live
├── .env.example # Template (commit this, not .env)
├── src/
│ ├── client.ts # Graph client factory (mock/live switching)
│ ├── onenote.ts # Business logic (testable)
│ └── types.ts # OneNote type definitions
├── tests/
│ ├── fixtures/
│ │ ├── notebooks.json # Mock notebook list response
│ │ ├── sections.json # Mock section list response
│ │ ├── pages.json # Mock page list response
│ │ ├── page-content.html # Mock page HTML (output format)
│ │ └── error-responses.json # Mock error responses for testing
│ ├── mocks/
│ │ └── graph-handlers.ts # MSW request handlers
│ └── onenote.test.ts # Unit tests
├── package.json
└── tsconfig.json
Step 2: Mock Graph API Server (TypeScript with MSW)
MSW (Mock Service Worker) intercepts HTTP requests at the network level, so your production code does not need any changes to work with mocks.
// tests/mocks/graph-handlers.ts
import { http, HttpResponse } from "msw";
const BASE = "https://graph.microsoft.com/v1.0";
// Import fixture data
import notebooksFixture from "../fixtures/notebooks.json";
import sectionsFixture from "../fixtures/sections.json";
import pagesFixture from "../fixtures/pages.json";
import { readFileSync } from "fs";
import { join } from "path";
const pageContentFixture = readFileSync(
join(__dirname, "../fixtures/page-content.html"),
"utf-8"
);
export const graphHandlers = [
// List notebooks
http.get(`${BASE}/me/onenote/notebooks`, () => {
return HttpResponse.json(notebooksFixture);
}),
// Create notebook
http.post(`${BASE}
Optimize OneNote Graph API performance for large notebooks, image handling, and batch operations.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote — Performance Tuning & Optimization
Overview
OneNote performance degrades predictably at scale: notebooks with 100+ sections take 3-5 seconds per API call when using $expand, pages with embedded images over 4MB fail silently, and sections hitting the page limit return 507 Insufficient Storage. Image uploads are capped at 25MB per multipart part, and requesting full page content for hundreds of pages without $select can exhaust your rate budget in seconds.
This skill provides tested patterns for every performance bottleneck: selective $expand and $select for minimal payloads, image compression before upload, batch requests via $batch, pagination with $top to avoid loading thousands of pages, and caching strategies that invalidate on change detection.
Key pain points addressed:
- Full
$expand=sections($expand=pages) on large notebooks can take 10+ seconds and return multi-MB responses
- Image uploads silently fail when a single multipart part exceeds 25MB — no error, just missing image
507 Insufficient Storage when a section hits its page limit (approximately 5,000 pages)
- Page content retrieval (
GET /pages/{id}/content) is 5-10x slower than metadata-only requests
Prerequisites
- Azure app registration with delegated permissions:
Notes.ReadWrite
- App-only auth deprecated March 31, 2025 — use delegated auth only
- Python:
pip install msgraph-sdk azure-identity Pillow (Pillow for image compression)
- Node/TypeScript:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node sharp (sharp for image compression)
Instructions
Step 1 — Use $select to Minimize Payload Size
Every Graph API call should specify $select to return only the fields you need. The default response includes navigation properties, OData metadata, and verbose timestamps that inflate payloads:
// BAD — returns ~2KB per page with all metadata
const pages = await client.api("/me/onenote/pages").get();
// GOOD — returns ~200 bytes per page with only needed fields
const pages = await client.api("/me/onenote/pages")
.select("id,title,lastModifiedDateTime")
.get();
// For notebooks, avoid expanding everything
// BAD — can take 10+ seconds on large notebooks
const notebooks = await client.api("/me/onenote/notebooks")
.expand("sections($expand=pages)")
.get();
// GOOD — get structure first, then drill into sections on demand
const notebooks = await client.api("/me/onenote/notebooks")
.select("id,displayName,lastModifiedDateTime,sectionsUrl")
.get();
Payload size comparison for a notebook with 50 sect
Production readiness checklist for OneNote Graph API integrations covering auth, rate limits, and failure modes.
ReadWriteEditGrep
OneNote Production Checklist
Overview
OneNote integrations that work perfectly in development break in production in predictable ways: SharePoint document libraries exceed the 5,000-item view threshold and stop returning notebooks, image uploads silently truncate above 4MB, rate limits compound across users during business hours, and MSAL token caches lose state across container restarts. This skill is a comprehensive go/no-go checklist organized by failure category. Each item references the specific Graph API behavior that causes the production failure and provides the fix. Use this checklist during launch reviews — every unchecked item is a production incident waiting to happen.
Prerequisites
- A functional OneNote integration that works in development/staging
- Azure AD app registration with delegated permissions configured
- Access to production monitoring infrastructure (logging, alerting)
- Familiarity with your deployment environment (containers, VMs, serverless)
- Completed
onenote-security-basics and onenote-rate-limits skills
Instructions
1. Authentication Checklist
| # |
Check |
Why it matters |
| 1.1 |
Using delegated auth (DeviceCodeCredential or InteractiveBrowserCredential) |
App-only auth (ClientSecretCredential) deprecated for OneNote March 31, 2025 |
| 1.2 |
MSAL token cache serialized to persistent storage |
Container restarts lose in-memory cache; users forced to re-authenticate |
| 1.3 |
Silent token renewal tested (call acquiretokensilent before every request) |
Access tokens expire in 1 hour; without silent renewal, users hit 401 hourly |
| 1.4 |
Refresh token 90-day expiry monitored |
Inactive users' refresh tokens expire silently; need re-auth flow |
| 1.5 |
Token cache file permissions set to 0600 (owner-only) |
Cache contains refresh tokens — world-readable is a credential leak |
| 1.6 |
Multi-tenant: tid claim validated on every token |
Prevents cross-tenant data leakage from token reuse |
Verification test:
import os, time
def verify_auth_resilience(client):
"""Test that auth survives token expiry cycle."""
# 1. Make a call to confirm auth works
response = client.me.onenote.notebooks.get()
assert response.value is not None, "Initial auth failed"
# 2. Verify token cache exists on disk
cache_path = os.path.expanduser("~/.onenote-token-cache.json")
assert os.path.exists(cache_path), "Token cache not persisted"
stat = os.stat(cache_path)
assert oct(stat.st_mode)[-3
Implement proper rate limit handling for OneNote Graph API with queue-based throttling.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote — Rate Limit Handling & Request Throttling
Overview
Microsoft Graph rate limits OneNote at 600 requests per 60 seconds per user and 10,000 requests per 10 minutes per app/tenant. When you exceed either limit, the API returns 429 Too Many Requests with a Retry-After header specifying how many seconds to wait. Most implementations either ignore this header entirely (retrying immediately, making things worse) or use a fixed backoff that wastes capacity.
This skill implements a token bucket rate limiter, queue-based request throttling, and proper Retry-After header parsing. For multi-user apps, it tracks per-user and per-tenant budgets independently.
Key pain points addressed:
- The
Retry-After header value is in seconds (not milliseconds) — many implementations parse this wrong
- The per-user limit (600/60s) is separate from the per-tenant limit (10,000/10min) — you can hit one without the other
- Batch requests (
$batch) count as one request toward the limit, regardless of how many operations are inside
- After a 429, subsequent requests to ANY OneNote endpoint are throttled — not just the endpoint that triggered it
Prerequisites
- Azure app registration with delegated permissions:
Notes.ReadWrite
- App-only auth deprecated March 31, 2025 — use delegated auth only
- Python:
pip install msgraph-sdk azure-identity
- Node/TypeScript:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
- Optional:
npm install p-queue for production queue management
Instructions
Step 1 — Understand the Rate Limit Structure
| Limit |
Scope |
Window |
Threshold |
| Per-user |
Single user's delegated token |
60 seconds (rolling) |
600 requests |
| Per-tenant |
All users + all apps in the tenant |
10 minutes (rolling) |
10,000 requests |
When either limit is hit:
- Response status:
429 Too Many Requests
- Response header:
Retry-After: (integer, not milliseconds)
- All subsequent OneNote requests for that scope are blocked until the window resets
- Non-OneNote Graph endpoints (Outlook, OneDrive) are not affected
Step 2 — Token Bucket Rate Limiter (TypeScript)
A token bucket preemptively throttles requests to stay below the limit, avoiding 429s entirely:
class TokenBucket {
private tokens: number;
private lastRefill: number;
private readonly maxTokens: number;
private readonly refillRate: number; // tokens per millisecond
constructor(
Reference architecture for OneNote integrations covering all notebook locations and API path patterns.
ReadWriteEditGrep
OneNote Reference Architecture
Overview
OneNote notebooks live in three completely different storage backends — personal OneDrive, SharePoint team sites, and Microsoft 365 Groups — each with its own Graph API path, permission model, and behavioral quirks. Building an integration that "just works with OneNote" means handling all three locations, because users do not know (or care) where their notebook is stored. The API path /me/onenote/notebooks only returns personal notebooks; SharePoint and Group notebooks require different endpoints entirely. This skill maps the full architecture: storage locations, API paths, the object hierarchy (and its gotchas), and a service abstraction layer that normalizes all three locations into a single interface.
Prerequisites
- Azure AD app registration with delegated permissions (
Notes.ReadWrite minimum)
- Familiarity with Microsoft Graph API URL structure (
https://graph.microsoft.com/v1.0)
- For SharePoint notebooks:
Sites.Read.All or Sites.ReadWrite.All permission
- For Group notebooks:
Group.Read.All or Group.ReadWrite.All permission
- Python:
pip install msgraph-sdk azure-identity or Node: npm install @microsoft/microsoft-graph-client @azure/identity
Instructions
System Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Your App │────>│ MSAL Auth │────>│ Azure AD │
│ (Client) │ │ (Delegated) │ │ Token Service │
└──────┬──────┘ └──────────────┘ └─────────────────┘
│
│ Bearer Token
v
┌──────────────────────────────────────────────────────────┐
│ Microsoft Graph API (v1.0) │
│ https://graph.microsoft.com/v1.0 │
├──────────────┬──────────────────┬────────────────────────┤
│ /me/onenote │ /sites/{id}/ │ /groups/{id}/ │
│ │ onenote │ onenote │
├──────────────┼──────────────────┼────────────────────────┤
│ Personal │ SharePoint │ Group │
│ OneDrive │ Document Lib │ Notebook │
│ Storage │ Storage │ Storage │
└──────────────┴──────────────────┴────────────────────────┘
Three Notebook Locations
1. Personal Notebooks (OneDrive)
GET https://graph.microsoft.com/v1.0/me/onenote/notebooks
GET https://graph.microsoft.com/v1.0/me/onenote/notebooks/{notebook-id}/sections
GET https://graph.microsoft.com/v1.0/me/onenote/sections/{section-id}/pages
- Owned by the signed-in user
- Stored in user's OneDrive root
/Documents/ or /Notebooks/
- Permission:
Notes.ReadWrite (user consent, no admin needed)
- Can
Production SDK patterns for OneNote Graph API: retry logic, batch requests, and safe file uploads.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote SDK Patterns
Overview
Production-grade patterns for the OneNote Graph API. The two biggest production issues are rate limits (600 requests per 60 seconds per user, 10,000 per 10 minutes per tenant) and silent upload failures where files >4MB return 200 OK with an empty response body — the page is never created but no error is raised.
This skill provides middleware chains, retry decorators, batch request patterns, and silent failure detection for both TypeScript and Python.
Prerequisites
- Completed
onenote-install-auth — working Graph API authentication
- Understanding of async/await patterns in your target language
- Node.js 18+ or Python 3.10+
Instructions
Pattern 1: Retry Middleware with Retry-After Header Parsing (TypeScript)
The Graph API returns a Retry-After header (in seconds) with 429 responses. Hardcoding a fixed retry delay wastes time or hits limits again.
import { Client, ClientOptions } from "@microsoft/microsoft-graph-client";
class RetryHandler {
private maxRetries = 3;
async execute(
context: { request: Request; options: RequestInit },
next: (context: any) => Promise<Response>
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const response = await next(context);
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After");
const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : 30;
console.warn(
`Rate limited (429). Retry-After: ${waitSeconds}s. ` +
`Attempt ${attempt + 1}/${this.maxRetries + 1}`
);
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
continue;
}
if (response.status === 502 || response.status === 503) {
const backoff = Math.pow(2, attempt) * 1000;
console.warn(
`Server error (${response.status}). Backing off ${backoff}ms.`
);
await new Promise((r) => setTimeout(r, backoff));
continue;
}
return response;
} catch (err) {
lastError = err as Error;
if (attempt < this.maxRetries) {
const backoff = Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, backoff));
}
}
}
throw lastError ?? new Error("Max retries exceeded");
}
}
Pattern 2: Retry Decorator (Python)
import asyncio
import functools
from typing import TypeVar, Callable, Any
T = TypeVar("T")
def retry_graph(max_retries: int = 3):
"""Decorator for Graph API calls with rate limit awareness."""
def decorator(func: Callable[
Implement secure authentication, token management, and permission scoping for OneNote Graph API.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Security Basics
Overview
OneNote Graph API security changed fundamentally on March 31, 2025, when Microsoft deprecated app-only authentication for OneNote endpoints. Every integration must now use delegated authentication through MSAL, which means real users must sign in — no more background service accounts with client secrets. This skill covers the full security surface: permission scoping, token lifecycle management, MSAL cache serialization, credential storage, and multi-tenant hardening. Get any of these wrong and your integration either breaks silently (expired tokens returning 401s) or over-provisions access (Notes.ReadWrite.All when Notes.Read suffices).
Prerequisites
- Azure AD app registration with redirect URI configured at https://portal.azure.com/#blade/MicrosoftAADRegisteredApps
- Microsoft 365 license (E3/E5/Business) with OneNote enabled
- Python:
pip install msgraph-sdk azure-identity msal or Node: npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
- Understanding of OAuth 2.0 authorization code flow and delegated permissions
Instructions
Permission Scope Matrix
Choose the minimum scope required for your use case:
| Scope |
Read notebooks |
Read pages |
Create pages |
Create notebooks |
Admin consent? |
Notes.Read |
Yes |
Yes |
No |
No |
No |
Notes.ReadWrite |
Yes |
Yes |
Yes |
Yes |
No |
Notes.ReadWrite.All |
Yes |
Yes |
Yes |
Yes |
Yes |
Notes.Create |
No |
No |
Yes |
Yes |
No |
Least-privilege recommendations:
- Read-only dashboards:
Notes.Read (user consent only)
- Personal note creation:
Notes.ReadWrite (user consent only)
- Cross-user/organizational access:
Notes.ReadWrite.All (requires tenant admin approval)
- Write-only ingestion:
Notes.Create (cannot read back what was written)
Delegated Authentication Setup (Post-2025 Mandatory)
CRITICAL: App-only authentication (ClientSecretCredential) was deprecated for OneNote endpoints on March 31, 2025. All code below uses delegated auth exclusively.
Python — Device Code Flow (headless/CLI environments):
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
import os
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
TENANT_ID = os.environ["AZURE_TENANT_ID"]
# Minimal scopes — only request what you ne
Migrate OneNote integrations across Graph SDK versions, auth deprecations, and API changes.
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote Upgrade & Migration
Overview
Microsoft shipped three breaking changes to OneNote integrations in under two years: webhook decommissioning (June 2023), search endpoint deprecation (April 2024), and app-only auth deprecation (March 2025). The Graph SDK itself had breaking changes between v5 and v6. This skill provides exact migration diffs, verification steps, and rollback strategies for each breaking change.
Prerequisites
- Existing OneNote integration using Graph API
- Node.js 18+ (TypeScript SDK) or Python 3.10+ (Python SDK)
- Git for branch-based migration with rollback capability
- Azure portal access for app registration changes (auth migration)
Instructions
Breaking Changes Timeline
| Date |
Change |
Impact |
| June 16, 2023 |
Webhooks decommissioned |
Subscription notifications stop |
| April 2024 |
Search endpoint deprecated |
/pages?search= returns 404 |
| March 31, 2025 |
App-only auth deprecated |
ClientSecretCredential returns 403 |
Migration 1: App-Only to Delegated Auth
Before (broken after March 2025):
// OLD — ClientSecretCredential (DEPRECATED for OneNote)
import { ClientSecretCredential } from "@azure/identity";
const credential = new ClientSecretCredential(TENANT_ID, CLIENT_ID, CLIENT_SECRET);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["https://graph.microsoft.com/.default"],
});
After:
// NEW — DeviceCodeCredential (required)
import { DeviceCodeCredential } from "@azure/identity";
const credential = new DeviceCodeCredential({
clientId: CLIENT_ID, tenantId: TENANT_ID,
userPromptCallback: (info) =>
console.log(`Open ${info.verificationUri} and enter code: ${info.userCode}`),
});
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["Notes.Read", "Notes.ReadWrite"], // Explicit, not .default
});
const client = Client.initWithMiddleware({ authProvider });
Python equivalent:
# OLD: credential = ClientSecretCredential(tenant_id, client_id, client_secret)
# NEW:
from azure.identity import DeviceCodeCredential
credential = DeviceCodeCredential(client_id=CLIENT_ID, tenant_id=TENANT_ID)
Required Azure portal changes: Add "Mobile and desktop applications" platform with http://localhost redirect URI to your app registration.
Migration 2: Webhooks to Polling
// OLD — Webhook subscription (DECOMMISSIONED June 2023)
//
Implement change detection for OneNote using polling and delta queries (webhooks decommissioned June 2023).
ReadWriteEditBash(npm:*)Bash(pip:*)Grep
OneNote — Change Detection (Polling & Delta Queries)
Overview
> OneNote webhooks were decommissioned June 16, 2023. The Graph subscription API (POST /subscriptions with changeType: "updated" on OneNote resources) returns 400 Bad Request. Unlike Outlook mail, calendar, and OneDrive — which still support push notifications — OneNote has no webhook replacement. You must poll.
This skill implements efficient change detection for OneNote using lastModifiedDateTime comparisons, delta query patterns, and rate-limit-aware polling intervals. The approach balances freshness (detecting changes within minutes) against the 600 requests/minute per-user rate limit.
Key pain points addressed:
- Subscription API for OneNote resources returns
400 — do not attempt it
- Delta queries (
/me/onenote/pages/delta) are not officially documented but work on some tenants
- Polling must stay within rate budget (600/min per user, 10,000/10min per tenant)
- Change detection requires comparing timestamps, not content diffs (output HTML is unstable)
Prerequisites
- Azure app registration with delegated permissions:
Notes.Read or Notes.ReadWrite
- App-only auth deprecated March 31, 2025 — use delegated auth only
- Python:
pip install msgraph-sdk azure-identity
- Node/TypeScript:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
- A persistent store for tracking last-seen timestamps (Redis, SQLite, file system)
Instructions
Step 1 — Understand Why Webhooks Do Not Work
// DO NOT DO THIS — it will return 400 Bad Request
// OneNote webhooks decommissioned June 16, 2023
const subscription = await client.api("/subscriptions").post({
changeType: "updated",
notificationUrl: "https://yourapp.com/webhooks/onenote",
resource: "/me/onenote/pages", // NOT SUPPORTED
expirationDateTime: new Date(Date.now() + 3600000).toISOString(),
});
// Error: "Subscription validation request failed. Resource not found."
For comparison, these Graph resources still support webhooks: Outlook messages, calendar events, OneDrive files, Teams messages, Planner tasks. OneNote is the notable exception.
Step 2 — Implement Timestamp-Based Polling (TypeScript)
The core pattern: periodically list pages ordered by lastModifiedDateTime and compare against your stored watermark.
import { Client } from "@microsoft/microsoft-graph-client";
interface ChangeEvent {
pageId: string;
title: string;
sectionId: string;
modifiedAt: string;
changeType: "created" | "modified";
}
class OneNotePolle
Ready to use onenote-pack?
|