Note Partial Edits
Append or insert into a BlackOps note with patch_notes without resending its body, so an AI agent can add one line to a large note safely and cheaply.
Add a line to a note without resending the whole note. patch_notes accepts two partial-edit fields, append_markdown and insert_markdown, that modify a note's body in place. Every byte you did not touch stays exactly as it was.
Why this exists
patch_notes has always been able to set content_markdown, and that replaces the entire body. On a short note that is fine. On a long one it is a problem with two halves.
The first half is cost. A dashboard note carrying months of history, Dataview queries and a task callout can run to fifteen thousand tokens. Adding one checkbox to it meant an agent retyping all fifteen thousand.
The second half is risk, and it is the one that actually bites. Retyping a note means re-emitting every code fence, every dataviewjs block, every callout delimiter. One dropped backtick silently breaks a query, and nothing tells you. Faced with that, an agent's safest move is to not edit the note at all, which is what kept happening.
Partial edits remove both. The agent sends the line it wants to add. The server does the splice.
Appending
append_markdown adds text to the end of the body, separated by a blank line.
{
"id": "0ea81400-979a-4bf7-93a2-53fc21efa4b9",
"append_markdown": "- [ ] #task #op-todo Approve the new tag"
}
The existing body is untouched. If the note has YAML frontmatter, the text lands after the body, never inside the frontmatter block.
Inserting at an anchor
Most edits are not at the end. A task belongs inside its callout; a new row belongs in its table. insert_markdown places text relative to an anchor: a literal substring of the body.
{
"id": "0ea81400-979a-4bf7-93a2-53fc21efa4b9",
"insert_markdown": {
"anchor": "> - [ ] #task #op-todo Review the tag vocabulary",
"position": "after",
"text": "> - [ ] #task #op-todo Approve the new tag"
}
}
| Field | Required | Meaning |
|---|---|---|
text | yes | The markdown to insert. Placed on its own line next to the anchor. |
anchor | yes | A literal substring of the body. Matched exactly, not as a pattern. |
position | no | before or after. Defaults to after. |
The text is placed on the line immediately next to the anchor, with a single newline and no blank line. That is deliberate: a blank line ends a callout, so an inserted task would fall out of it. If you want a blank line, put one in text.
Anchors are matched against the body only. The note's frontmatter is not searched, so an anchor like status: Open will not match a frontmatter key and quietly corrupt your metadata.
Use whole lines as anchors. The text is spliced in at the match itself, so an anchor that stops mid-line puts the newline mid-line too and leaves the rest of that line butted up against what you inserted. Anchoring on a complete line, or a run of them, avoids this. The server will not quietly relocate your insert to a tidier spot, for the same reason it will not pick between two matching anchors.
An ambiguous anchor is refused, never guessed
An anchor must match exactly once. If it does not, the call fails and nothing is written.
| Code | Meaning |
|---|---|
ANCHOR_NOT_FOUND | The anchor matched zero times. |
ANCHOR_NOT_UNIQUE | The anchor matched more than once. The message tells you the count. |
Picking the first match would be worse than failing. On a long note you would not notice the edit landing in the wrong section, and you would have no signal that anything went wrong. When you hit ANCHOR_NOT_UNIQUE, extend the anchor with a neighbouring line until it is unique.
Concurrency
A partial edit reads the body, changes it, and writes it back. If another writer lands in between, writing unconditionally would drop their change while reporting success.
Partial edits are therefore guarded on the note's version. If the version moved, the server re-reads the note, re-applies your edit to the current content, and writes again. Both changes survive, in the order they landed.
It will make three write attempts in total. If all three lose the race, the call returns 409 CONCURRENT_MODIFICATION and writes nothing. Retry it.
Full-body content_markdown replacement is unguarded and unchanged. It keeps its long-standing last-write-wins behaviour, because a caller sending a complete body is stating exactly what the note should contain.
Rules
- One content field per call. Sending
content_markdowntogether with a partial edit, or both partial-edit fields at once, returnsCONFLICTING_CONTENT_EDIT. Two edits in one request have no defined order. - Partial edits require a UUID.
patch_notesalso resolves slugs and titles, but a partial edit modifies a body the caller has not read back, and a title can match more than one note. Resolve the id withget_noteorlist_notesfirst. Addressing a partial edit by slug or title returnsPARTIAL_EDIT_REQUIRES_UUID. - An empty edit is refused, not applied as a silent no-op, so a malformed call is visible rather than swallowed.
- Metadata stays consistent with the body that was written. Fields BlackOps derives from a note's frontmatter, such as its tags and its Big Board phase, are recomputed from the content the write actually landed on, including after a concurrency retry.
- Vault sync is identical. A partial edit commits to your connected Obsidian repo exactly as a full replace does, and the response carries the same
syncblock.
When to use which
Reach for a partial edit whenever you are adding to a note rather than rewriting it, and always on a note large enough that you would not want to retype it. Use content_markdown when you are genuinely replacing the body, or when you are composing a note from scratch.
Related
- Note Taxonomy — tag vocabulary and folder routing for notes
- Knowledge Sync & Brains — how notes reach your Obsidian vault
- MCP Tool Catalog — the full tool surface