Apple Notes SDK Patterns
Overview
Production patterns for Apple Notes automation: JXA wrapper class, error handling, batch operations, and cross-account support.
Prerequisites
- A scoped account/folder configuration resolved outside the JXA source string.
- A safe process invocation boundary that passes source and data without shell interpolation.
- Durable idempotency tracking for writes and a synthetic local test corpus.
Instructions
- Treat names, bodies, queries, and folder identifiers as data—not template fragments or shell arguments.
- Resolve only explicitly configured accounts and folders, and fail if the target is absent rather than creating it implicitly.
- Keep list and search results scoped and minimize returned fields; never log note bodies by default.
- Serialize mutations, record an opaque idempotency key before the call, and reconcile timeouts before retrying.
Procedure
Step 1: JXA Client Wrapper (Node.js)
// src/notes-client.ts
import { execSync } from "child_process";
class AppleNotesClient {
private runJxa(script: string): string {
const escaped = script.replace(/'/g, "\\'");
return execSync(`osascript -l JavaScript -e '${escaped}'`, {
encoding: "utf8",
timeout: 30000,
}).trim();
}
listNotes(folder?: string, limit: number = 50): Array<{ id: string; title: string; modified: string }> {
const script = folder
? `const Notes = Application("Notes"); const f = Notes.defaultAccount.folders().find(f => f.name() === "${folder}"); (f ? f.notes() : []).slice(0, ${limit}).map(n => JSON.stringify({id: n.id(), title: n.name(), modified: n.modificationDate().toISOString()})).join("\\n")`
: `const Notes = Application("Notes"); Notes.defaultAccount.notes().slice(0, ${limit}).map(n => JSON.stringify({id: n.id(), title: n.name(), modified: n.modificationDate().toISOString()})).join("\\n")`;
return this.runJxa(script).split("\n").filter(Boolean).map(l => JSON.parse(l));
}
createNote(title: string, body: string, folder?: string): string {
const folderPart = folder
? `let f = account.folders().find(f => f.name() === "${folder}"); if (!f) { f = Notes.Folder({name: "${folder}"}); account.folders.push(f); }`
: "let f = account.folders[0];";
return this.runJxa(`
const Notes = Application("Notes");
const account = Notes.defaultAccount;
${folderPart}
const note = Notes.Note({name: ${JSON.stringify(title)}, body: ${JSON.stringify(body)}});
f.notes.push(note);
note.id();
`);
}
searchNotes(query: string): Array<{ title: string; folder: string }> {
const result = this.runJxa(`
const Notes = Application("Notes");
const q = "${query}".toLowerCase();
Notes.defaultAccount.notes().filter(n =>
n.name().toLowerCase().includes(q) || n.body().toLowerCase().includes(q)
).slice(0, 20).map(n => JSON.stringify({title: n.name(), folder: n.container().name()})).join("\\n");
`);
return result.split("\n").filter(Boolean).map(l => JSON.parse(l));
}
}
export { AppleNotesClient };
Step 2: Batch Operations with Throttling
async function batchCreateNotes(
client: AppleNotesClient,
notes: Array<{ title: string; body: string; folder?: string }>,
delayMs: number = 500,
): Promise<string[]> {
const ids: string[] = [];
for (const note of notes) {
const id = client.createNote(note.title, note.body, note.folder);
ids.push(id);
await new Promise(r => setTimeout(r, delayMs));
}
return ids;
}
Output
- Type-safe JXA client wrapper for Node.js
- List, create, search operations via osascript
- Batch operations with throttling
Error Handling
If source generation, JSON parsing, or an Apple Event call fails, retain the opaque operation key and return a redacted error category. Do not retry a create until the scoped target has been checked for the prior operation. Reject unconfigured folders, over-limit requests, and any input that would require shell-string interpolation.
Examples
For a synthetic import, resolve the test folder from reviewed configuration, enqueue one record with a source-record key, and verify its returned opaque identifier before advancing. For production, use a process API with argument arrays or stdin rather than the illustrative interpolated execSync command strings above; keep the adapter implementation in a reviewed module.
Resources
- Mac Automation Scripting Guide
- JXA Examples