Skip to content

Write and move rows

The engine checks every write before it changes anything, records it in the journal, and lets you undo it with undo_migration. A write answers with the notes it created, changed, moved or deleted.

The first example adds a row to the film diary of Tables; the others work on a small diary of five classic films.

When several clients write at once, the engine checks each write on the latest data. Concurrent writes explains the rules and when to retry.

insert_row writes a new note. The engine fills in the default (status) and an id, and names the note from filename. body sets the note’s text.

insert_row

{
"table": "film",
"values": { "title": "Metropolis", "year": 1927, "minutes": 153 },
"body": "Next on my list. Two and a half hours of silent film need a quiet evening.\n"
}

Answer:

{
"ok": true,
"op": "insert_row",
"files_created": 1,
"planned_creates": ["datanotes/main/public/film/Metropolis.md"],
"commits": [{ "table": "main.public.film", "version": 1, "operation": "WRITE" }],
"migration_id": "20261012T165219-insert_row-insert-into-film"
}

datanotes/main/public/film/Metropolis.md now reads:

---
table: main.public.film
id: 01a13107-85bc-7ff7-84d8-05f5c1334887
title: Metropolis
year: 1927
minutes: 153
status: to watch
---
Next on my list. Two and a half hours of silent film need a quiet evening.

Without path, the table’s filename pattern names the new note. When another note has that name, the engine adds -2, -3 and so on.

A path you pass must be free. Windows and macOS see Nosferatu.md and nosferatu.md as one file. So on every system, the engine refuses a name that another note has in another case, and names that note:

insert_row

{
"table": "film",
"values": { "title": "The Kid", "year": 1921 },
"path": "datanotes/main/public/film/nosferatu.md"
}

Refused:

insert_row refused — nothing was written:
conflict datanotes/main/public/film/nosferatu.md: note already exists in another case: "datanotes/main/public/film/Nosferatu.md" (one file on Windows and macOS)
Hint: Another note has this file name, maybe in another case (Windows and macOS see one file): pass another "path", give the row values its table's filename pattern turns into a free name, or update that note with update_row. Then call insert_row again.
Docs: https://datanotes.dev/guides/read-and-write/write-rows.md#pick-a-free-file-name-for-a-new-row (offline: read_docs("guides/read-and-write/write-rows#pick-a-free-file-name-for-a-new-row"))

update_row changes exactly one row, which you address by path or by its id. set sets columns and unset removes them. The engine checks the whole row before it writes anything.

update_row

{
"table": "film",
"path": "datanotes/main/public/film/His Girl Friday.md",
"set": { "status": "watched", "rating": 5, "tags": ["screwball"] }
}

Answer:

{
"ok": true,
"op": "update_row",
"notes_changed": 1,
"planned_edits": [
{
"path": "datanotes/main/public/film/His Girl Friday.md",
"set": { "status": "watched", "rating": 5, "tags": ["screwball"] },
"remove": []
}
],
"commits": [{ "table": "main.public.film", "version": 6, "operation": "UPDATE" }],
"migration_id": "20261012T165223-update_row-update-film"
}

update_row

{
"table": "film",
"path": "datanotes/main/public/film/His Girl Friday.md",
"unset": ["tags"]
}

The engine refuses a path that isn’t a row, and creates nothing:

update_row

{
"table": "film",
"path": "datanotes/main/public/film/Night of the Living Dead.md",
"set": { "rating": 4 }
}

Refused:

film has no row at datanotes/main/public/film/Night of the Living Dead.md
Hint: Name the row by its "path" (the vault path of its note, e.g. "Films/Metropolis.md") or its "id": select_rows with columns ["path", "id"] and a "where" finds them.
Docs: https://datanotes.dev/guides/read-and-write/write-rows.md#update-one-row (offline: read_docs("guides/read-and-write/write-rows#update-one-row"))

update_row edits the note’s text with these arguments:

  • body_sections: replaces the text under one heading and leaves the rest of the note alone; the engine adds a missing heading at the end
  • body_append and body_prepend: add text after or before the existing text
  • body: replaces all of the text

update_row

{
"table": "film",
"path": "datanotes/main/public/film/Nosferatu.md",
"body_sections": {
"Rewatch": "Watched it again in October. The shadow on the stairs still works a century later."
},
"body_append": "Next: Metropolis, for more German Expressionism.\n"
}

Answer:

{
"ok": true,
"op": "update_row",
"notes_changed": 1,
"planned_edits": [
{
"path": "datanotes/main/public/film/Nosferatu.md",
"set": {},
"remove": [],
"body": true
}
],
"commits": [{ "table": "main.public.film", "version": 8, "operation": "UPDATE" }],
"migration_id": "20261012T165223-update_row-update-film-3",
"ambiguous_mentions": [
{
"path": "datanotes/main/public/film/Nosferatu.md",
"text": "Metropolis",
"reason": "sentence_start",
"candidates": ["datanotes/main/public/film/Metropolis.md"],
"context": "Next: Metropolis, for more German Expressionism.",
"hint": "\"Metropolis\" starts a sentence, a line or a label, so its capital does not make it a name: read the note and the text around it, then write the link with update_row and autolink: false, or leave the text as it is."
}
]
}

