
Automating Notes
- 23 installs
- 39 repo stars
- Updated January 14, 2026
- spillwavesolutions/automating-mac-apps-plugin
Helps with productivity & planning tasks during AI-assisted development.
About
automating-notes is a Claude Code skill for productivity & planning. It helps solo builders move faster with AI-assisted coding.
- automating-notes
- Productivity & Planning
- AI-coding skill
Automating Notes by the numbers
- 23 all-time installs (skills.sh)
- Ranked #1,989 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/automating-mac-apps-plugin --skill automating-notesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 39 |
| Last updated | January 14, 2026 |
| Repository | spillwavesolutions/automating-mac-apps-plugin ↗ |
What it does
Helps with productivity & planning tasks during AI-assisted development.
Files
Automating Apple Notes (JXA-first, AppleScript discovery)
Relationship to the macOS automation skill
- Standalone for Notes; reuse
automating-mac-appsfor permissions, shell, and Objective-C/UI scripting patterns. - PyXA Installation: To use PyXA examples in this skill, see the installation instructions in
automating-mac-appsskill (PyXA Installation section).
Core framing
- Notes uses an AEOM hierarchy: Application → Accounts → Folders → Notes (with nested folders).
- Load:
automating-notes/references/notes-basics.mdfor complete specifier reference.
Workflow (default)
1) Resolve account/folder explicitly (iCloud vs On My Mac); validate existence and permissions. 2) Ensure target path exists (create folders if needed); handle creation failures gracefully. 3) Create notes with explicit name and HTML body; verify creation success. 4) Query/update with .whose filters; batch delete/move as needed; check counts before/after operations. 5) For checklists/attachments, use clipboard/Objective-C scripting if dictionary support is insufficient; test fallbacks. 6) For meeting/people workflows, file notes under meetings/<company>/<date>-<meeting-title> and people/<first>-<last>/...; validate final structure.
Quickstart (ensure path + create)
JXA (Legacy):
const Notes = Application("Notes");
// Ensure folder path exists, creating intermediate folders as needed
function ensurePath(acc, path) {
const parts = path.split("/").filter(Boolean);
let container = acc;
parts.forEach(seg => {
let f; try {
f = container.folders.byName(seg);
f.name(); // Verify access
} catch (e) {
// Folder doesn't exist, create it
f = Notes.Folder({ name: seg });
container.folders.push(f);
}
container = f;
});
return container;
}
try {
// Get iCloud account and ensure meeting folder exists
const acc = Notes.accounts.byName("iCloud");
const folder = ensurePath(acc, "meetings/Acme/2024-07-01-Review");
// Create new note in the folder
folder.notes.push(Notes.Note({
name: "Client Review",
body: "<h1>Client Review</h1><div>Agenda...</div>"
}));
console.log("Note created successfully");
} catch (e) {
console.error("Failed to create note: " + e.message);
}PyXA (Recommended Modern Approach):
import PyXA
notes = PyXA.Notes()
def ensure_path(account, path):
"""Ensure folder path exists, creating intermediate folders as needed"""
parts = [p for p in path.split("/") if p] # Filter empty parts
container = account
for part in parts:
try:
# Try to find existing folder
folder = container.folders().by_name(part)
folder.name() # Verify access
except:
# Folder doesn't exist, create it
folder = notes.make("folder", {"name": part})
container.folders().push(folder)
container = folder
return container
try:
# Get iCloud account
account = notes.accounts().by_name("iCloud")
# Ensure meeting folder exists
folder = ensure_path(account, "meetings/Acme/2024-07-01-Review")
# Create new note in the folder
note = folder.notes().push({
"name": "Client Review",
"body": "<h1>Client Review</h1><div>Agenda...</div>"
})
print("Note created successfully")
except Exception as e:
print(f"Failed to create note: {e}")PyObjC with Scripting Bridge:
from ScriptingBridge import SBApplication
notes = SBApplication.applicationWithBundleIdentifier_("com.apple.Notes")
def ensure_path(account, path):
"""Ensure folder path exists, creating intermediate folders as needed"""
parts = [p for p in path.split("/") if p]
container = account
for part in parts:
try:
folder = container.folders().objectWithName_(part)
folder.name() # Verify access
except:
# Create new folder
folder = notes.classForScriptingClass_("folder").alloc().init()
folder.setName_(part)
container.folders().addObject_(folder)
container = folder
return container
try:
# Get iCloud account
accounts = notes.accounts()
account = None
for acc in accounts:
if acc.name() == "iCloud":
account = acc
break
if account:
# Ensure meeting folder exists
folder = ensure_path(account, "meetings/Acme/2024-07-01-Review")
# Create new note
note = notes.classForScriptingClass_("note").alloc().init()
note.setName_("Client Review")
note.setBody_("<h1>Client Review</h1><div>Agenda...</div>")
folder.notes().addObject_(note)
print("Note created successfully")
else:
print("iCloud account not found")
except Exception as e:
print(f"Failed to create note: {e}")Validation Checklist
- [ ] Account access works (iCloud vs On My Mac)
- [ ] Folder creation and path resolution succeeds
- [ ] Note creation with valid HTML body completes
- [ ] Note appears in Notes UI
- [ ]
.whosequeries return expected results - [ ] Error handling covers missing accounts/folders
HTML & fallbacks
- Allowed tags:
<h1>-<h3>,<b>,<i>,<u>,<ul>/<ol>/<li>,<div>/<p>/<br>,<a>. - Security: Always sanitize HTML input; avoid
<script>,<style>, or event handlers to prevent XSS in rendered notes. - Checklists/attachments: Objective-C/clipboard fallback (Cmd+Shift+L for checklist, paste image via NSPasteboard + System Events).
- Append helper: replace
</body>with extra HTML, or append when missing; validate HTML structure post-modification.
When Not to Use
- Cross-platform note taking (use Notion API, Obsidian, or Markdown files)
- iCloud sync operations requiring status feedback (limited API support)
- Non-macOS platforms
- Rich formatting beyond supported HTML tags
- Collaborative editing workflows (no multi-user support)
What to load
- Basics & specifiers:
automating-notes/references/notes-basics.md - Recipes (create/move/query/ensure path/checklists):
automating-notes/references/notes-recipes.md - Advanced (HTML body rules, attachments/UI, JSON import/export, ObjC bridge):
automating-notes/references/notes-advanced.md - Dictionary/type map:
automating-notes/references/notes-dictionary.md - PyXA API Reference (complete class/method docs):
automating-notes/references/notes-pyxa-api-reference.md
Notes Advanced (HTML, attachments, ObjC/UI)
HTML body rules
- Use supported tags:
<h1>-<h3>,<b>,<i>,<u>,<ul>/<ol>/<li>,<div>/<p>/<br>,<a>. - Notes wraps paragraphs in
<div>; keep bodies well-formed. Avoid<script>/<style>(stripped).
Append content safely
function appendBody(note, html) {
const body = note.body();
const replacement = body.includes("</body>") ?
body.replace("</body>", `${html}</body>`) :
body + html;
note.body = replacement;
}Attachments / images workaround
- Direct attachment creation via JXA is limited. Use clipboard + UI scripting:
1) Load file/image to NSPasteboard via ObjC. 2) Activate Notes, focus note, send Cmd+V via System Events.
- Requires Accessibility + FDA permissions.
JSON import/export
- Import: read JSON via
JSON.parseand create folders/notes to mirror structure. - Export: iterate notes, collect
{name, body, creationDate, id}, and write to file via ObjCNSString/NSFileManager.
Move and account boundaries
- Use
Notes.move(spec, { to: folder }). Crossing accounts effectively copies and may change IDs.
Performance
- Use
.whoseto filter server-side; avoid property access in tight loops. - Batch deletes/moves by calling commands on specifiers.
Error handling
- -1728 (can't get) → invalid specifier; check existence.
- -1700 (type mismatch) → wrong type; ensure strings/dates are correct.
- -10000 (handler failed) → retry or restart Notes; check permissions.
Notes JXA Basics
Initialize
const Notes = Application("Notes");
Notes.includeStandardAdditions = true;Hierarchy & specifiers
- Application → Accounts → Folders → Notes (folders can nest).
Notes.accounts.byName("iCloud")→ account specifier.account.folders.byName("Work")→ folder specifier.folder.notes→ notes specifier.- Use methods to read:
note.name(),note.body().
Create note (constructor + push)
const acc = Notes.accounts.byName("iCloud");
const f = acc.folders.byName("Work");
const n = Notes.Note({
name: "Meeting Notes",
body: "<h1>Team Sync</h1><div>Decisions...</div>"
});
f.notes.push(n);Ensure folder path
- Iterate path segments, check
folders.byName, create if missing (see recipes).
Move note
Notes.move(noteSpecifier, { to: targetFolder });Query
- Server-side filter:
folder.notes.whose({ name: { _contains: "Q3" } }). - Access properties on specifier to batch-read:
.name(),.id().
Notes Dictionary & Types
Core classes
Application("Notes")- Elements:
accounts,folders,notes Account→ properties:name,id; elements:foldersFolder→ properties:name,id,container; elements:folders,notesNote→ properties:name,body(HTML),creationDate,modificationDate,id,container
Commands
- Create:
Notes.Note({...})+folder.notes.push(obj)orNotes.make({ new: 'note', at: folder, withProperties: {...} }) - Move:
Notes.move(noteSpecifier, { to: folderSpecifier }) - Delete:
note.delete()orfolder.notes.whose(...).delete()
Filters
.whosewith operators:_contains,_beginsWith,_endsWith,_greaterThan,_lessThan,_equals,_not,_and,_or- Examples:
app.notes.whose({ name: { _contains: "Draft" } })folder.notes.whose({ creationDate: { _greaterThan: someDate } })
Reading/writing
- Read with methods:
note.name(),note.body(),note.id() - Write with assignment:
note.body = "<h1>Title</h1>...";
PyXA Notes Module API Reference
New in PyXA version 0.0.1 - Control macOS Notes.app using JXA-like syntax from Python.
This reference documents all classes, methods, properties, and enums in the PyXA Notes module. For practical examples and usage patterns, see notes-recipes.md.
Contents
- Class Hierarchy
- XANotesApplication
- XANote
- XANotesFolder
- XANotesAttachment
- XANotesAccount
- XANotesDocument
- XANotesWindow
- List Classes
- Enumerations
- Quick Reference Tables
---
Class Hierarchy
XAObject
├── XANotesApplication (XASBApplication, XACanOpenPath, XACanPrintPath)
│ ├── XANote (XAClipboardCodable, XAShowable, XADeletable)
│ │ └── XANotesAttachment (XAClipboardCodable)
│ ├── XANotesFolder (XAClipboardCodable)
│ ├── XANotesAccount (XAClipboardCodable)
│ ├── XANotesDocument (XAClipboardCodable)
│ └── XANotesWindow (XASBWindow)
└── List Classes
├── XANoteList (XAList, XAClipboardCodable)
├── XANotesFolderList (XAList, XAClipboardCodable)
├── XANotesAttachmentList (XAList, XAClipboardCodable)
├── XANotesAccountList (XAList, XAClipboardCodable)
└── XANotesDocumentList (XAList, XAClipboardCodable)---
XANotesApplication
Bases: XASBApplication, XACanOpenPath, XACanPrintPath
Main entry point for interacting with Notes.app.
Properties
| Property | Type | Description |
|---|---|---|
default_account | XANotesAccount | The default account for new notes |
frontmost | bool | Whether Notes is the frontmost application |
name | str | Application name |
selection | XANoteList | Currently selected notes |
version | str | Application version |
Methods
accounts(filter=None) -> XANotesAccountList
Returns a list of accounts matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
import PyXA
notes = PyXA.Application("Notes")
accounts = notes.accounts()
for acc in accounts:
print(acc.name)attachments(filter=None) -> XANotesAttachmentList
Returns all attachments across all notes matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
documents(filter=None) -> XANotesDocumentList
Returns a list of documents matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
folders(filter=None) -> XANotesFolderList
Returns all folders matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
folders = notes.folders()
print(folders.name())
# ['Notes', 'Recently Deleted', 'Projects', ...]notes(filter=None) -> XANoteList
Returns all notes matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
all_notes = notes.notes()
# Filter by name
project_notes = notes.notes({"name": "Project"})new_note(name='New Note', body='', folder=None) -> XANote
Creates a new note.
Parameters:
name(str) - Title of the note (default: 'New Note')body(str) - HTML body content (default: '')folder(XANotesFolder | None) - Target folder (default: default folder)
Returns: The newly created note
Example:
note = notes.new_note(
name="Meeting Notes",
body="<h1>Meeting</h1><p>Agenda items...</p>"
)new_folder(name='New Folder', account=None) -> XANotesFolder
Creates a new folder.
Parameters:
name(str) - Name of the folder (default: 'New Folder')account(XANotesAccount | None) - Target account (default: default account)
Returns: The newly created folder
Example:
folder = notes.new_folder(name="Projects")make(specifier, properties=None, data=None) -> XAObject
Creates a new element without adding to any list. Use XAList.push() to add.
Parameters:
specifier(str | ObjectType) - Class name to create ('note', 'folder')properties(dict) - Properties for the objectdata(Any) - Initialization data
Example:
note = notes.make("note", {"name": "New Note", "body": "<p>Content</p>"})
folder.notes().push(note)open(file_ref) -> XANote
Opens a file as a note.
Parameters:
file_ref(str | XAPath) - Path to file to open
Returns: The opened note
---
XANote
Bases: XAObject, XAClipboardCodable, XAShowable, XADeletable
Represents an individual note in Notes.app.
Properties
| Property | Type | Description |
|---|---|---|
body | str | HTML content of the note |
container | XANotesFolder | Folder containing this note |
creation_date | datetime | When the note was created |
id | str | Unique identifier |
modification_date | datetime | When the note was last modified |
name | str | Title of the note |
password_protected | bool | Whether the note is locked |
plaintext | str | Plain text content (no HTML) |
shared | bool | Whether the note is shared |
Methods
attachments(filter=None) -> XANotesAttachmentList
Returns attachments in this note matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
note = notes.notes()[0]
attachments = note.attachments()
for att in attachments:
print(att.name)move_to(folder) -> XANote
Moves the note to a different folder.
Parameters:
folder(XANotesFolder) - Target folder
Returns: Self, for method chaining
Example:
archive = notes.folders().by_name("Archive")
note.move_to(archive)show() -> XANote
Shows the note in the Notes main window.
Returns: Self, for method chaining
show_separately() -> XANote
Opens the note in its own separate window.
Returns: Self, for method chaining
Example:
note.show_separately() # Opens in new windowdelete()
Deletes the note (moves to Recently Deleted).
Example:
note.delete()get_clipboard_representation() -> str
Returns a string representation suitable for the clipboard.
Returns: Plain text content of the note
---
XANotesFolder
Bases: XAObject, XAClipboardCodable
Represents a folder in Notes.app.
Properties
| Property | Type | Description |
|---|---|---|
container | XANotesAccount | Account containing this folder |
id | str | Unique identifier |
name | str | Name of the folder |
shared | bool | Whether the folder is shared |
Methods
notes(filter=None) -> XANoteList
Returns notes in this folder matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
folder = notes.folders().by_name("Projects")
project_notes = folder.notes()folders(filter=None) -> XANotesFolderList
Returns subfolders matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
subfolders = folder.folders()move_to(destination) -> XANotesFolder
Moves the folder to a new location.
Parameters:
destination(XANotesFolder | XANotesAccount) - Target location
Returns: Self, for method chaining
show() -> XANotesFolder
Shows the folder in the Notes main window.
Returns: Self, for method chaining
delete()
Deletes the folder and its contents.
get_clipboard_representation() -> str
Returns a string representation suitable for the clipboard.
Returns: Folder name
---
XANotesAttachment
Bases: XAObject, XAClipboardCodable
Represents an attachment in a note.
Properties
| Property | Type | Description |
|---|---|---|
container | XANote | Note containing this attachment |
content_identifier | str | Content identifier for the attachment |
creation_date | datetime | When the attachment was added |
id | str | Unique identifier |
modification_date | datetime | When the attachment was last modified |
name | str | Filename of the attachment |
shared | bool | Whether the attachment is shared |
url | `XAURL | None` |
Methods
save(directory) -> XANotesAttachment
Saves the attachment to a directory.
Parameters:
directory(str | XAPath) - Target directory path
Returns: Self, for method chaining
Example:
attachment = note.attachments()[0]
attachment.save("/Users/me/Downloads")show() -> XANotesAttachment
Shows the attachment in the Notes main window.
Returns: Self, for method chaining
show_separately() -> XANotesAttachment
Opens the attachment in its own window.
Returns: Self, for method chaining
delete()
Deletes the attachment from the note.
get_clipboard_representation() -> list[NSURL | str]
Returns a clipboard representation of the attachment.
Returns: List containing URL and/or name
---
XANotesAccount
Bases: XAObject, XAClipboardCodable
Represents an account (iCloud, On My Mac, etc.) in Notes.app.
Properties
| Property | Type | Description |
|---|---|---|
default_folder | XANotesFolder | Default folder for new notes |
id | str | Unique identifier |
name | str | Account name (e.g., "iCloud") |
upgraded | bool | Whether the account has been upgraded |
Methods
folders(filter=None) -> XANotesFolderList
Returns folders in this account matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
icloud = notes.accounts().by_name("iCloud")
icloud_folders = icloud.folders()notes(filter=None) -> XANoteList
Returns all notes in this account matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
show() -> XANotesAccount
Shows the account in the Notes sidebar.
Returns: Self, for method chaining
get_clipboard_representation() -> str
Returns a string representation suitable for the clipboard.
Returns: Account name
---
XANotesDocument
Bases: XAObject, XAClipboardCodable
Represents a Notes document.
Properties
| Property | Type | Description |
|---|---|---|
file | str | File location on disk |
modified | bool | Whether document has unsaved changes |
name | str | Document name |
Methods
get_clipboard_representation() -> str
Returns a string representation suitable for the clipboard.
Returns: Document name
---
XANotesWindow
Bases: XASBWindow
Represents a Notes application window.
Properties
| Property | Type | Description |
|---|---|---|
document | XANotesDocument | Document displayed in the window |
---
List Classes
PyXA provides list wrapper classes with fast enumeration and bulk property access.
XANoteList
Wrapper for lists of notes with bulk operations.
Attribute Methods (return lists):
notes_list = notes.notes()
notes_list.body() # -> list[str]
notes_list.container() # -> XANotesFolderList
notes_list.creation_date() # -> list[datetime]
notes_list.id() # -> list[str]
notes_list.modification_date() # -> list[datetime]
notes_list.name() # -> list[str]
notes_list.password_protected()# -> list[bool]
notes_list.plaintext() # -> list[str]
notes_list.shared() # -> list[bool]
notes_list.attachments() # -> XANotesAttachmentListFilter Methods:
notes_list.by_body(body)
notes_list.by_container(container)
notes_list.by_creation_date(creation_date)
notes_list.by_id(id)
notes_list.by_modification_date(modification_date)
notes_list.by_name(name)
notes_list.by_password_protected(password_protected)
notes_list.by_plaintext(plaintext)
notes_list.by_shared(shared)Action Methods:
notes_list.show_separately() # -> XANoteList
notes_list.get_clipboard_representation() # -> list[str]XANotesFolderList
Wrapper for lists of folders with bulk operations.
Attribute Methods:
folders = notes.folders()
folders.container() # -> XANotesAccountList
folders.folders() # -> XANotesFolderList (subfolders)
folders.id() # -> list[str]
folders.name() # -> list[str]
folders.notes() # -> XANoteList
folders.shared() # -> list[bool]Filter Methods:
folders.by_container(container)
folders.by_id(id)
folders.by_name(name)
folders.by_shared(shared)Action Methods:
folders.get_clipboard_representation() # -> list[str]XANotesAttachmentList
Wrapper for lists of attachments with bulk operations.
Attribute Methods:
attachments = note.attachments()
attachments.container() # -> XANoteList
attachments.content_identifier() # -> list[str]
attachments.creation_date() # -> list[datetime]
attachments.id() # -> list[str]
attachments.modification_date() # -> list[datetime]
attachments.name() # -> list[str]
attachments.shared() # -> list[bool]
attachments.url() # -> list[XAURL | None]Filter Methods:
attachments.by_container(container)
attachments.by_content_identifier(content_identifier)
attachments.by_creation_date(creation_date)
attachments.by_id(id)
attachments.by_modification_date(modification_date)
attachments.by_name(name)
attachments.by_shared(shared)
attachments.by_url(url)Action Methods:
attachments.save(directory) # -> XANotesAttachmentListXANotesAccountList
Wrapper for lists of accounts with bulk operations.
Attribute Methods:
accounts = notes.accounts()
accounts.default_folder() # -> XANotesFolderList
accounts.folders() # -> XANotesFolderList
accounts.id() # -> list[str]
accounts.name() # -> list[str]
accounts.notes() # -> XANoteList
accounts.upgraded() # -> list[bool]Filter Methods:
accounts.by_default_folder(default_folder)
accounts.by_id(id)
accounts.by_name(name)
accounts.by_upgraded(upgraded)Action Methods:
accounts.get_clipboard_representation() # -> list[str]XANotesDocumentList
Wrapper for lists of documents with bulk operations.
Attribute Methods:
docs = notes.documents()
docs.file() # -> list[str]
docs.modified() # -> list[bool]
docs.name() # -> list[str]Filter Methods:
docs.by_file(file)
docs.by_modified(modified)
docs.by_name(name)Action Methods:
docs.get_clipboard_representation() # -> list[str]---
Enumerations
FileFormat
File format options.
| Value | Raw Value | Description |
|---|---|---|
NATIVE | 1769235821 | Native Notes format |
ObjectType
Creatable object types.
| Value | Description |
|---|---|
ACCOUNT | Notes account |
ATTACHMENT | Note attachment |
FOLDER | Notes folder |
NOTE | Individual note |
---
Quick Reference Tables
Common Operations
| Task | Code |
|---|---|
| Get Notes app | notes = PyXA.Application("Notes") |
| Get all notes | all_notes = notes.notes() |
| Get note by name | note = notes.notes().by_name("Title") |
| Create note | note = notes.new_note(name="Title", body="<p>Content</p>") |
| Create folder | folder = notes.new_folder(name="Projects") |
| Get iCloud account | icloud = notes.accounts().by_name("iCloud") |
| Get folder notes | folder_notes = folder.notes() |
| Move note | note.move_to(target_folder) |
| Delete note | note.delete() |
| Show note | note.show() |
| Open in new window | note.show_separately() |
| Save attachment | attachment.save("/path/to/directory") |
| Get plain text | text = note.plaintext |
| Get HTML body | html = note.body |
Property Access Patterns
# Single object
note = notes.notes()[0]
print(note.name)
print(note.body)
print(note.creation_date)
# Bulk access on lists
all_notes = notes.notes()
print(all_notes.name()) # Returns list[str]
print(all_notes.creation_date()) # Returns list[datetime]
print(all_notes.shared()) # Returns list[bool]
# Filtering
shared_notes = all_notes.by_shared(True)
recent = all_notes.by_modification_date(datetime.now())Account and Folder Navigation
# Navigate hierarchy: Account -> Folders -> Notes
icloud = notes.accounts().by_name("iCloud")
projects = icloud.folders().by_name("Projects")
project_notes = projects.notes()
# Get subfolders
subfolders = projects.folders()
# Get all notes in account
all_icloud_notes = icloud.notes()Working with Attachments
# Get attachments from a note
note = notes.notes()[0]
attachments = note.attachments()
# Save all attachments
for att in attachments:
att.save("/Downloads")
# Bulk save
attachments.save("/Downloads")
# Filter by name
images = attachments.by_name(".png")---
See Also
- PyXA Notes Documentation - Official PyXA documentation
- notes-recipes.md - Practical usage examples
- notes-basics.md - JXA fundamentals
- notes-advanced.md - Advanced patterns and HTML handling
- notes-dictionary.md - Complete property reference
Notes JXA Recipes
Ensure folder path (account-based)
JXA:
function ensurePath(acc, path) {
const parts = path.split("/").filter(Boolean);
let container = acc;
parts.forEach(seg => {
let f;
try { f = container.folders.byName(seg); f.name(); }
catch (e) {
f = Notes.Folder({ name: seg });
container.folders.push(f);
}
container = f;
});
return container;
}
const acc = Notes.accounts.byName("iCloud");
const meetingsFolder = ensurePath(acc, "meetings/Acme/2024-07-01-Review");PyXA:
import PyXA
def ensure_path(account, path):
"""Ensure folder path exists, creating intermediate folders as needed"""
parts = [p for p in path.split("/") if p] # Filter empty parts
container = account
for part in parts:
try:
# Try to find existing folder
folder = container.folders().by_name(part)
folder.name() # Verify access
except:
# Folder doesn't exist, create it
folder = PyXA.Application("Notes").make("folder", {"name": part})
container.folders().push(folder)
container = folder
return container
# Usage
notes = PyXA.Application("Notes")
icloud_account = notes.accounts().by_name("iCloud")
meetings_folder = ensure_path(icloud_account, "meetings/Acme/2024-07-01-Review")Create note with HTML body
JXA:
const note = Notes.Note({
name: "Client Review",
body: "<h1>Client Review</h1><div>Agenda...</div><h2>Decisions</h2><ul><li>...</li></ul>"
});
meetingsFolder.notes.push(note);PyXA:
import PyXA
# Create note with HTML body (using the meetings folder from above)
html_body = """
<h1>Client Review</h1>
<div>Agenda items to discuss...</div>
<h2>Decisions</h2>
<ul>
<li>Decision 1: ...</li>
<li>Decision 2: ...</li>
</ul>
"""
note = meetings_folder.notes().push({
"name": "Client Review",
"body": html_body
})
print(f"Created note: {note.name()}")Query recent notes
JXA:
const yesterday = new Date(Date.now() - 86400*1000);
const recent = meetingsFolder.notes.whose({ creationDate: { _greaterThan: yesterday } });
const names = recent.name();PyXA:
import PyXA
from datetime import datetime, timedelta
# Query notes created in the last 24 hours
yesterday = datetime.now() - timedelta(days=1)
# Filter notes by creation date
recent_notes = meetings_folder.notes().filter(
lambda note: note.creation_date() > yesterday
)
# Get names of recent notes
recent_names = [note.name() for note in recent_notes]
print(f"Recent notes: {recent_names}")Move note
JXA:
const target = ensurePath(acc, "Archive/2024");
Notes.move(meetingsFolder.notes.byName("Client Review"), { to: target });PyXA:
# Move note to archive folder
archive_folder = ensure_path(icloud_account, "Archive/2024")
note_to_move = meetings_folder.notes().by_name("Client Review")
if note_to_move:
note_to_move.move_to(archive_folder)
print("Note moved to archive")
else:
print("Note not found")Checklist workaround (UI)
- Create note text, then (if needed) front Notes and send
Cmd+Shift+Lvia System Events to toggle checklist on selected lines.
People dossiers
- Save under
people/<first>-<last>/<date>-<title>andpeople/<first>-<last>/overview.
const personFolder = ensurePath(acc, "people/Ada-Lovelace");
const overview = ensurePath(acc, "people/Ada-Lovelace/overview");
const dossier = Notes.Note({
name: "Overview",
body: "<h1>Ada Lovelace</h1><div>Birthday: ...</div><div>Allergies: ...</div>"
});
overview.notes.push(dossier);#!/usr/bin/env python3
"""
Create Note Script - PyXA Implementation
Creates a new note in Apple Notes
Usage: python create_note.py "Note Title" "Note Content" ["Folder Name"]
"""
import sys
import PyXA
def create_note(title, content, folder_name=None):
"""Create a new note in Notes app"""
try:
notes = PyXA.Application("Notes")
# Find or create target folder
target_folder = None
if folder_name:
# Try to find existing folder
for account in notes.accounts():
for folder in account.folders():
if folder.name == folder_name:
target_folder = folder
break
if target_folder:
break
# Create folder if it doesn't exist
if not target_folder:
# Create in first account
first_account = notes.accounts()[0]
target_folder = first_account.folders().push({"name": folder_name})
if not target_folder:
# Use default folder (first account's default)
first_account = notes.accounts()[0]
target_folder = first_account.folders()[0] # Default folder
# Create the note
note = target_folder.notes().push({
"name": title,
"body": content
})
# Make it visible
note.show()
print(f"Created note '{title}' in folder '{folder_name or 'default'}'")
return True
except Exception as e:
print(f"Error creating note: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python create_note.py 'Note Title' 'Note Content' ['Folder Name']")
sys.exit(1)
title = sys.argv[1]
content = sys.argv[2]
folder = sys.argv[3] if len(sys.argv) > 3 else None
success = create_note(title, content, folder)
sys.exit(0 if success else 1)#!/usr/bin/env python3
"""
Create Notes Folder Script - PyXA Implementation
Creates a new folder in Apple Notes
Usage: python create_notes_folder.py "Folder Name" ["Account Name"]
"""
import sys
import PyXA
def create_notes_folder(folder_name, account_name=None):
"""Create a new folder in Notes app"""
try:
notes = PyXA.Application("Notes")
# Find target account
target_account = None
if account_name:
# Find specific account
for account in notes.accounts():
if account.name == account_name:
target_account = account
break
else:
# Use first account (usually iCloud)
target_account = notes.accounts()[0]
if not target_account:
print(f"Account '{account_name}' not found" if account_name else "No accounts found")
return False
# Check if folder already exists
existing_folders = target_account.folders()
for folder in existing_folders:
if folder.name == folder_name:
print(f"Folder '{folder_name}' already exists in account '{target_account.name}'")
return True
# Create new folder
new_folder = target_account.folders().push({
"name": folder_name
})
print(f"Created folder '{folder_name}' in account '{target_account.name}'")
return True
except Exception as e:
print(f"Error creating notes folder: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python create_notes_folder.py 'Folder Name' ['Account Name']")
sys.exit(1)
folder_name = sys.argv[1]
account_name = sys.argv[2] if len(sys.argv) > 2 else None
success = create_notes_folder(folder_name, account_name)
sys.exit(0 if success else 1)#!/usr/bin/env python3
"""
Search Notes Script - PyXA Implementation
Searches for notes containing specific text
Usage: python search_notes.py "search term" ["folder name"]
"""
import sys
import PyXA
def search_notes(search_term, folder_name=None):
"""Search for notes containing the search term"""
try:
notes = PyXA.Application("Notes")
matching_notes = []
for account in notes.accounts():
folders_to_search = []
if folder_name:
# Search specific folder
for folder in account.folders():
if folder.name == folder_name:
folders_to_search = [folder]
break
else:
# Search all folders
folders_to_search = account.folders()
for folder in folders_to_search:
try:
folder_notes = folder.notes()
for note in folder_notes:
# Check title and body
title = note.name or ""
body = note.body or ""
if (search_term.lower() in title.lower() or
search_term.lower() in body.lower()):
matching_notes.append({
'title': title,
'folder': folder.name,
'account': account.name,
'id': note.id
})
except Exception as e:
print(f"Error searching folder {folder.name}: {e}")
continue
# Display results
if matching_notes:
print(f"Found {len(matching_notes)} notes containing '{search_term}':")
for i, note in enumerate(matching_notes, 1):
print(f"{i}. '{note['title']}' in {note['account']} > {note['folder']}")
else:
print(f"No notes found containing '{search_term}'")
return matching_notes
except Exception as e:
print(f"Error searching notes: {e}")
return []
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python search_notes.py 'search term' ['folder name']")
sys.exit(1)
search_term = sys.argv[1]
folder = sys.argv[2] if len(sys.argv) > 2 else None
results = search_notes(search_term, folder)
sys.exit(0 if results else 1)#!/usr/bin/env python3
"""Trigger Notes Automation prompt via a read-only AppleScript call."""
import subprocess
import sys
from textwrap import dedent
APPLESCRIPT = dedent(
"""
tell application "Notes"
activate
set accountNames to name of every account
set folderNames to name of every folder
return "Accounts: " & (accountNames as text) & " | Folders: " & (folderNames as text)
end tell
"""
)
def main() -> int:
print("Requesting Automation permission for Notes...")
result = subprocess.run(
["osascript", "-e", APPLESCRIPT],
capture_output=True,
text=True,
)
if result.stdout.strip():
print(result.stdout.strip())
if result.returncode != 0:
print(result.stderr.strip() or "Notes check failed without error output.")
return result.returncode
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env bash
# Trigger Notes Automation prompt via a read-only AppleScript call.
set -euo pipefail
echo "Requesting Automation permission for Notes..."
osascript -e 'tell application "Notes"
activate
set accountNames to name of every account
set folderNames to name of every folder
return "Accounts: " & (accountNames as text) & " | Folders: " & (folderNames as text)
end tell'
echo "Notes responded. If prompted, grant Terminal/Python permission."