Journal and undo
The journal records every write to rows and to the schema, with the data to undo it and who made it. list_migrations lists the writes, newest first, and undo_migration reverts one, batches included. An undo is itself a write, so you can undo it too.
The journal works on every table, whatever its history setting. Reading a table as it was and restoring it need its transaction log too. A database the engine creates keeps one for every table: see Table history and time travel.
The examples work on a small diary of five classic films.
The engine rolls back a write it cannot finish
Section titled “The engine rolls back a write it cannot finish”A write can touch many notes, and the engine changes them one at a time. Sometimes it can’t change one of them halfway through:
- Another program holds the file, such as an editor, an antivirus or a sync tool
- Someone edits the note between the check and the change
- Someone deletes or moves the note meanwhile
The engine then puts back what it had already changed. It undoes the journal entry of the interrupted operation, as undo_migration would, so the database stays as it was. You don’t need to undo anything yourself.
The answer has ok: false, the files it couldn’t change in failed and rolled_back: true. The HTTP API answers with status 503, which means nothing changed and you can retry. Its hint tells you to free the file and send the operation again:
{ "ok": false, "op": "update_rows", "migration_id": "20261011T101500-update_rows-update-film", "failed": [ { "path": "datanotes/main/public/film/Nosferatu.md", "error": "EBUSY: resource busy or locked" } ], "rolled_back": true, "notes_changed": 0}Through the MCP server, the agent reads the same answer as text, which says that nothing is left changed.
The journal keeps both steps. list_migrations shows the interrupted operation as undone, and a rollback entry that undid it. You can’t undo a rollback: send the operation again instead. The table history gets no version of the interrupted operation.
Inside a batch, the engine rolls back the interrupted step first. The batch then undoes the steps before it, so the whole batch leaves nothing behind.
On Windows, a file another program has open can’t be renamed for a moment. The engine retries a move for about three seconds before it gives up.
When the rollback can’t finish either
Section titled “When the rollback can’t finish either”The rollback can stop too, for example when the same program still holds a file. The answer then has rolled_back: false. failed lists the file that stopped the operation and each file the rollback left, with the error not rolled back: ….
To finish the rollback:
- Close the program that holds the files.
- Call
undo_migrationwith the operation’smigration_id. - Send the operation again.
undo_migration skips the files the rollback already put back, so you can call it again until it succeeds.
What undo can’t take back
Section titled “What undo can’t take back”undo_migration reverts the writes the journal holds. Some changes never reach the journal, so no undo brings them back:
vacuum_table: it deletes old versions of a table’s history for good. Itsdry_runis on by default.configure_database: the database settings aren’t journaled. Set the old value again withconfigure_database.- Webhooks:
create_webhookanddelete_webhookaren’t journaled. Delete or create the webhook again. sync_semantic_index: it rebuilds the semantic index, which the engine can build again at any time.- Hand edits: the journal doesn’t record a note you edit outside the engine. With history on, the table’s log records it, and
restore_tablebrings back the version before it. Without history, nothing can. - Old entries: when the engine starts, it drops journal entries older than
journalRetentionDays, 90 days by default.list_migrationsno longer lists them, and they can’t be undone. - A crash or
kill -9: a write cut short this way may leave some notes written and no journal entry. Find the rows it left withcheck_table, and repair them with a new write.
restore_database doesn’t fill these gaps either: it goes back only through tables with history on. To revert one operation, use undo_migration.
Undo a write
Section titled “Undo a write”Every write returns a migration_id. list_migrations lists them, newest first, with who made each one, and undo_migration reverts one.
list_migrations
{ "table": "film", "limit": 3 }Answer:
{ "total": 13, "migrations": [ { "migration_id": "20261012T165223-delete_rows-archive-1-rows-of-film", "op": "delete_rows", "summary": "archive 1 rows of film", "table": "main.public.film", "executed_at": "2026-10-12T16:52:23.376Z", "notes_changed": 0, "files_changed": 1, "undone": false, "files_failed": 0, "by": "docs-example" }, { "migration_id": "20261012T165223-merge_rows-merge-2-rows-into-film", "op": "merge_rows", "summary": "merge 2 rows into film", "table": "main.public.film", "executed_at": "2026-10-12T16:52:23.360Z", "notes_changed": 1, "files_changed": 1, "undone": false, "files_failed": 0, "by": "docs-example" }, { "migration_id": "20261012T165223-update_rows-update-film", "op": "update_rows", "summary": "update film", "table": "main.public.film", "executed_at": "2026-10-12T16:52:23.346Z", "notes_changed": 3, "files_changed": 0, "undone": false, "files_failed": 0, "by": "docs-example" } ]}undo_migration
{ "migration_id": "20261012T165223-delete_rows-archive-1-rows-of-film" }Answer:
{ "ok": true, "op": "undo_migration", "undoes": "20261012T165223-delete_rows-archive-1-rows-of-film", "migration_id": "20261012T165223-undo_migration-undo-20261012t165223-delete_ro", "conflicts": [], "failed": []}Battleship Potemkin is back:
select_rows
{ "table": "film", "columns": ["title"], "where": { "title": "Battleship Potemkin" } }Answer:
{ "table": "main.public.film", "total": 1, "rows": [ { "path": "datanotes/main/public/film/Battleship Potemkin.md", "title": "Battleship Potemkin" } ]}Undo refuses to overwrite later changes
Section titled “Undo refuses to overwrite later changes”An undo never overwrites later work. The engine compares the values the operation wrote with the notes as they are now. If one of those values changed since, the engine refuses the undo and lists the changes. Changes to other columns don’t block the undo, and the engine keeps them.
update_row
{ "table": "film", "path": "datanotes/main/public/film/Sherlock Jr..md", "set": { "rating": 5 }}Answer:
{ "ok": true, "op": "update_row", "notes_changed": 1, "planned_edits": [ { "path": "datanotes/main/public/film/Sherlock Jr..md", "set": { "rating": 5 }, "remove": [] } ], "commits": [{ "table": "main.public.film", "version": 14, "operation": "UPDATE" }], "migration_id": "20261012T165223-update_row-update-film-5"}Then someone changes the rating again:
update_row
{ "table": "film", "path": "datanotes/main/public/film/Sherlock Jr..md", "set": { "rating": 4 }}undo_migration
{ "migration_id": "20261012T165223-update_row-update-film-5" }Refused:
undo_migration refused — nothing was written:conflict datanotes/main/public/film/Sherlock Jr..md: "rating" changed since the operationHint: Later changes touched the same notes: undo those first (list_migrations lists them, newest first), or change the rows with update_row.Docs: https://datanotes.dev/guides/history/journal-and-undo.md#undo-refuses-to-overwrite-later-changes (offline: read_docs("guides/history/journal-and-undo#undo-refuses-to-overwrite-later-changes"))The undo of an insert trashes the new note only while it’s as the insert left it:
insert_row
{ "table": "film", "values": { "title": "The General", "year": 1926 } }update_row
{ "table": "film", "path": "datanotes/main/public/film/The General.md", "body": "Buster Keaton drives a locomotive.\n"}undo_migration
{ "migration_id": "20261012T165223-insert_row-insert-into-film-6" }Refused:
undo_migration refused — nothing was written:conflict datanotes/main/public/film/The General.md: created note changed since the operationHint: Later changes touched the same notes: undo those first (list_migrations lists them, newest first), or change the rows with update_row.Docs: https://datanotes.dev/guides/history/journal-and-undo.md#undo-refuses-to-overwrite-later-changes (offline: read_docs("guides/history/journal-and-undo#undo-refuses-to-overwrite-later-changes"))