onenote-webhooks-events
Implement change detection for OneNote using polling and delta queries (webhooks decommissioned June 2023). Use when building real-time sync, change monitoring, or event-driven OneNote integrations. Trigger with "onenote changes", "onenote polling", "onenote sync", "onenote delta query".
What this skill does
# 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
```typescript
// 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.
```typescript
import { Client } from "@microsoft/microsoft-graph-client";
interface ChangeEvent {
pageId: string;
title: string;
sectionId: string;
modifiedAt: string;
changeType: "created" | "modified";
}
class OneNotePoller {
private watermarks: Map<string, string> = new Map(); // sectionId → ISO timestamp
private intervalMs: number;
private timer: NodeJS.Timeout | null = null;
private client: Client;
private onChanges: (events: ChangeEvent[]) => void;
constructor(
client: Client,
onChanges: (events: ChangeEvent[]) => void,
intervalSeconds: number = 30 // Poll every 30s — uses ~2 req/min per section
) {
this.client = client;
this.onChanges = onChanges;
this.intervalMs = intervalSeconds * 1000;
}
async start(sectionIds: string[]): Promise<void> {
// Initialize watermarks to "now" to avoid processing historical pages
const now = new Date().toISOString();
for (const id of sectionIds) {
this.watermarks.set(id, now);
}
this.timer = setInterval(() => this.poll(sectionIds), this.intervalMs);
console.log(`Polling ${sectionIds.length} sections every ${this.intervalMs / 1000}s`);
}
stop(): void {
if (this.timer) clearInterval(this.timer);
}
private async poll(sectionIds: string[]): Promise<void> {
const allChanges: ChangeEvent[] = [];
for (const sectionId of sectionIds) {
try {
const watermark = this.watermarks.get(sectionId)!;
const pages = await this.client.api(
`/me/onenote/sections/${sectionId}/pages`
)
.select("id,title,lastModifiedDateTime,createdDateTime")
.filter(`lastModifiedDateTime ge ${watermark}`)
.orderby("lastModifiedDateTime desc")
.top(50)
.get();
for (const page of pages.value ?? []) {
if (!page.title) continue; // Skip deleted pages (null title)
const isNew = page.createdDateTime === page.lastModifiedDateTime;
allChanges.push({
pageId: page.id,
title: page.title,
sectionId,
modifiedAt: page.lastModifiedDateTime,
changeType: isNew ? "created" : "modified",
});
}
// Advance watermark
if (pages.value?.length > 0) {
this.watermarks.set(sectionId, pages.value[0].lastModifiedDateTime);
}
} catch (err: any) {
if (err.statusCode === 429) {
const retryAfter = parseInt(err.headers?.["retry-after"] ?? "60", 10);
console.warn(`Rate limited on section ${sectionId}, backing off ${retryAfter}s`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
} else {
console.error(`Poll error for section ${sectionId}:`, err.message);
}
}
}
if (allChanges.length > 0) {
this.onChanges(allChanges);
}
}
}
```
### Step 3 — Rate Budget Planning
With a 600 requests/minute per-user limit, plan your polling capacity:
| Sections Monitored | Poll Interval | Requests/Min | Budget Used |
|---|---|---|---|
| 5 | 30s | 10 | 1.7% |
| 20 | 30s | 40 | 6.7% |
| 50 | 60s | 50 | 8.3% |
| 100 | 60s | 100 | 16.7% |
| 200 | 120s | 100 | 16.7% |
Reserve at least 50% of your rate budget for user-initiated operations (CRUD, search). If monitoring 100+ sections, increase the poll interval to 120s or use the tiered approach below.
### Step 4 — Tiered Polling (Prioritize Active Sections)
Not all sections change equally. Poll recently-active sections more frequently:
```typescript
interface TieredSection {
id: string;
tier: "hot" | "warm" | "cold";
lastChange: Date;
}
function assignTier(lastChange: Date): "hot" | "warm" | "cold" {
const ageMs = Date.now() - lastChange.getTime();
const oneHour = 3600_000;
const oneDay = 86400_000;
if (ageMs < oneHour) return "hot"; // Changed in last hour
if (ageMs < oneDay) return "warm"; // Changed in last day
return "cold"; // Stale
}
const pollIntervals = {
hot: 15_000, // 15 seconds
warm: 120_000, // 2 minutes
cold: 600_000, // 10 minutes
};
// Re-evaluate tiers after each poll cycle
```
### Step 5 — Python Async Polling
```python
import asyncio
from datetime import datetime, timezone
from msgraph import GraphServiceClient
class OneNotePoller:
def __init__(self, client: GraphServiceClient, interval_seconds: int = 30):
self.client = client
self.interval = interval_seconds
self.watermarks: dict[str, str] = {}
self._running = False
async def start(self, section_ids: list[str], callback):
"""Start polling sections for changes."""
self._running = True
now = datetime.now(timezone.utc).isoformat()
for sid in section_ids:
self.watermarks[sid] = now
while self._running:
changes = []
for sid in section_ids:
try:
pages = await self.client.me.onenote.sections.by_onenote_section_id(
sid
).pages.get()
for page in (pages.value or []):
if not page.title:
continue
modified = page.last_modified_date_time.isoformat()
if modified > self.watermarks[sid]:
changes.append({
"page_id": page.id,
"title": page.title,
"section_id": sid,
"modified_at": modified,
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.