¶graphID=`. It selects
the note like `list-note-paragraphs` and the paragraph by exactly one of:
- `contains`: a snippet of the paragraph. Case, runs of spaces, and Unicode
width are ignored.
- `match`: the whole paragraph, compared the same way.
- `blockIndex`: from `list-note-paragraphs`.
When `contains` or `match` hits several paragraphs, pass `occurrence` (1-based)
or a longer snippet.
The link is returned only when the paragraph's ID is `unique` in the note.
Otherwise the result is an error whose `structuredContent` carries the usual
`code` plus a `reason`: `paragraph-id-shared`, `paragraph-id-missing`,
`no-match`, `ambiguous-paragraph`, `occurrence-out-of-range`, `ambiguous-note`
(several notes have that title; the message lists their folders), `not-found`,
`encrypted`, or `no-body`. The tool never creates or changes a paragraph ID,
and an edit in Notes can later replace the ID and break the link. With
`recordAnchor` it also writes the local anchor registry, so the tool is not
marked read-only in its MCP annotations.
**Requires:** Full Disk Access.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id`, `title`, `folder` | string | One of `id`, `title` | Note selector, as in `list-note-paragraphs` |
| `contains` | string | One of `contains`, `match`, `blockIndex` | Snippet of the paragraph |
| `match` | string | One of `contains`, `match`, `blockIndex` | The whole paragraph |
| `blockIndex` | number | One of `contains`, `match`, `blockIndex` | The paragraph's `blockIndex` |
| `occurrence` | number | No | Which match to use when several paragraphs match |
| `recordAnchor` | boolean | No | Also record a [paragraph anchor](#paragraph-anchors) for the linked paragraph and return its `anchorId`. Writes only the local anchor registry |
---
#### Paragraph anchors
A paragraph link breaks when Notes replaces or copies the paragraph's ID. A
paragraph anchor records enough about the paragraph to find it again: the
note's Notes UUID, the paragraph ID and its status, the paragraph's normalized
text and a fingerprint of it, the fingerprints of the paragraphs before and
after it, its `blockIndex`, and when it was recorded. Anchors are kept in one
local file, `~/Library/Application Support/apple-notes-mcp/paragraph-anchors.json`
(or the absolute path in `APPLE_NOTES_MCP_ANCHOR_FILE`). It is created with
mode 0600 in a 0700 directory, replaced atomically under a lock file, and
holds the text of every anchored paragraph. A symlinked, unreadable or
corrupt registry is refused and left as it is. The anchor tools never change
Notes.
Resolving an anchor tries three steps in order, and every step fails closed:
1. **Paragraph ID.** A paragraph whose ID is still unique in the note is the
anchored paragraph, even if its text was edited or it moved. A link to that
ID opens exactly that paragraph.
2. **Exact text.** The same normalized text. When several paragraphs have it,
only the one whose neighbours still match is chosen; a tie is `ambiguous`.
3. **Text and neighbours.** Similar text between both recorded neighbours, or
nearly identical text beside one of them. Two close candidates are
`ambiguous`; one below `minConfidence` is `low-confidence`.
A match whose ID is now shared or missing is `needs-reminting`: the paragraph
is found (the result names its block), but no safe link exists until it gets a
new ID. Public automation cannot set a paragraph ID, so this server only
reports it and returns no `url`.
To share anchored links outside Notes, see the
[paragraph anchor resolver](#paragraph-anchor-resolver-opt-in).
#### `create-paragraph-anchor`
Records an anchor for one paragraph, selected exactly as in
`get-paragraph-link` (note by `id` or `title`/`folder`; paragraph by one of
`contains`, `match`, `blockIndex`, plus `occurrence`). Unlike
`get-paragraph-link`, a paragraph whose ID is shared or missing can be
anchored. Recording the same paragraph again (same note, ID, text and block)
returns the existing anchor with `created: false`.
Returns `anchor`, `created`, and `url` when the paragraph currently has a
unique ID.
**Requires:** Full Disk Access. Writes only the anchor registry.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id`, `title`, `folder` | string | One of `id`, `title` | Note selector, as in `list-note-paragraphs` |
| `contains`, `match`, `blockIndex`, `occurrence` | | One of `contains`, `match`, `blockIndex` | Paragraph selector, as in `get-paragraph-link` |
#### `resolve-paragraph-anchor`
Finds an anchored paragraph in the note as it is now. The result has `status`
(`resolved`, `needs-reminting`, `ambiguous`, `low-confidence`, `not-found`,
`note-not-found`, `note-deleted` for a note in Recently Deleted, or
`note-unreadable` for a locked note), `method` (`paragraph-id`, `exact-text`,
`text-and-neighbours`), `confidence` (0 to 1), `match` (`blockIndex`, `text`,
`paragraphId`, `paragraphIdStatus`), `changes` (`textChanged`,
`blockIndexChanged`, `paragraphIdChanged`), and `message`. `url`, the current
`applenotes://` link, is present only when `status` is `resolved`.
**Requires:** Full Disk Access.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `anchorId` | string | Yes | The anchor (`pa_` and 24 hex digits) |
| `minConfidence` | number | No | Lowest confidence accepted as a match (default 0.6) |
| `refresh` | boolean | No | After a match with confidence 0.8 or more, store the paragraph as it is now (text, neighbours, block, ID), so later edits are tracked from here. Reports `refreshed` or `refreshSkipped` |
#### `list-paragraph-anchors`
Lists recorded anchors in the order recorded, with `total`, the `registry`
path and `page` info. Reads only the registry.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `noteIdentifier` | string | No | Only anchors in the note with this Notes UUID |
| `offset` | number | No | First anchor to return (default 0) |
| `limit` | number | No | Maximum anchors (default 100, max 1000) |
#### `get-paragraph-anchor`
Returns one stored anchor as recorded. Reads only the registry.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `anchorId` | string | Yes | The anchor |
#### `prune-paragraph-anchors`
Removes anchors that no longer resolve, or the anchors you list. It is a dry
run unless `dryRun` is `false`. Without `anchorIds` it resolves every anchor
in scope and treats `not-found` and `note-not-found` as stale. A note in
Recently Deleted (`note-deleted`) is kept by default because it can be
restored. Returns `stale` (`anchorId`, `status`, `message`), `examined`, and
`removed`.
**Requires:** Full Disk Access unless `anchorIds` is given.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `anchorIds` | string[] | No | Remove exactly these anchors instead of resolving |
| `noteIdentifier` | string | No | Only anchors in the note with this Notes UUID |
| `statuses` | string[] | No | Statuses treated as stale (default `not-found`, `note-not-found`) |
| `dryRun` | boolean | No | Report without removing (default `true`) |
---
#### `get-note-structure`
Returns a read-only overview of one note from the NoteStore database, in one
call:
- `text` (decoded body), `textLength`, `wordCount`, `charCount` (Unicode
characters, attachment placeholders excluded), and `blockSummary` (the
`get-note-blocks` summary counts).
- `links`, each with a `kind`: `inline` (a hyperlink on text), `card` (a rich
link preview attachment, with `attachmentId` and `previewPath`), `note` (a
native link chip to another note), or `section` (a native link chip to a
heading or paragraph, with `section`). Notes deep links also carry
`targetNote` and `paragraphId`. `linkCounts` totals them.
- `tags` (native tags in body order) and `attachments`, listed the way
[`list-attachments`](#list-attachments) with `includePaths` lists them: the
same `kind` (`image`, `scan`, `drawing`, `pdf`, `audio`, `video`, `url`,
`table`, `other`), the same body order, and the same `previewPath`, plus the
card `title`/`url`, `fileSize`, and body position. Gallery items and
recording parts are nested under `children`; `attachmentCount` counts
top-level attachments only.
- `deepLink`, `isShared` (the note or any enclosing folder is shared),
`isLocked`, `isPinned`, `inRecentlyDeleted`, `lastViewed`,
`checklistTotal`, `checklistDone`, `hasDrawing` (classic sketches and Paper
drawings), and `firstImage` (the same lead visual `list-attachments`
returns with `firstImage`, plus its attachment `id`).
`lastViewed` is an ISO date, or null with `lastViewedStatus` set to
`never-viewed`, `not-recorded`, `malformed`, or `unsupported` (the column
does not exist on this macOS version). A password-protected note returns its
metadata and attachment rows with `bodyDecoded: false` and the body-derived
fields null.
**Requires:** Full Disk Access.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact note ID: the `x-coredata://` id, or the note's Notes UUID or numeric key (see [Identifier forms](#identifier-forms)) |
| `includeText` | boolean | No | Include the decoded text (default true). Text larger than `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` is omitted with `textOmitted: true` |
Link URLs are returned as stored; check `linkSafe` before emitting one into
HTML.
---
#### `list-note-links`
Lists links, read-only from the NoteStore database, in one note (`id`) or
across a `folder`, an `account`, or the whole library (no selector). Each link
has a `kind`:
| Kind | What it is | Where Notes stores it |
|------|------------|-----------------------|
| `inline` | A hyperlink on text | Inside the note body |
| `card` | A rich link preview | An attachment row with the URL and title |
| `note` | A native link chip to another note | An inline-attachment row with a Notes deep link |
| `section` | A native link chip to a heading or paragraph | Same, with `paragraphID` in the deep link |
Each row has `url`, `text` (label), `linkSafe`, `targetNote` and `paragraphId`
for Notes deep links, `section` for section chips, and for cards
`attachmentId` and `previewPath` (Notes' largest cached preview image, found
the same way [`list-attachments`](#list-attachments) finds it, or null when
Notes has none). Each row also names its source: `noteId`, `noteIdentifier`,
`noteTitle`, `noteModified`, `folder`, `folderPath` (as `list-folders` prints
it), `account`, and `accountIdentifier`. When bodies were decoded, `start` and
`blockIndex` give the link's position, and `inBody: false` marks a card or chip
row with no marker left in the body.
Inline links need every body in scope decompressed and decoded, so a folder,
account, or library scan includes them only with `includeInline: true`
(slower). A single note always includes them. A `folder` scope includes its
subfolders unless `includeSubfolders` is false. Scans skip Recently Deleted
and folderless notes; a note requested by `id` is read wherever it is. Links
come newest-modified note first, in body order within a note.
**Requires:** Full Disk Access.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | No | One exact note ID (x-coredata id, Notes UUID, or numeric key). Do not combine with `account` or `folder` |
| `account` | string | No | Account name (exact or unique-prefix match, as in the other tools) |
| `folder` | string | No | Folder name (any depth, must be unique) or path from the top level as `list-folders` prints it, such as `Work/Clients`; escape a literal slash as `\/` |
| `includeSubfolders` | boolean | No | With `folder`, also list notes in its subfolders (default true) |
| `includeInline` | boolean | No | Decode bodies for inline links (default true for `id`, false otherwise) |
| `kinds` | string[] | No | Only these kinds: `inline`, `card`, `note`, `section` |
| `offset` | number | No | First link to return (default 0). Use `page.nextOffset` |
| `limit` | number | No | Maximum links per page (default 200, max 2000) |
The response also reports `scope`, `counts` per kind, `notesInScope`,
`notesWithoutBody` (locked, empty or undecodable bodies when inline links were
requested), and `page`. A page stops early to stay under
`APPLE_NOTES_MCP_BLOCKS_MAX_BYTES`. An ambiguous folder name is refused with a
message that lists the matching paths.
---
#### `list-native-tags`
Lists actual native Notes tags. This differs from textual hashtag search. It is
read-only and requires Full Disk Access.
- **Folder mode** (pass `folder`, optionally `account`): maps each tag used in that
folder to its matching note IDs. The response reports `complete: false` and per-note
errors when some native metadata is unavailable.
- **Inventory mode** (omit `folder`): an account-wide inventory with counts, read-only from the
database. `account` narrows it to one account; omit it to count every
account. Each `inventory` entry has `tag`, `noteCount` (distinct notes, Recently
Deleted excluded), per-account counts in `accounts`, and any other `spellings` Notes
treats as the same tag. Tags with no remaining notes are listed with `noteCount: 0`.
A tag counts for a note only while the note body still references it. Locked or
unreadable bodies are counted from the tag objects alone and reported through
`unverifiedNotes` and `complete: false`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `folder` | string | No | Folder to list (nested paths supported). Omit for the inventory |
| `account` | string | No | Account name, exact or unique prefix |
---
#### `native-tags-status`
Checks whether exactly one configured Native Tags Shortcut is installed. An
installed workflow may still need macOS permission on its first execution, and
installation does not show whether that consent was given: run it once in the
foreground in Shortcuts.app and choose **Always Allow** after install or upgrade
(see [Troubleshooting](#native-writes-time-out-or-report-an-uncertain-outcome)).
---
#### `add-native-tags`
Adds actual native Notes tag objects to one exact note using `id`, a fresh
`expectedContentHash`, a distinctive existing `scopeText`, and `tags`. The
operation verifies the note's original text, links, and native objects after the
Shortcut runs. It refuses ambiguous title-and-scope matches and never retries an
uncertain write. Install the signed workflow as described in
[`shortcuts/README.md`](shortcuts/README.md).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact note ID, in any [identifier form](#identifier-forms) |
| `expectedContentHash` | string | Yes | `contentHash` from a fresh read of the note |
| `scopeText` | string | Yes | Distinctive phrase already in the note, 12–500 characters |
| `tags` | string[] | Yes | 1–100 tag names to add |
---
#### `get-note-plaintext`
Retrieves a note's body as plain text, with no HTML markup.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | No | Note ID (preferred - more reliable than title) |
| `title` | string | No | Note title (use `id` instead when available) |
| `account` | string | No | Account containing the note (defaults to Notes.app's default account; exact or unique-prefix match, ignored if `id` is provided) |
**Note:** Either `id` or `title` must be provided. This reads the note's native `plaintext` property, so it skips the HTML-to-text conversion that `get-note-content` plus a Markdown pass would do. Use `get-note-content` when you need the HTML, or `get-note-markdown` when you want Markdown with checklist state.
**Returns:** The plain-text content of the note in `structuredContent.plaintext`, or error if not found.
---
#### `get-note-details`
Retrieves metadata about a note (without full content).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `title` | string | Yes | Exact title of the note |
| `account` | string | No | Account containing the note (defaults to Notes.app's default account; exact or unique-prefix match) |
**Example:**
```json
{
"title": "Project Plan"
}
```
**Returns:** JSON with note metadata:
```json
{
"id": "x-coredata://...",
"title": "Project Plan",
"created": "2025-01-15T10:30:00.000Z",
"modified": "2025-01-20T14:22:00.000Z",
"shared": false,
"passwordProtected": false,
"account": "iCloud"
}
```
---
#### `get-note-by-id`
Retrieves a note using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`), or the note's Notes UUID or numeric key (see [Identifier forms](#identifier-forms)) |
**Returns:** JSON with note metadata, plus `identifier`, `folderIdentifier`, and `accountIdentifier` when Full Disk Access is granted, or error if not found.
---
#### `show-note`
Reveals a note in Notes.app using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`), or the note's Notes UUID or numeric key |
| `separately` | boolean | No | Open in a separate note window when supported by Notes.app |
**Returns:** Confirmation that Notes.app accepted the show command.
---
#### `update-note`
Updates an existing note's content and/or title.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being replaced |
| `newTitle` | string | No | New title (if changing the title; ignored when `format` is `"html"`) |
| `newContent` | string | Yes | New content for the note body |
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
| `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
| `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
Title-only updates are rejected because Apple Notes titles are not unique.
**Example - Using ID (recommended):**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"newContent": "Updated content here"
}
```
**Example - Update with HTML formatting:**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"newContent": "New findings with bold emphasis.
console.log('hello');
",
"format": "html"
}
```
**Returns:** Confirmation with the exact ID, post-save `contentHash`, and
`verifiedVisibleText: true`, or an error if the note changed before saving.
Apple Notes normalizes HTML, so this proves the visible text after saving, not
byte-identical rich formatting.
**Note:** `newContent` **replaces the entire note body** — it is not appended. To add to a note, prefer [`append-to-note`](#append-to-note), which does the read-and-concatenate for you and always round-trips the body as HTML. If you do read-modify-write by hand, note that `get-note-content` replaces oversized inline images with text placeholders (see [`get-note-content`](#get-note-content)) — writing that body back bakes the placeholders in.
**Rich-content safety:** `update-note` refuses to replace a note when its rich
metadata is unavailable or it contains attachments, native tags, inline
objects, or checklists that AppleScript cannot preserve. Existing link
destinations must remain present unless `allowLinkChanges` is explicitly set.
---
#### `delete-note`
Deletes a note (moves to Recently Deleted in Notes.app).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being deleted |
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
| `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
| `guardNoteId` | string | No | A second note (usually a verified copy) that must still be intact; needs Full Disk Access |
| `expectedGuardContentHash` | string | With `guardNoteId` | `contentHash` of the guard note from `get-note-content` |
| `requireActiveNoteId` | string | No | A second note that must still exist, be unlocked, stay outside Recently Deleted, and not be a Quick Note; its content is not fingerprinted. Needs Full Disk Access |
Title-only deletion is rejected. If the note changed after the supplied hash
was read, deletion is also rejected.
**Notes with large images.** Notes.app returns a note's body with each inline
image embedded as base64, so a note holding one 40 MB image has a body of about
110 MB. Body reads accept up to 512 MB of output, and `delete-note` compares a
body longer than 5 MB against a private temporary file rather than embedding it
in the AppleScript. The comparison still covers the whole body, and the file
is removed when the delete finishes.
**Copy-then-retire.** To delete an original only while its copy is still good,
read both notes, verify the copy, and pass the copy as `guardNoteId` with its
`contentHash` as `expectedGuardContentHash`. The copy's revision is re-read just
before the delete, and its body, lock state, and folder are checked again
inside the delete AppleScript, with the same fail-closed Recently Deleted test
as the note being deleted. The guard note must not be a Quick Note, a flag only
the database holds, so the guard needs Full Disk Access. A copy the database
has not saved yet passes on the live checks alone. The pair is still not one
transaction: the rich revision (which also covers checklists and attachments)
is a pre-check, and the in-script check covers the body and state Notes.app
exposes. `requireActiveNoteId` is the narrower form for a destination you
wrote yourself rather than copied.
**Example - Using ID (recommended):**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
```
**Returns:** Confirmation message, or error if note not found. The delete and a
check of the note's original folder run in one AppleScript: if Notes.app accepts
the delete but the note is still listed in that folder, the call reports that
nothing was deleted instead of claiming success.
A note that is already in Recently Deleted is refused, because deleting it there
removes it permanently. The folder is read live from Notes.app in the same
AppleScript; the Recently Deleted folder is recognised by its database id (with
Full Disk Access) or by its English name. The check fails closed: a note whose
folder Notes.app reports as something other than a folder (as it does for a note
trashed earlier in the same Notes session) is refused the same way, and a note
whose folder cannot be read at all is refused with a message to retry. To remove
such a note for good, do it in Notes.app.
**⚠️ Safety:** Irreversible from the agent's side — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact id(s) being deleted.
---
#### `move-note`
Moves a note to a different folder. The note is relocated in place via Notes.app's native `move`, so its id, creation date, and all embedded attachments (files, images, scans, PDFs, audio) are preserved. The destination folder must already exist — create it first with [`create-folder`](#create-folder).
A smart folder is never a destination: it only gathers notes by its rules, and Notes.app would move the note to Recently Deleted or store a created note where no folder shows it. `create-note`, `create-note-with-attachment`, `move-note`, `batch-move-notes`, and `create-folder` refuse one before writing anything, with `code: "unsupported"`, `committed: false`, and `reason: "smart_folder_destination"`. An ordinary folder that shares a smart folder's name is still found. Detection reads the NoteStore database, so it needs Full Disk Access; without it, destinations resolve as before.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`) |
| `account` | string | No | Account whose `folder` is the destination (default: the default account) |
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
| `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
Title-only moves are rejected.
**Example - Using ID (recommended):**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"folder": "Archive"
}
```
**Returns:** Confirmation only after the same note ID is read back and its
actual destination folder ID matches the requested folder.
---
#### Folder scope guards
`update-note`, `append-to-note`, `delete-note`, `move-note`, and
`batch-move-notes` accept three optional folder preconditions. Use exact
folder ids from `list-folders`.
- `ifFolderId`: the note must currently be in exactly this folder.
- `ifAncestorFolderId`: the note must be inside this folder or any of its
subfolders.
- `forbiddenAncestorFolderIds` (up to 50): the note must not be inside any of
these folders or their subfolders. For `move-note` and `batch-move-notes`,
the destination must not be either.
The checks read Notes.app's live folders inside the same AppleScript as the
write, immediately before it. If the folder observed by the check fails a
precondition, the write is refused with `Scope guard failed: …`. The check and
write are separate Notes operations; another actor can move the note between
them. A native append to a protected note runs through Shortcuts, so its
precheck runs in a separate AppleScript before the append.
For `batch-move-notes`, the same preconditions apply to every supplied note ID.
Each note is checked independently immediately before its move. A scope mismatch
leaves that note unchanged and appears in its failure result; the remaining
items continue. A batch can therefore move some notes and refuse others.
**Example - retire a note only while it is still in the inbox:**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"ifFolderId": "x-coredata://ABC123/ICFolder/p12"
}
```
---
#### `append-to-note`
Appends or prepends content to an existing note without replacing it. Always reads and writes as HTML, preserving all existing rich formatting.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Exact CoreData note ID returned by a read or search |
| `expectedContentHash` | string | Yes | `contentHash` from the exact note version being extended |
| `content` | string | Yes | Text to append to the note body |
| `position` | string | No | `"after"` (default) appends to the end; `"before"` inserts directly below the note's title line, so the title stays first |
| `separator` | string | No | String placed between existing content and new content (default: two newlines → `
` in HTML) |
| `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
| `scopeText` | string | Native-object notes only | Unique existing phrase, as for [`append-native`](#append-native) |
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
| `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
Title-only appends are rejected.
**Example - Append plaintext:**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"content": "New item added today"
}
```
**Example - Prepend HTML:**
```json
{
"id": "x-coredata://ABC123/ICNote/p456",
"expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"content": "Status: done
",
"format": "html",
"position": "before"
}
```
**Returns:** Confirmation with the exact ID, post-save `contentHash`, and
`verifiedVisibleText: true`. Apple Notes normalizes HTML, so this proves the
visible text after saving, not byte-identical rich formatting. Warns when the
note is shared with collaborators.
**Safety:** The append is rejected if the note changed since it was read, rich
metadata is unavailable, or the note contains attachments. Existing link
destinations are verified after saving.
**Notes containing native objects** (a table, a checklist, native tags) cannot be
spliced, so they are routed to the native end-append bridge — see
[`append-native`](#append-native). That path additionally requires `scopeText`,
keeps the default blank-line `separator` and `position: "after"`, and accepts a
fixed HTML subset rather than anything Notes.app can render:
| | Native append |
|---|---|
| Elements | `` `` `
` `` `` `