Write tools

~7 min read

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:

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:

  1. writes the page at path,

  2. links it under index.md’s ## Pages heading (override with index_section), and

  3. appends 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).

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

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
---