Write tools
Write tools
The write tools all require the write scope. They write directly to the vault — except when the calling key is propose-only, in which case every write is intercepted and turned into a reviewable proposal instead (see Suggestions mode). There is no separate “propose” tool: you call the same write_note / patch_section / apply_edits, and the key’s mode decides whether it commits or queues.
| Tool | Effect | Requires scope |
|---|---|---|
write_note |
Create, overwrite, append, or prepend a note | write |
patch_section |
Patch a heading / block / frontmatter target | write |
apply_edits |
Apply a sequence of atomic find/replace edits | write |
file_page |
Write a new page + link it in index.md + log it |
write |
move_note |
Rename/move a note and rewrite inbound links | write |
delete_note |
Soft-delete (or hard-delete) a note | write |
write_note
Create a note or write to an existing one. The mode picks the behaviour.
Input:
{
"path": "Projects/New project.md",
"content": "---\ntitle: ...\n---\n\n# ...",
"mode": "overwrite" // create | overwrite | append | prepend (default overwrite)
}
mode |
Behaviour |
|---|---|
create |
Fail if the note already exists |
overwrite |
Create or fully replace (default) |
append |
Add content to the end |
prepend |
Insert content after the frontmatter |
Behaviour:
Creates intermediate folders as needed.
Atomic via temp-file + rename.
Sets up the link/tag index entry immediately.
Notifies the editor if the note is currently open (doesn’t overwrite the user’s unsaved buffer).
patch_section
Patch just one part of a note rather than rewriting the whole body.
Input:
{
"path": "Inbox/random thought.md",
"target_type": "heading", // heading | block | frontmatter
"target": "## Notes", // the heading text, block id, or frontmatter key
"op": "append", // append | prepend | replace
"content": "- New observation\n"
}
For target_type: "frontmatter", target is the key and the op sets/replaces its value.
Errors: target not found, invalid path.
apply_edits
Apply a sequence of find/replace edits to a note atomically — sed-like edits without targeting heading or block ids. Each find must match exactly once. If any edit doesn’t match, or matches more than once (ambiguous), the whole call rolls back and the file is unchanged.
Input:
{
"path": "Inbox/random thought.md",
"edits": [
{"find": "status: draft", "replace": "status: in-progress"},
{"find": "## Notes", "replace": "## Notes\n\n- New observation"}
]
}
Returns: the updated note path and the applied count.
There is no base_sha256 / optimistic-concurrency parameter on apply_edits. The exactly-once-match rule is itself the safety guard: if the file drifted such that an anchor string is now missing or duplicated, the edit fails loudly instead of writing to the wrong place. To re-anchor, read_note again and rebuild the find strings.
Errors: Note not found, Edit matched N times — \find` must be unique, find` not found.
file_page
Write a new wiki page and maintain the catalog and ledger in one call. It:
writes the page at
path,links it under
index.md’s## Pagesheading (override withindex_section), andappends a dated entry to
log.md.
Use it when filing an ingest summary or a durable answer so the wiki compounds. To update an existing page, use patch_section instead.
Input:
{
"path": "Pages/Vector databases.md",
"content": "---\ntitle: Vector databases\n---\n\n# ...",
"title": "Vector databases", // optional; for the catalog link
"summary": "One-line catalog summary", // optional
"index_section": "Pages", // optional; the index.md heading to link under
"log_op": "ingest" // optional; the verb in the log entry
}
file_page is available to propose-only keys: it decomposes into a reviewable batch — the page (create), the index.md catalog bullet (prepend), and the log.md ledger entry (append), all sharing one batchId — so a human accepts or rejects the whole compounding step together. The returned receipt is {status: "pending", batchId, proposalIds, count}.
wiki_maintain
Close the lint loop: runs the semantic health check and files each new finding as a > [!ai] directive — into the cited page when it’s a real, reviewable note, otherwise into a dedicated Lint findings.md review note (never the catalog index.md). The model’s wording is filed as an unverified lead to investigate, not an instruction, and capped per pass — so a hostile note can’t launder an executable directive through lint. Deduped against .lettuce/lint-state.json so repeated/scheduled runs don’t flood the vault.
Input: {} · Returns: {filed, skipped, directives}. Requires an LLM provider and a write-scoped key. From the shell: lettuce maintain <vault> (or lettuce lint --semantic --file); also runnable on a cadence via a schedules.yml action: maintain.
ingest_url / ingest_file
Pull a source into the raw sources/ layer so you can then read_note it and file_page a summary — the agent-reachable end of the capture loop (act on a suggested-source finding yourself).
ingest_url{url}— fetches the page (http/https), runs readability, localizes images, writessources/<slug>.mdwith provenance frontmatter.ingest_file{path}— converts an existing vault file (vault-relativepath;pdf/docx/odt/rtf/epub/html/md/…) intosources/.
Returns: {path}. Both write-scoped; both de-duplicate (skip an exact re-ingest and an embedding near-duplicate) and fire any file:created:sources/** trigger. Non-HTML/PDF formats need pandoc on PATH.
move_note / delete_note
move_note(from,to,update_linksdefault true) renames/moves a note and rewrites inbound links — both[[wikilinks]]and plain[text](path.md)Markdown links (the latter is what Karpathy-style catalogs use). Markdown links are repointed to the note’s new location, recomputing the relative path and preserving any#fragment. (Inline links only; reference-style[label][ref]definitions are left untouched.)The response includes
updatedLinks(how many links were repointed) andrewrittenPaths(which source notes were touched by the relink) — use the latter to know what else changed.rename_folderreports the same asrewritten_paths: folder renames repoint path-qualified links ([[folder/note]],[x](folder/note.md)); basename[[note]]links need no rewriting and keep the text you wrote.delete_note(path,confirm,hard) soft-deletes to.trashunlesshard: true; requiresconfirm: true.
Both work under Suggestions mode (they map to a single-file preview). The only writes a propose-only key cannot use are the folder tools — create_folder / rename_folder / delete_folder — which reshape the directory tree and have no proposal shape in Suggestions mode v1. (file_page is supported — it decomposes into a batch, as above.)
Patterns
Create a note safely
client.tools.write_note(
path=f"Inbox/{title}.md",
content=body,
mode="create" # fail instead of clobbering if a human got there first
)
Edit by re-anchoring
note = client.tools.read_note(path="Projects/Lettuce.md")
# Build find strings from the body you just read, then:
client.tools.apply_edits(
path=note.path,
edits=[{"find": "status: building", "replace": "status: shipped"}]
)
# If the edit fails (not found / ambiguous), re-read and rebuild the anchors.
File a durable page
client.tools.file_page(
path="Pages/Vector databases.md",
content=body,
title="Vector databases",
summary="What they are and when to reach for one",
)
# The page is written, linked under index.md ## Pages, and logged.
When the key is propose-only, the same write_note / patch_section / apply_edits calls return a pending receipt with a proposalId instead of committing. See Agent reference/06 Suggestion tools for how to track those.
Immutable paths (the raw sources/ layer)
The raw ingest layer is write-once. By default, files under sources/ are immutable: you can create a new file there (this is how lettuce ingest and URL fetches add provenance) and append to an existing one, but write_note mode=overwrite/prepend, patch_section, apply_edits, delete_note, and move_note (moving a source out) on an existing source are rejected with a clear error. Moving a note into sources/ is allowed (it’s an additive create).
This protects provenance — an agent can’t silently rewrite the source a page was derived from. To work with a source, derive a new note from it instead.
get_vault_info returns the active prefixes as immutablePaths, so an agent knows the rule up front. A vault overrides the default with an immutable: list in its index.md frontmatter (mirroring the lint: schema):
---
immutable: [sources/, vendor/] # protect these; `immutable: []` turns the guard off
---
Related
Agent reference/06 Suggestion tools — managing your own pending proposals
Agent reference/07 Concurrency and soft-locks — what happens under contention
Suggestions mode/01 Overview — the human side of propose-only keys