datanotes/main/public/film/Nosferatu.md now reads:

---
table: main.public.film
id: 01a13107-9666-7510-ae12-2ba7a197a17a
title: Nosferatu
year: 1922
status: watched
rating: 5
---
## Notes
Max Schreck doesn’t need a single word.
## Rewatch
Watched it again in October. The shadow on the stairs still works a century later.
Next: Metropolis, for more German Expressionism.

The engine links Metropolis, the note of another film, and lists the link in links_added. Automatic links shows which names it links.

Guard a write against concurrent edits with expect

Section titled “Guard a write against concurrent edits with expect”

A person may edit a note between an agent’s read and its write. expect makes the write a compare-and-set: it goes through only if the row still has those values.

update_row

{
"table": "film",
"path": "datanotes/main/public/film/Sherlock Jr..md",
"expect": { "status": "to watch" },
"set": { "status": "watched" }
}

Answer:

{
"ok": true,
"op": "update_row",
"notes_changed": 1,
"planned_edits": [
{
"path": "datanotes/main/public/film/Sherlock Jr..md",
"set": { "status": "watched" },
"remove": []
}
],
"commits": [{ "table": "main.public.film", "version": 9, "operation": "UPDATE" }],
"migration_id": "20261012T165223-update_row-update-film-4"
}

Run the same call again. status is no longer to watch, so the engine writes nothing.

update_row

{
"table": "film",
"path": "datanotes/main/public/film/Sherlock Jr..md",
"expect": { "status": "to watch" },
"set": { "status": "watched" }
}

Refused:

update_row refused — nothing was written:
conflict datanotes/main/public/film/Sherlock Jr..md: row changed: expected status = "to watch", found "watched" — read it again
Hint: The row changed since you read it: read it again (select_rows), then retry update_row with "expect" set to what you read.
Docs: https://datanotes.dev/guides/read-and-write/write-rows.md#guard-a-write-against-concurrent-edits-with-expect (offline: read_docs("guides/read-and-write/write-rows#guard-a-write-against-concurrent-edits-with-expect"))

update_rows changes every row matching where. expect_count guards the write: the engine refuses it unless exactly that many rows match, so a filter that’s too broad changes nothing.

update_rows

{
"table": "film",
"where": { "status": "watched" },
"set": { "tags": ["seen-2026"] },
"expect_count": 2
}

Refused:

update_rows refused — nothing was written:
conflict datanotes/main/public/film: "where" matched 3 rows, expected 2: nothing was written
Hint: Check the rows with select_rows and the same "where", then narrow it or change expect_count.
Docs: https://datanotes.dev/guides/read-and-write/write-rows.md#update-many-rows (offline: read_docs("guides/read-and-write/write-rows#update-many-rows"))

update_rows

{
"table": "film",
"where": { "status": "watched" },
"set": { "tags": ["seen-2026"] },
"expect_count": 3
}

Answer:

{
"ok": true,
"op": "update_rows",
"notes_changed": 3,
"planned_edits": [
{
"path": "datanotes/main/public/film/His Girl Friday.md",
"set": { "tags": ["seen-2026"] },
"remove": []
},
{
"path": "datanotes/main/public/film/Nosferatu.md",
"set": { "tags": ["seen-2026"] },
"remove": []
},
{
"path": "datanotes/main/public/film/Sherlock Jr..md",
"set": { "tags": ["seen-2026"] },
"remove": []
}
],
"commits": [{ "table": "main.public.film", "version": 10, "operation": "UPDATE" }],
"migration_id": "20261012T165223-update_rows-update-film"
}

merge_rows is an upsert: it inserts rows or updates existing ones. A row whose on columns match an existing row updates that row, and the engine inserts the others. The whole merge is one operation with one undo.

merge_rows

{
"table": "film",
"on": ["title"],
"rows": [
{ "title": "Sherlock Jr.", "rating": 4 },
{ "title": "Battleship Potemkin", "year": 1925 }
]
}

Answer:

{
"ok": true,
"op": "merge_rows",
"notes_changed": 1,
"files_created": 1,
"planned_edits": [
{
"path": "datanotes/main/public/film/Sherlock Jr..md",
"set": { "rating": 4 },
"remove": []
}
],
"planned_creates": ["datanotes/main/public/film/Battleship Potemkin.md"],
"commits": [{ "table": "main.public.film", "version": 11, "operation": "MERGE" }],
"migration_id": "20261012T165223-merge_rows-merge-2-rows-into-film"
}

select_rows

{
"table": "film",
"columns": ["title", "rating", "status"],
"where": { "title": { "in": ["Sherlock Jr.", "Battleship Potemkin"] } }
}

Answer:

