Edit Note
notes.editMutate the body of an existing Obsidian note using replace, append, prepend, insert after heading, or insert after block reference modes. Fails if note does not exist; create it first with notes.create.
Instructions
Mutate the body of an existing note. The mode field selects how content is applied: replace overwrites the whole note; append adds to the end; prepend adds after the frontmatter (or at the top if none); after-heading inserts after the first heading whose text matches anchor (no leading #); after-block inserts after the block reference ^anchor. Fails if the note does not exist — use notes.create first. replace mode is idempotent-destructive; the others are additive.
Operates on the session-active vault (see vault.current — selectable via vault.select) unless an explicit vaultPath argument is passed, which always wins.
Examples:
Example 1 — Append a new journal entry to the end of today's note:
{
"mode": "append",
"path": "Journal/2026-04-24.md",
"content": "\n## Afternoon\n\nFinished the tool consolidation."
}Example 2 — Insert content after a specific heading:
{
"mode": "after-heading",
"path": "Projects/Alpha.md",
"anchor": "Open questions",
"content": "- Do we need to bump the Zod major?\n"
}Example 3 — Insert after a block reference:
{
"mode": "after-block",
"path": "Notes/idea.md",
"anchor": "idea-1",
"content": "Follow-up thought …"
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| changed | Yes | True if the tool altered vault state on this call; false if it was a no-op. | |
| target | Yes | The path or identifier the tool acted on. | |
| summary | Yes | Short human-readable summary of what happened. |
Implementation Reference
- src/server/tools/notes.ts:54-94 (handler)The handler function for notes.edit that dispatches to the appropriate domain function based on the mode: replace, append, prepend, after-heading, or after-block.
async function handleEdit(context: DomainContext, args: NotesEditArgs) { if (args.mode === "replace") { return updateNote(context, { path: args.path, content: args.content, mergeStrategy: "replace", vaultPath: args.vaultPath, }); } if (args.mode === "append") { return appendToNote(context, { filePath: args.path, content: args.content, vaultPath: args.vaultPath, }); } if (args.mode === "prepend") { // No dedicated prepend domain fn — compose via read+replace. const existing = await readNote(context, { path: args.path, vaultPath: args.vaultPath }); return updateNote(context, { path: args.path, content: `${args.content}${existing.content.startsWith("\n") ? "" : "\n"}${existing.content}`, mergeStrategy: "replace", vaultPath: args.vaultPath, }); } if (args.mode === "after-heading") { return insertAfterHeading(context, { filePath: args.path, heading: args.anchor, content: args.content, vaultPath: args.vaultPath, }); } return insertAfterBlock(context, { filePath: args.path, blockId: args.anchor, content: args.content, vaultPath: args.vaultPath, }); } - src/server/tools/notes.ts:170-210 (registration)Tool registration for notes.edit, defining name, title, description, input/output schemas, annotations, and handler.
{ name: "notes.edit", title: "Edit Note", description: "Mutate the body of an existing note. The `mode` field selects how `content` is applied: `replace` overwrites the whole note; `append` adds to the end; `prepend` adds after the frontmatter (or at the top if none); `after-heading` inserts after the first heading whose text matches `anchor` (no leading `#`); `after-block` inserts after the block reference `^anchor`. Fails if the note does not exist — use `notes.create` first. `replace` mode is idempotent-destructive; the others are additive.", inputSchema: notesEditArgsSchema, outputSchema: mutationResultSchema, annotations: ADDITIVE, handler: async (context, rawArgs) => { const args = notesEditArgsSchema.parse(rawArgs) as NotesEditArgs; return handleEdit(context, args); }, inputExamples: [ { description: "Append a new journal entry to the end of today's note", input: { mode: "append", path: "Journal/2026-04-24.md", content: "\n## Afternoon\n\nFinished the tool consolidation.", }, }, { description: "Insert content after a specific heading", input: { mode: "after-heading", path: "Projects/Alpha.md", anchor: "Open questions", content: "- Do we need to bump the Zod major?\n", }, }, { description: "Insert after a block reference", input: { mode: "after-block", path: "Notes/idea.md", anchor: "idea-1", content: "Follow-up thought …", }, }, ], }, - src/schema/notes.ts:115-125 (schema)Input schema for notes.edit — a discriminated union on 'mode' with five variants: replace, append, prepend, after-heading, after-block.
export const notesEditArgsSchema = z .discriminatedUnion("mode", [ editReplaceShape, editAppendShape, editPrependShape, editAfterHeadingShape, editAfterBlockShape, ]) .describe( "Discriminated union on `mode`. `replace` overwrites the whole note body; `append` adds to the end; `prepend` adds to the start (after frontmatter); `after-heading` inserts after a heading (anchor = heading text, no `#`); `after-block` inserts after a block reference (anchor = block id, no `^`).", ); - src/schema/notes.ts:70-113 (schema)Individual Zod shapes for each notes.edit mode variant, defining the required and optional fields (path, content, anchor, vaultPath).
const editReplaceShape = z.object({ mode: z.literal("replace"), path: notePathSchema, content: z.string().describe("Replacement body for the entire note."), vaultPath: z.string().optional(), }); const editAppendShape = z.object({ mode: z.literal("append"), path: notePathSchema, content: z.string(), vaultPath: z.string().optional(), }); const editPrependShape = z.object({ mode: z.literal("prepend"), path: notePathSchema, content: z.string(), vaultPath: z.string().optional(), }); const editAfterHeadingShape = z.object({ mode: z.literal("after-heading"), path: notePathSchema, content: z.string(), anchor: z .string() .min(1) .describe( "Heading text (without the leading `#`s) to insert after. Matches the first heading in the note that has this exact text.", ), vaultPath: z.string().optional(), }); const editAfterBlockShape = z.object({ mode: z.literal("after-block"), path: notePathSchema, content: z.string(), anchor: z .string() .min(1) .describe("Block id (without the `^` prefix) to insert after. Obsidian block refs only."), vaultPath: z.string().optional(), }); - src/domain/smart-insert.ts:6-83 (helper)Supporting domain functions used by notes.edit handleEdit: insertAfterHeading, insertAfterBlock, and appendToNote (along with updateNote in notes.ts for replace/prepend).
export async function insertAfterHeading( context: DomainContext, args: { filePath: string; heading: string; content: string; vaultPath?: string }, ) { const vaultRoot = requireVaultPath(context, args.vaultPath); const absolutePath = resolveVaultPath(vaultRoot, args.filePath); const original = await readUtf8(absolutePath); const lines = original.split(/\r?\n/); const index = lines.findIndex((line) => new RegExp(`^#{1,6}\\s+${escapeRegExp(args.heading)}\\s*$`).test(line), ); if (index < 0) { return { changed: false, target: args.filePath, error: `Heading '${args.heading}' not found` }; } lines.splice(index + 1, 0, args.content); await writeUtf8(absolutePath, `${lines.join("\n")}${original.endsWith("\n") ? "\n" : ""}`); return { changed: true, target: args.filePath, summary: `Inserted content after heading ${args.heading}`, }; } export async function insertAfterBlock( context: DomainContext, args: { filePath: string; blockId: string; content: string; vaultPath?: string }, ) { const vaultRoot = requireVaultPath(context, args.vaultPath); const absolutePath = resolveVaultPath(vaultRoot, args.filePath); const original = await readUtf8(absolutePath); const blockId = args.blockId.startsWith("^") ? args.blockId : `^${args.blockId}`; const lines = original.split(/\r?\n/); const index = lines.findIndex((line) => new RegExp(`\\s${escapeRegExp(blockId)}\\s*$`).test(line), ); if (index < 0) { return { changed: false, target: args.filePath, error: `Block '${blockId}' not found` }; } lines.splice(index + 1, 0, args.content); await writeUtf8(absolutePath, `${lines.join("\n")}${original.endsWith("\n") ? "\n" : ""}`); return { changed: true, target: args.filePath, summary: `Inserted content after block ${blockId}`, }; } export async function updateFrontmatterField( context: DomainContext, args: { filePath: string; field: string; value: unknown; vaultPath?: string }, ) { const vaultRoot = requireVaultPath(context, args.vaultPath); const absolutePath = resolveVaultPath(vaultRoot, args.filePath); const original = await readUtf8(absolutePath); const parsed = parseFrontmatter(original); parsed.data[args.field] = args.value; await writeUtf8(absolutePath, stringifyFrontmatter(parsed)); return { changed: true, target: args.filePath, summary: `Updated frontmatter field ${args.field}`, }; } export async function appendToNote( context: DomainContext, args: { filePath: string; content: string; vaultPath?: string }, ) { const vaultRoot = requireVaultPath(context, args.vaultPath); const absolutePath = resolveVaultPath(vaultRoot, args.filePath); const original = await readUtf8(absolutePath); await writeUtf8(absolutePath, `${original}${args.content}`); return { changed: true, target: args.filePath, summary: `Appended content to ${args.filePath}` }; } export function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }