$npx -y skills add SpillwaveSolutions/automating-mac-apps-plugin --skill automating-notesAutomates Apple Notes via JXA. Use when asked to "create notes programmatically", "automate Notes app", "JXA notes scripting", or "organize notes with automation". Covers accounts/folders/notes, HTML bodies, queries, moves, and Objective-C/UI fallbacks for Notes.app automation on
| 1 | # Automating Apple Notes (JXA-first, AppleScript discovery) |
| 2 | |
| 3 | ## Relationship to the macOS automation skill |
| 4 | - Standalone for Notes; reuse `automating-mac-apps` for permissions, shell, and Objective-C/UI scripting patterns. |
| 5 | - **PyXA Installation:** To use PyXA examples in this skill, see the installation instructions in `automating-mac-apps` skill (PyXA Installation section). |
| 6 | |
| 7 | ## Core framing |
| 8 | - Notes uses an AEOM hierarchy: Application → Accounts → Folders → Notes (with nested folders). |
| 9 | - **Load:** `automating-notes/references/notes-basics.md` for complete specifier reference. |
| 10 | |
| 11 | ## Workflow (default) |
| 12 | 1) Resolve account/folder explicitly (iCloud vs On My Mac); validate existence and permissions. |
| 13 | 2) Ensure target path exists (create folders if needed); handle creation failures gracefully. |
| 14 | 3) Create notes with explicit `name` and HTML `body`; verify creation success. |
| 15 | 4) Query/update with `.whose` filters; batch delete/move as needed; check counts before/after operations. |
| 16 | 5) For checklists/attachments, use clipboard/Objective-C scripting if dictionary support is insufficient; test fallbacks. |
| 17 | 6) For meeting/people workflows, file notes under `meetings/<company>/<date>-<meeting-title>` and `people/<first>-<last>/...`; validate final structure. |
| 18 | |
| 19 | ## Quickstart (ensure path + create) |
| 20 | |
| 21 | **JXA (Legacy):** |
| 22 | ```javascript |
| 23 | const Notes = Application("Notes"); |
| 24 | |
| 25 | // Ensure folder path exists, creating intermediate folders as needed |
| 26 | function ensurePath(acc, path) { |
| 27 | const parts = path.split("/").filter(Boolean); |
| 28 | let container = acc; |
| 29 | parts.forEach(seg => { |
| 30 | let f; try { |
| 31 | f = container.folders.byName(seg); |
| 32 | f.name(); // Verify access |
| 33 | } catch (e) { |
| 34 | // Folder doesn't exist, create it |
| 35 | f = Notes.Folder({ name: seg }); |
| 36 | container.folders.push(f); |
| 37 | } |
| 38 | container = f; |
| 39 | }); |
| 40 | return container; |
| 41 | } |
| 42 | |
| 43 | try { |
| 44 | // Get iCloud account and ensure meeting folder exists |
| 45 | const acc = Notes.accounts.byName("iCloud"); |
| 46 | const folder = ensurePath(acc, "meetings/Acme/2024-07-01-Review"); |
| 47 | |
| 48 | // Create new note in the folder |
| 49 | folder.notes.push(Notes.Note({ |
| 50 | name: "Client Review", |
| 51 | body: "<h1>Client Review</h1><div>Agenda...</div>" |
| 52 | })); |
| 53 | console.log("Note created successfully"); |
| 54 | } catch (e) { |
| 55 | console.error("Failed to create note: " + e.message); |
| 56 | } |
| 57 | ``` |
| 58 | |
| 59 | **PyXA (Recommended Modern Approach):** |
| 60 | ```python |
| 61 | import PyXA |
| 62 | |
| 63 | notes = PyXA.Notes() |
| 64 | |
| 65 | def ensure_path(account, path): |
| 66 | """Ensure folder path exists, creating intermediate folders as needed""" |
| 67 | parts = [p for p in path.split("/") if p] # Filter empty parts |
| 68 | container = account |
| 69 | |
| 70 | for part in parts: |
| 71 | try: |
| 72 | # Try to find existing folder |
| 73 | folder = container.folders().by_name(part) |
| 74 | folder.name() # Verify access |
| 75 | except: |
| 76 | # Folder doesn't exist, create it |
| 77 | folder = notes.make("folder", {"name": part}) |
| 78 | container.folders().push(folder) |
| 79 | |
| 80 | container = folder |
| 81 | |
| 82 | return container |
| 83 | |
| 84 | try: |
| 85 | # Get iCloud account |
| 86 | account = notes.accounts().by_name("iCloud") |
| 87 | |
| 88 | # Ensure meeting folder exists |
| 89 | folder = ensure_path(account, "meetings/Acme/2024-07-01-Review") |
| 90 | |
| 91 | # Create new note in the folder |
| 92 | note = folder.notes().push({ |
| 93 | "name": "Client Review", |
| 94 | "body": "<h1>Client Review</h1><div>Agenda...</div>" |
| 95 | }) |
| 96 | |
| 97 | print("Note created successfully") |
| 98 | |
| 99 | except Exception as e: |
| 100 | print(f"Failed to create note: {e}") |
| 101 | ``` |
| 102 | |
| 103 | **PyObjC with Scripting Bridge:** |
| 104 | ```python |
| 105 | from ScriptingBridge import SBApplication |
| 106 | |
| 107 | notes = SBApplication.applicationWithBundleIdentifier_("com.apple.Notes") |
| 108 | |
| 109 | def ensure_path(account, path): |
| 110 | """Ensure folder path exists, creating intermediate folders as needed""" |
| 111 | parts = [p for p in path.split("/") if p] |
| 112 | container = account |
| 113 | |
| 114 | for part in parts: |
| 115 | try: |
| 116 | folder = container.folders().objectWithName_(part) |
| 117 | folder.name() # Verify access |
| 118 | except: |
| 119 | # Create new folder |
| 120 | folder = notes.classForScriptingClass_("folder").alloc().init() |
| 121 | folder.setName_(part) |
| 122 | container.folders().addObject_(folder) |
| 123 | |
| 124 | container = folder |
| 125 | |
| 126 | return container |
| 127 | |
| 128 | try: |
| 129 | # Get iCloud account |
| 130 | accounts = notes.accounts() |
| 131 | account = None |
| 132 | for acc in accounts: |
| 133 | if acc.name() == "iCloud": |
| 134 | account = acc |
| 135 | break |
| 136 | |
| 137 | if account: |
| 138 | # Ensure meeting folder exists |
| 139 | folder = ensure_path(account, "meetings/Acme/2024-07-01-Review") |
| 140 | |
| 141 | # Create new note |
| 142 | note = notes.classForScriptingClass_("note").alloc().init() |
| 143 | note.setName_("Client |