English
updateNotes: programmatic note editing
updateNotes is the GraphQL mutation for programmatically modifying note content. It handles four operations in a single call — create/replace (upsert), atomic find-and-replace (patch), changing frontmatter keys (meta), and hiding a note from public view — and commits changes immediately without a separate commitNotes call.
Use updateNotes when you know exactly what to change. For a full vault sync from an Obsidian plugin, pushNotes + commitNotes remains the right path.
Mutation signature
mutation UpdateNotes($input: UpdateNotesInput!) {
updateNotes(input: $input) {
... on UpdateNotesSuccessPayload { paths }
... on UpdateNotesHashMismatchPayload { path actualHash }
... on UpdateNotesPatchNotFoundPayload { path find }
... on ErrorPayload { message }
}
}
Variables:
{
"input": {
"changes": [ /* one or more NoteChangeInput objects */ ]
}
}
Operation types
Each item in changes is a NoteChangeInput with exactly one of four fields set.
upsert — create or fully replace a note
{
"upsert": {
"path": "inbox.md",
"content": "# Inbox\n\n- item"
}
}
Creates the note if it does not exist; replaces content entirely if it does.
Optional expectedHash: sha256 of the current content, base64url-encoded. If the note has changed since you read it, the mutation returns UpdateNotesHashMismatchPayload instead of writing.
patch — atomic find-and-replace within a note
{
"patch": {
"path": "todo.md",
"find": "- [ ] buy milk",
"replace": "- [x] buy milk"
}
}
The server reads the current content, replaces the first (and only) occurrence of find with replace, and saves — all in one transaction.
Rules:
findmust appear exactly once. If absent or ambiguous, the mutation returnsUpdateNotesPatchNotFoundPayloadwith thepathandfindstring that failed.findis an exact string match, not a regex.- If
findequalsreplace, the note is unchanged, no new version is created, and no change event is raised: no webhooks fire and re-embedding is skipped. - Empty
replaceis valid — it deletes the matched string.
Optional expectedHash works the same as for upsert.
meta — change frontmatter keys
{
"meta": {
"path": "todo.md",
"set": { "status": "done", "priority": 2 },
"unset": ["due"]
}
}
Changes top-level keys of the note's frontmatter without sending the whole note or the exact text of the old value.
Rules:
setadds a key that is absent and replaces the value of one that is present. A value can be a string, number, boolean, list, object ornull.unsetremoves keys. Removing a key the note does not have is not an error.- Only top-level keys:
"og.title"is a key literally namedog.title, nottitleinsideog. To change a nested value, set the whole top-level key. - Every other key keeps its line, its order, its quoting and its comments. The note body is not touched. New keys are added after the last key, in sorted order.
- A note without frontmatter gets a
---block at the top; the body below it stays byte for byte the same. - Setting a key to the string, number or boolean it already holds keeps the line as it is, and the note gets no new version.
- A string that starts with a line break, or has a line of dashes only such as
---, is written double-quoted on one line,"one\n---\ntwo", so it keeps its value and cannot close the frontmatter early. - A
...line ends the YAML document: keys after it are outside the frontmatter, trip2g does not read them as meta, and the edit leaves them alone. - JSON does not tell
1.0from1, so a whole number is written without a fraction:1.0lands as1; send"1.0"as a string to keep it. - The note must exist. A missing note, or frontmatter that cannot be edited key by key (invalid YAML, a
{...}flow mapping, keys indented from the start of the line, a YAML alias such as*name, a key declared twice, a block with no closing---), returns anErrorPayload. - The edited note is read back before it is saved. If any key other than the named ones would read differently, or a named key would not get its value, nothing is saved and the mutation returns an
ErrorPayload. setandunsetcannot both be empty, one key cannot be in both, and a key cannot be blank or contain a line break or another control character.
Optional expectedHash works the same as for patch.
hide — remove a note from public view
{
"hide": {
"path": "draft.md"
}
}
Marks the note as hidden without changing its content. The note remains in the vault but is no longer served to readers. The hide is recorded against an admin: the signed-in admin, the admin who created the API key, or the admin who created the webhook that issued the token.
Checkbox toggle (patch use case)
The patch operation is the standard mechanism for toggling task checkboxes from a page.
How it works:
- The note is rendered with
allow_task_toggle: truein its frontmatter. The renderer addsdata-lineattributes to checkbox list items containing the original markdown line. - When a reader clicks a checkbox, the frontend reads
data-linefrom the element and callsupdateNoteswith:find: the original line, e.g.- [ ] buy milkreplace: the toggled line, e.g.- [x] buy milk(or reverse to uncheck)
- On success, the UI updates optimistically.
sequenceDiagram
participant Reader
participant Frontend
participant Server
Note over Frontend: Note rendered with data-line attrs<br/>(allow_task_toggle: true)
Reader->>Frontend: Click checkbox
Frontend->>Server: updateNotes patch<br/>find=unchecked, replace=checked
alt find appears exactly once
Server-->>Frontend: UpdateNotesSuccessPayload
Frontend->>Reader: Optimistic UI update
else duplicate task text
Server-->>Frontend: UpdateNotesPatchNotFoundPayload
Frontend->>Reader: cannot toggle - duplicate task
end
Uniqueness requirement. If the same task text appears more than once in the note, patch returns UpdateNotesPatchNotFoundPayload. The frontend should handle this by showing a "cannot toggle — duplicate task text" message rather than silently failing.
Enable checkbox toggling with frontmatter:
allow_task_toggle: true
To enable it across all notes, use a frontmatter patch:
* → { allow_task_toggle: true }
Conflict detection with expectedHash
expectedHash is the sha256 hash of the note's current content, encoded as base64url.
When provided:
- The server checks the hash before applying the change.
- If the content has changed since you read it, the mutation returns
UpdateNotesHashMismatchPayloadwithpathand the currentactualHash. - Retry by re-fetching the note and repeating the operation with the new hash.
flowchart TD
A[Read note + content hash] --> B[Transform content]
B --> C[updateNotes with expectedHash]
C --> D{Hash still<br/>matches?}
D -->|Yes| E[UpdateNotesSuccessPayload<br/>change applied]
D -->|No| F[UpdateNotesHashMismatchPayload<br/>actualHash]
F --> A
For checkbox toggles triggered directly from the UI, expectedHash is typically not needed — the find string itself acts as a guard. If the note changed and the line no longer exists exactly, the patch returns PatchNotFound instead of clobbering the new content.
Use expectedHash when your agent reads a note, transforms it, and writes back — and you need to guarantee no concurrent edit slipped in between.
Batch operations
Send multiple changes in one call:
{
"input": {
"changes": [
{
"patch": {
"path": "todo.md",
"find": "- [ ] buy milk",
"replace": "- [x] buy milk"
}
},
{
"upsert": {
"path": "log/2026-05-18.md",
"content": "# Log\n\nTask completed."
}
}
]
}
}
Each change is processed independently. The batch stops at the first error and returns the error payload — changes before the failed item are already applied. Empty NoteChangeInput objects {} are silently skipped.
Error responses
| Payload | Meaning | Recommended action |
|---|---|---|
UpdateNotesSuccessPayload |
All changes applied | Read paths for the list of affected note paths; updated[].warnings holds each note's warnings after the save |
UpdateNotesHashMismatchPayload |
Note changed since last read | Re-fetch note, retry with actualHash |
UpdateNotesPatchNotFoundPayload |
find string not found or appears more than once |
Show error to user, or re-fetch and retry with updated find |
ErrorPayload |
Validation or system error | Log message, fix input |
Authentication
updateNotes accepts three kinds of access. They are checked in this order, and the first one present wins.
1. Signed-in admin — full access; write patterns don't apply. Any way trip2g recognises an admin works:
-
a browser session (cookie) — this is how app pages on the same site save, see SPA on top of trip2g;
-
an admin's personal token in a header:
Authorization: Bearer t2g_... -
the same personal token in the URL:
?token=t2g_....
2. An API key — write access is limited to the key's write patterns:
X-Api-Key: your-api-key
If the target path is not covered by the key's write patterns, the mutation returns an ErrorPayload.
3. A short-lived webhook token — for webhook agents, see below:
Authorization: Bearer eyJhbGc...
With none of the three the request fails with missing X-API-Key in request header. hideNotes, uploadNoteAsset and commitNotes work the same way, except that hideNotes and commitNotes answer the webhook token with an ErrorPayload; a webhook agent hides a note with the hide change instead.
Integration with webhooks
Webhook agents receive a temporary api_token in the request payload (when Pass API key is enabled). That token can be used directly with updateNotes to write back to the vault.
The agent response changes[] field also supports patch operations as an alternative to calling updateNotes directly — send "kind": "patch" with find and replace instead of content in a change object. A change with "kind": "meta" and set / unset instead of content edits frontmatter keys with the same rules as the meta operation. See webhooks for the full agent response format.
Cron webhooks can trigger agents on a schedule. A cron agent might, for example, check all todo/** notes for overdue tasks and patch their status fields — running nightly without any external infrastructure.
Anchor-marker pattern
For append-style operations (inbox bots, log entries), place a marker string in the note and use patch to insert before or after it while keeping the marker in place:
Note content:
# Inbox
$INBOX$
Agent call:
{
"patch": {
"path": "inbox.md",
"find": "$INBOX$",
"replace": "## 2026-05-18 10:00\nNew message\n\n$INBOX$"
}
}
Each call prepends a new entry above the marker. The marker stays in the note and serves as the insertion point for the next call. This pattern works without expectedHash because the marker string is unique by construction.
Conditional invocation pattern
An agent should call updateNotes only when it has a concrete change to make:
receive webhook payload
if changes[] contains a note with a checkbox to toggle:
call updateNotes with patch (find=unchecked line, replace=checked line)
handle PatchNotFound (duplicate text) → return error to UI
handle HashMismatch → re-fetch and retry
else:
return {"status": "ok"} # no-op, no write needed
The patch semantics enforce this naturally — if the find string is absent, the mutation returns an error rather than silently writing, so agents that call unconditionally also get correct behaviour.