From apple-notes-pack
Optimize Apple Notes automation performance for large note collections. Trigger: "apple notes performance".
npx claudepluginhub flight505/skill-forge --plugin apple-notes-packThis skill is limited to using the following tools:
Apple Notes automation performance degrades linearly with note count because JXA loads all note objects into memory when you access a collection. A vault with 10,000+ notes can take 30+ seconds for a simple list operation. The primary bottleneck is the Apple Events bridge between your script and Notes.app — every property access (name, body, date) is a separate IPC call. This guide covers cachi...
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Apple Notes automation performance degrades linearly with note count because JXA loads all note objects into memory when you access a collection. A vault with 10,000+ notes can take 30+ seconds for a simple list operation. The primary bottleneck is the Apple Events bridge between your script and Notes.app — every property access (name, body, date) is a separate IPC call. This guide covers caching strategies, incremental sync, batch optimization, and architectural patterns to keep automation responsive at scale.
| Operation | 100 notes | 1,000 notes | 10,000 notes |
|---|---|---|---|
| List all (names only) | ~0.5s | ~3s | ~30s |
Search by name (.whose()) | ~0.3s | ~2s | ~20s |
| Full-text search (body scan) | ~1s | ~8s | ~80s |
| Create single note | ~0.2s | ~0.2s | ~0.2s |
| Export all to JSON | ~1s | ~10s | ~100s |
Count notes only (.length) | ~0.1s | ~0.3s | ~1s |
// BAD: Each property access is a separate Apple Event IPC call
const Notes = Application("Notes");
const allNotes = Notes.defaultAccount.notes();
allNotes.forEach(n => {
console.log(n.name()); // IPC call 1
console.log(n.body()); // IPC call 2
console.log(n.modificationDate()); // IPC call 3
});
// With 1000 notes = 3000 IPC calls
// GOOD: Batch extract in a single JXA evaluation
const data = Notes.defaultAccount.notes().map(n => ({
title: n.name(),
modified: n.modificationDate().toISOString(),
}));
// Single JXA evaluation, much faster for bulk reads
#!/bin/bash
# Export notes to SQLite for fast local queries
DB="$HOME/.notes-cache.db"
sqlite3 "$DB" "CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY, title TEXT, body TEXT, folder TEXT,
created TEXT, modified TEXT, indexed_at TEXT
);"
osascript -l JavaScript -e '
const Notes = Application("Notes");
Notes.defaultAccount.notes().map(n => JSON.stringify({
id: n.id(), title: n.name(), body: n.plaintext(),
folder: n.container().name(),
created: n.creationDate().toISOString(),
modified: n.modificationDate().toISOString()
})).join("\n");
' | while IFS= read -r line; do
echo "$line" | jq -r '[.id, .title, .body, .folder, .created, .modified, now | todate] | @csv' \
| sqlite3 "$DB" ".import /dev/stdin notes"
done 2>/dev/null
# Now query locally (instant)
sqlite3 "$DB" "SELECT title FROM notes WHERE body LIKE '%project%' ORDER BY modified DESC LIMIT 10;"
// src/sync/incremental.ts
import { execSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
const LAST_SYNC_FILE = ".notes-last-sync";
function getLastSync(): Date {
try { return new Date(readFileSync(LAST_SYNC_FILE, "utf8").trim()); }
catch { return new Date(0); } // First run: sync everything
}
function incrementalSync(): void {
const lastSync = getLastSync();
const allNotes = JSON.parse(execSync(
`osascript -l JavaScript -e 'JSON.stringify(Application("Notes").defaultAccount.notes().map(n => ({id: n.id(), title: n.name(), modified: n.modificationDate().toISOString()})))'`,
{ encoding: "utf8" }
));
const changed = allNotes.filter((n: any) => new Date(n.modified) > lastSync);
console.log(`${changed.length} notes modified since ${lastSync.toISOString()}`);
// Process only changed notes (fetch full body only for these)
for (const note of changed) {
console.log(`Syncing: ${note.title}`);
// ... process individual note
}
writeFileSync(LAST_SYNC_FILE, new Date().toISOString());
}
.whose() for Filtered Queries// .whose() pushes filtering to Notes.app (faster than client-side filter)
const Notes = Application("Notes");
// Faster than loading all notes and filtering in JS
const recentNotes = Notes.defaultAccount.notes.whose({
_match: [ObjectSpecifier().modificationDate, ">", new Date(Date.now() - 86400000)]
});
// Search by name (case-insensitive)
const matches = Notes.defaultAccount.notes.whose({
name: { _contains: "project" }
});
| Issue | Cause | Solution |
|---|---|---|
| Script hangs for >60s | Too many notes with body() access | Use .length first to assess scale; use cache for large vaults |
| Memory spike during export | All note bodies loaded into JXA runtime | Process in batches; stream to file instead of building array |
| SQLite cache stale | Forgot to re-sync after edits | Run incremental sync on schedule via launchd |
.whose() returns wrong results | Complex predicates not supported in JXA | Fall back to full load + JS filter for complex queries |
| iCloud sync slows writes | Each write triggers sync | Batch writes with 1s delay; use "On My Mac" for bulk import |
For handling rate limits during bulk operations, see apple-notes-rate-limits. For monitoring performance trends, see apple-notes-observability.