Interact with the Notes app on macOS
The Notes plugin lets 4D read and create content in the macOS Notes app. It talks to Notes through Apple Events: most commands use ScriptingBridge, and adding attachments, reading an attachment’s file location, and showing a note use NSAppleScript. Accounts, folders, notes and attachments are identified by the x-coredata://… ID strings that Notes itself uses. Lists come back as a Collection, single items as an Object; dates inside them are Text (ISO 8601), not 4D dates.
| Command | Returns | Purpose |
|---|---|---|
| Notes GET ACCOUNTS | Collection |
List every account, with its folder IDs |
| Notes GET FOLDERS | Collection |
List every folder, with its note and subfolder IDs |
| Notes GET NOTES | Collection |
List every note, with its attachment IDs |
| Notes GET ATTACHMENTS | Collection |
List every attachment, with the ID of its note |
| Notes Get account | Object |
One account and its folder tree |
| Notes Get folder | Object |
One folder, its subfolder tree and its notes |
| Notes Get note | Object |
One note, including its HTML body and dates |
| Notes Get attachment | Object |
One attachment, including the location of its file |
| Notes Create folder | Object |
Create a folder in an account or another folder |
| Notes Create note | Object |
Create a note, optionally with picture or file attachments |
| Notes SHOW | — | Show a note in the Notes app |
| Notes SET NOTIFICATION | — | Not implemented (no effect) |
| Notes Get notification | Text |
Not implemented (returns empty text) |
Platforms: macOS (Intel and Apple silicon).
tccutil reset AppleEvents.NSAppleEventsUsageDescription. The application that hosts the plugin (4D, or your built and signed application) must have this key in its Info.plist. Without it, macOS never shows the permission prompt and the commands silently fail. See the README for details and screenshots.Get commands fetch the IDs of every item of their kind before looking up the one you asked for, so they get slower as the library grows.{} or an empty collection []. Test the result (for example with OB Is empty or .length) before using it.Text of the form x-coredata://<store UUID>/<entity>/p<number>, for example x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15. Get them from the GET commands; they belong to one Mac’s Notes database.Text: creationDate/modificationDate in UTC (2019-08-07T10:42:31Z) and creationLocalDate/modificationLocalDate in the Mac’s local time zone, without a suffix (2019-08-07T19:42:31).NSAppleScript, which Notes Create note (attachments), Notes Get attachment and Notes SHOW use, is documented by Apple as main-thread-only. Calling these from a preemptive process hasn’t been verified; prefer a cooperative process.accounts:=Notes GET ACCOUNTS
| Parameter | Type | Description |
|---|---|---|
| Result | Collection |
One object per account |
Returns every account configured in Notes (iCloud, “On My Mac”, Exchange, …). Each element has these properties:
| Property | Type | Description |
|---|---|---|
name |
Text |
Account name |
id |
Text |
Account ID |
folders |
Collection |
IDs (Text) of the account’s folders, as Notes lists them for the account |
To get the folder names and the whole folder tree of one account, pass its id to Notes Get account.
From the plugin’s test method (TEST_notes.4dm):
$accounts:=Notes GET ACCOUNTS
Example result:
[
{
"name": "Exchange",
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSAccount/p2",
"folders": [
"x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p4"
]
},
{
"name": "iCloud",
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICAccount/p11",
"folders": [
"x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p12",
"x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p10"
]
}
]
Find an account by name and read its folder tree:
$accounts:=Notes GET ACCOUNTS
$icloud:=$accounts.query("name = :1"; "iCloud")
If ($icloud.length#0)
$account:=Notes Get account($icloud[0].id)
End if
folders:=Notes GET FOLDERS
| Parameter | Type | Description |
|---|---|---|
| Result | Collection |
One object per folder |
Returns the folders the Notes app lists at the application level, across all accounts. Each element has these properties:
| Property | Type | Description |
|---|---|---|
name |
Text |
Folder name |
id |
Text |
Folder ID |
notes |
Collection |
IDs (Text) of the notes in this folder |
folders |
Collection |
IDs (Text) of the folder’s direct subfolders |
Folder names are not unique: each account has its own default folder, and several accounts can have folders with the same name. Use the id.
From the plugin’s test method (TEST_notes.4dm):
$folders:=Notes GET FOLDERS
Example result:
[
{
"name": "Notes",
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p12",
"notes": [],
"folders": []
},
{
"name": "Recently Deleted",
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p10",
"notes": [
"x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p22",
"x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p21",
"x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15"
],
"folders": []
}
]
Count the notes in each folder:
$folders:=Notes GET FOLDERS
For each ($folder; $folders)
ALERT($folder.name+": "+String($folder.notes.length))
End for each
notes:=Notes GET NOTES
| Parameter | Type | Description |
|---|---|---|
| Result | Collection |
One object per note |
Returns every note in every account, including notes in Recently Deleted. Each element has these properties:
| Property | Type | Description |
|---|---|---|
name |
Text |
Note title |
id |
Text |
Note ID |
attachments |
Collection |
IDs (Text) of the note’s attachments |
The body and dates are not included; use Notes Get note for them.
From the plugin’s test method (TEST_notes.4dm):
$notes:=Notes GET NOTES
Example result:
[
{
"name": "Shopping",
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15",
"attachments": []
}
]
Read the body of every note that has attachments:
$notes:=Notes GET NOTES
For each ($item; $notes)
If ($item.attachments.length#0)
$note:=Notes Get note($item.id)
// $note.body is HTML
End if
End for each
attachments:=Notes GET ATTACHMENTS
| Parameter | Type | Description |
|---|---|---|
| Result | Collection |
One object per attachment |
Returns every attachment in every note. Each element has these properties:
| Property | Type | Description |
|---|---|---|
name |
Text |
Attachment name (usually the file name) |
id |
Text |
Attachment ID |
note |
Text |
ID of the note that contains the attachment |
From the plugin’s test method (TEST_notes.4dm):
$attachments:=Notes GET ATTACHMENTS
Example result:
[
{
"name": "image.png",
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICAttachment/p23",
"note": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15"
}
]
account:=Notes Get account(id)
| Parameter | Type | Description |
|---|---|---|
id |
Text |
Account ID, from Notes GET ACCOUNTS |
| Result | Object |
The account, or an empty object if no account has this ID |
Returns the account with its complete folder tree:
| Property | Type | Description |
|---|---|---|
id |
Text |
Account ID |
name |
Text |
Account name |
folders |
Collection |
Folder objects, each with name, id and folders (its own subfolders, recursively) |
These folder objects don’t list notes; use Notes Get folder for that.
From the plugin’s test method (TEST_notes.4dm):
$account:=Notes Get account("x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSAccount/p2")
Example result:
{
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSAccount/p2",
"name": "Exchange",
"folders": [
{
"name": "Notes",
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p4",
"folders": []
}
]
}
folder:=Notes Get folder(id)
| Parameter | Type | Description |
|---|---|---|
id |
Text |
Folder ID, from Notes GET FOLDERS or Notes Get account |
| Result | Object |
The folder, or an empty object if no folder has this ID |
Returns the folder, its subfolder tree and the notes it contains directly:
| Property | Type | Description |
|---|---|---|
id |
Text |
Folder ID |
name |
Text |
Folder name |
folders |
Collection |
Subfolder objects, each with name, id and folders (recursively) |
notes |
Collection |
An object with name and id for each note directly in this folder (notes in subfolders are not included) |
From the plugin’s test method (TEST_notes.4dm):
$folder:=Notes Get folder("x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p4")
Example result:
{
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p4",
"name": "Notes",
"folders": [],
"notes": [
{
"name": "Shopping",
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSNote/p4"
}
]
}
note:=Notes Get note(id)
| Parameter | Type | Description |
|---|---|---|
id |
Text |
Note ID, from Notes GET NOTES or Notes Get folder |
| Result | Object |
The note, or an empty object if no note has this ID |
| Property | Type | Description |
|---|---|---|
id |
Text |
Note ID |
folder |
Text |
ID of the folder that contains the note |
name |
Text |
Note title |
body |
Text |
Note content as HTML, as Notes returns it (for example <div>Shopping</div>) |
creationDate |
Text |
Creation date, UTC |
modificationDate |
Text |
Last modification date, UTC |
creationLocalDate |
Text |
Creation date, local time |
modificationLocalDate |
Text |
Last modification date, local time |
attachments |
Collection |
IDs (Text) of the note’s attachments |
From the plugin’s test method (TEST_notes.4dm):
$note:=Notes Get note("x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15")
Example result:
{
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p15",
"folder": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p10",
"name": "Shopping",
"body": "<div>Shopping</div>\n",
"creationDate": "2019-08-07T10:42:31Z",
"modificationDate": "2019-08-07T19:50:27Z",
"creationLocalDate": "2019-08-07T19:42:31",
"modificationLocalDate": "2019-08-08T04:50:27",
"attachments": []
}
attachment:=Notes Get attachment(id)
| Parameter | Type | Description |
|---|---|---|
id |
Text |
Attachment ID, from Notes GET ATTACHMENTS or Notes Get note |
| Result | Object |
The attachment, or an empty object if no attachment has this ID |
| Property | Type | Description |
|---|---|---|
id |
Text |
Attachment ID |
name |
Text |
Attachment name |
note |
Text |
ID of the note that contains the attachment |
contents |
Text |
Location of the attachment’s file inside the Notes data folder, as an HFS path (Macintosh HD:Users:…:410.png) |
contentIdentifier |
Text |
Content ID (cid:…) |
creationDate |
Text |
Creation date, UTC |
modificationDate |
Text |
Last modification date, UTC |
creationLocalDate |
Text |
Creation date, local time |
modificationLocalDate |
Text |
Last modification date, local time |
contents is read with AppleScript (contents of attachment … as Unicode text). When Notes doesn’t return a file for the attachment, the property may be missing or empty (inferred from the code; not tested for every attachment kind). The file lives in Notes’ own data folder (~/Library/Group Containers/group.com.apple.notes/): copy it rather than modifying it.
From the plugin’s test method (issue_2.4dm):
$attachment:=Notes Get attachment("x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICAttachment/p23")
SET TEXT TO PASTEBOARD:C523(JSON Stringify:C1217($attachment; *))
Example result:
{
"id": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICAttachment/p23",
"contents": "Macintosh HD:Users:miyako:Library:Group Containers:group.com.apple.notes:Accounts:F67EF39F-73DE-4FB8-840C-6034A72B5272:Media:C955A294-CA7D-43BC-8C88-92033CFF1702:1_B8A3CD7C-9BA6-4322-A17E-C5AF8E90F3B7:410.png",
"note": "x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p22",
"creationDate": "2024-05-01T04:56:52Z",
"modificationDate": "2024-05-01T04:56:52Z",
"creationLocalDate": "2024-05-01T13:56:52",
"modificationLocalDate": "2024-05-01T13:56:52",
"contentIdentifier": "cid:6ADF0ACD-9233-4072-8549-AE958FD76D4A@icloud.apple.com",
"name": "410.png"
}
Turn contents into a 4D File object (on macOS the HFS path is the platform path):
$attachment:=Notes Get attachment($id)
If (String($attachment.contents)#"")
$file:=File($attachment.contents; fk platform path)
End if
folder:=Notes Create folder(parent; name)
| Parameter | Type | Description |
|---|---|---|
parent |
Text |
ID of a folder (the new folder becomes its subfolder) or of an account (the new folder is created at the top level of that account) |
name |
Text |
Name of the new folder; must not be empty |
| Result | Object |
The new folder, or an empty object on failure |
parent is first looked up as a folder ID, then as an account ID. If it matches neither, or name is empty, nothing is created and the result is an empty object. On success the result has two properties:
| Property | Type | Description |
|---|---|---|
id |
Text |
ID of the new folder |
name |
Text |
Its name, as Notes stored it |
From the plugin’s test method (TEST_notes.4dm, where the line is commented out):
//$folder:=Notes Create folder("x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p4"; "title4")
Example result:
{
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSFolder/p16",
"name": "title4"
}
Create a folder at the top level of the first account:
$accounts:=Notes GET ACCOUNTS
If ($accounts.length#0)
$folder:=Notes Create folder($accounts[0].id; "Invoices")
If (OB Is empty($folder))
ALERT("The folder could not be created.")
End if
End if
note:=Notes Create note(folder; title; body; attachments; names)
| Parameter | Type | Description |
|---|---|---|
folder |
Text |
ID of the folder that receives the note (an account ID is not accepted) |
title |
Text |
Note title |
body |
Text |
Note content; Notes treats it as HTML |
attachments |
Array Picture or Array Blob |
Files to attach; pass an empty array for none |
names |
Array Text |
File name for each attachment, by element number; pass an empty array to use generated names |
| Result | Object |
The new note, or an empty object on failure |
The note is created at the top of folder. Then each element of attachments is written to a temporary file, attached with AppleScript, and the temporary file is deleted.
Array Picture) are converted to PNG first. Without a name, the file is called <generated UUID>.png.Array Blob) are attached unchanged. Without a name, the file is called <generated UUID> with no extension, so pass a name with the right extension (report.pdf); Notes uses it to decide how to show the file.0 of the arrays is processed too; empty pictures and empty BLOBs are skipped.names{i} names attachments{i}. An empty name, a missing element, or a names array that isn’t Array Text gives a generated name. (Up to version 4.3.0 the element check was inverted: in-range names were ignored, and only an element at or past the end of names was read. Fixed after 4.3.0.)The result describes the note as created:
| Property | Type | Description |
|---|---|---|
id |
Text |
ID of the new note |
name |
Text |
The title you passed |
body |
Text |
The body you passed |
creationDate |
Text |
Creation date, UTC |
modificationDate |
Text |
Last modification date, UTC |
creationLocalDate |
Text |
Creation date, local time |
modificationLocalDate |
Text |
Last modification date, local time |
If folder isn’t a known folder ID, or Notes doesn’t create the note, the result is an empty object and no attachment is added. An attachment that fails is skipped without an error, and the note is still returned. To check what was attached, call Notes Get note with the returned id and look at attachments.
Pass both arrays even when you have no attachments; the sample code always does, and calling the command without them hasn’t been verified.
From the plugin’s test method (issue_3.4dm), attaching a picture with an explicit name:
ARRAY PICTURE:C279($images; 1)
READ PICTURE FILE:C678(Get 4D folder:C485(Current resources folder:K5:16)+"test.png"; $image)
$images{1}:=$image
ARRAY TEXT:C222($names; 1)
$names{1}:="test.png"
$note:=Notes Create note("x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICFolder/p12"; "title"; "body"; $images; $names)
SET TEXT TO PASTEBOARD:C523(JSON Stringify:C1217($note; *))
From the plugin’s test method (test_create.4dm), attaching a PDF file as a BLOB:
$testFile:=Folder:C1567(fk desktop folder:K87:19).file("test.pdf")
C_OBJECT:C1216($note)
$title:="test pdf"
$body:="body body"
ARRAY BLOB:C1222($attachments;0)
ARRAY TEXT:C222($fileNames;0)
INSERT IN ARRAY:C227($attachments;1)
DOCUMENT TO BLOB:C525($testFile.platformPath;$attachments{1})
APPEND TO ARRAY:C911($fileNames;$testFile.name+$testFile.extension)
$note:=Notes Create note ("x-coredata://2BB27C30-9D8D-4501-8ADD-D16531CA22F5/ICFolder/p11";$title;$body;$attachments;$fileNames)
Example result:
{
"id": "x-coredata://D219C617-A827-4FFA-B8F1-1ADC065277D6/EWSNote/p4",
"name": "title",
"body": "body",
"creationDate": "2024-05-01T06:05:55Z",
"modificationDate": "2024-05-01T06:05:55Z",
"creationLocalDate": "2024-05-01T15:05:55",
"modificationLocalDate": "2024-05-01T15:05:55"
}
A note without attachments, with HTML formatting:
ARRAY PICTURE($none; 0)
ARRAY TEXT($noNames; 0)
$note:=Notes Create note($folderId; "Meeting"; "<div><b>Agenda</b></div><div>1. Budget</div>"; $none; $noNames)
Notes SHOW(note)
| Parameter | Type | Description |
|---|---|---|
note |
Text |
A note ID, or text to search for |
| Result | — | None |
note starts with x-coredata://, Notes is asked to show the note with that ID.note, one after the other.If no note matches, nothing happens and no error is raised.
From the plugin’s test method (test_show.4dm):
Notes SHOW("x-coredata://5E8616AB-4594-4644-8F88-CE7880A4E33D/ICNote/p22")
Show the notes that mention an order number:
Notes SHOW("Order 10423")
Notes SET NOTIFICATION(method)
| Parameter | Type | Description |
|---|---|---|
method |
Text |
Ignored |
| Result | — | None |
Not implemented. The command is declared but does nothing.
method:=Notes Get notification
| Parameter | Type | Description |
|---|---|---|
| Result | Text |
Always empty |
Not implemented. The command is declared but never sets a result, so you get an empty text.
NSAppleEventsUsageDescription in its Info.plist (see Requirements & platform notes).Info.plist key is missing. Run tccutil reset AppleEvents in Terminal and restart 4D to be asked again.Get or Create command. The ID doesn’t exist (IDs belong to one Mac’s Notes database), the parent of Notes Create folder is neither a folder nor an account, its name was empty, or the folder of Notes Create note is an account ID instead of a folder ID.invoice.pdf in names.creationDate and modificationDate are UTC (Z suffix); use creationLocalDate/modificationLocalDate for the Mac’s local time.Get commands first list every item of their kind. Keep the IDs and objects you already have instead of calling a Get command in a tight loop.$accounts:=Notes GET ACCOUNTS
$folders:=Notes GET FOLDERS
$notes:=Notes GET NOTES
$attachments:=Notes GET ATTACHMENTS
$account:=Notes Get account($accounts[0].id)
$folder:=Notes Get folder($folders[0].id)
$note:=Notes Get note($notes[0].id)
$attachment:=Notes Get attachment($attachments[0].id)
$newFolder:=Notes Create folder($account.id; "New folder")
ARRAY PICTURE($images; 0)
ARRAY TEXT($names; 0)
$newNote:=Notes Create note($newFolder.id; "Title"; "<div>Body</div>"; $images; $names)
Notes SHOW($newNote.id)