{
"table": "main.public.film",
"total": 2,
"rows": [
{
"path": "datanotes/main/public/film/Battleship Potemkin.md",
"title": "Battleship Potemkin",
"rating": null,
"status": "to watch"
},
{
"path": "datanotes/main/public/film/Sherlock Jr..md",
"title": "Sherlock Jr.",
"rating": 4,
"status": "watched"
}
]
}

By default, delete_rows moves the matching rows to the table’s archive folder, so you lose nothing. mode: "delete" moves them to the trash instead, and the journal keeps their content for undo. Always pass where: without it, every row matches.

delete_rows

{ "table": "film", "where": { "title": "Battleship Potemkin" } }

Answer:

{
"ok": true,
"op": "delete_rows",
"files_moved": 1,
"planned_moves": [
{
"from": "datanotes/main/public/film/Battleship Potemkin.md",
"to": "datanotes/main/public/film/_archive/Battleship Potemkin.md"
}
],
"commits": [{ "table": "main.public.film", "version": 12, "operation": "DELETE" }],
"migration_id": "20261012T165223-delete_rows-archive-1-rows-of-film"
}

list_folder

{ "folder": "datanotes/main/public/film/_archive" }

Answer:

{
"folder": "datanotes/main/public/film/_archive",
"folders": [],
"files": ["datanotes/main/public/film/_archive/Battleship Potemkin.md"]
}

move_rows moves rows to another table. During the move, it can rename keys, set values and drop keys, and it checks every row against the destination first. The engine reports keys the destination doesn’t know and never drops them silently. relocate: true also moves the notes to the destination’s folder.

create_table

{
"name": "watchlist",
"description": "Films I plan to watch soon",
"columns": [
{
"name": "title",
"type": "string",
"required": true,
"description": "The film's title"
},
{ "name": "released", "type": "integer", "description": "Year the film came out" }
],
"filename": "{title}"
}

move_rows

{
"table": "film",
"to_table": "watchlist",
"where": { "status": "to watch" },
"relocate": true
}

Refused:

move_rows refused — nothing was written:
invalid datanotes/main/public/film/Battleship Potemkin.md: year is not a column of table "watchlist"
invalid datanotes/main/public/film/Battleship Potemkin.md: status is not a column of table "watchlist"
invalid datanotes/main/public/film/Charade.md: year is not a column of table "watchlist"
invalid datanotes/main/public/film/Charade.md: status is not a column of table "watchlist"
invalid datanotes/main/public/film/Metropolis.md: year is not a column of table "watchlist"
invalid datanotes/main/public/film/Metropolis.md: status is not a column of table "watchlist"
invalid datanotes/main/public/film/The General.md: year is not a column of table "watchlist"
invalid datanotes/main/public/film/The General.md: status is not a column of table "watchlist"
Hint: Write only the table's columns: describe_table lists them. To keep a new front matter key, add_column first. Then call move_rows again (dry_run: true checks without writing).
Docs: https://datanotes.dev/guides/database-objects/tables.md#describe-a-table-before-writing-to-it (offline: read_docs("guides/database-objects/tables#describe-a-table-before-writing-to-it"))

The watchlist has no status, tags or rating column, so the call drops them, and year becomes released:

move_rows

{
"table": "film",
"to_table": "watchlist",
"where": { "status": "to watch" },
"rename": { "year": "released" },
"drop": ["status", "tags", "rating"],
"relocate": true
}
Answer (51 lines)
{
"ok": true,
"op": "move_rows",
"notes_changed": 4,
"files_moved": 4,
"planned_edits": [
{
"path": "datanotes/main/public/film/Battleship Potemkin.md",
"set": { "table": "main.public.watchlist", "released": 1925 },
"remove": ["year", "status"]
},
{
"path": "datanotes/main/public/film/Charade.md",
"set": { "table": "main.public.watchlist", "released": 1963 },
"remove": ["year", "status"]
},
{
"path": "datanotes/main/public/film/Metropolis.md",
"set": { "table": "main.public.watchlist", "released": 1927 },
"remove": ["year", "status"]
},
{
"path": "datanotes/main/public/film/The General.md",
"set": { "table": "main.public.watchlist", "released": 1926 },
"remove": ["year", "status"]
}
],
"planned_moves": [
{
"from": "datanotes/main/public/film/Battleship Potemkin.md",
"to": "datanotes/main/public/watchlist/Battleship Potemkin.md"
},
{
"from": "datanotes/main/public/film/Charade.md",
"to": "datanotes/main/public/watchlist/Charade.md"
},
{
"from": "datanotes/main/public/film/Metropolis.md",
"to": "datanotes/main/public/watchlist/Metropolis.md"
},
{
"from": "datanotes/main/public/film/The General.md",
"to": "datanotes/main/public/watchlist/The General.md"
}
],
"commits": [
{ "table": "main.public.film", "version": 22, "operation": "MOVE ROWS" },
{ "table": "main.public.watchlist", "version": 1, "operation": "MOVE ROWS" }
],
"migration_id": "20261012T165223-move_rows-move-4-rows-from-film-to-watch"
}