Hand edits
You can create, change, rename or remove a row outside the engine: in an editor, with git or through a sync tool. The engine checks the edit a moment later against the same rules as a write, but never refuses it: the note stays as you wrote it.
The change feed reports what the edit breaks, and check_table lists the rows that are invalid now. The engine fixes what it owns: generated columns, two-way links, and the links to a note you rename or move.
Catch hand edits that break a rule
Section titled “Catch hand edits that break a rule”The engine can’t refuse an edit made outside it, in an editor, through git or by a sync tool. Instead, it checks the edit as soon as it sees it, as check_table would:
- The row’s own rules: types, enums, required columns, links, CHECKs and filtered link targets
- Rows that link to it: when a rule follows links to the edited row
- Dangling links: when you delete or rename a row by hand, the engine reports the links it leaves dangling
The engine reports what the edit breaks in three places:
- Change feed: the event lists the problems in
violations, whereconstraintnames a CHECK; the event leaves out rules the row already broke - Log: a WARN line
- History: when history is on, the
EXTERNAL_EDITversion counts them inoperation_metrics.violations
Here you turn Silent film into a top level by hand, with kind: cinema. It keeps a parent, and Slapstick comedy now hangs from a top level.
First, wait until the change feed includes the rename’s rewrite of Slapstick comedy’s link, and keep the feed’s position:
list_changes
{ "tables": ["genre"], "wait_ms": 10000 }You edit datanotes/main/public/genre/Silent film.md by hand, outside the engine. The note now reads:
---table: main.public.genreid: 01a13107-8fe6-746f-a5a7-fefb251465d8name: Silent filmkind: cinemaparent: "[[Cinema]]"---Read the hand edit’s event. list_changes long-polls with wait_ms until the event arrives:
list_changes
{ "since": 39, "tables": ["genre"], "wait_ms": 10000 }Answer:
[ { "seq": 40, "kind": "update", "path": "datanotes/main/public/genre/Silent film.md", "source": "external", "changes": { "kind": { "before": "era", "after": "cinema" } }, "violations": [ { "path": "datanotes/main/public/genre/Silent film.md", "violations": [ { "code": "check", "constraint": "parent_by_level", "message": "violates CHECK parent_by_level" } ] }, { "path": "datanotes/main/public/genre/Slapstick comedy.md", "violations": [ { "code": "check", "constraint": "parent_by_level", "message": "violates CHECK parent_by_level (after the edit of [[Silent film]], a row it links to)" } ] } ] }]The rows stay as they are until you fix them. check_table lists the rows that are invalid now, and without table it checks every table:
check_table (no arguments)
Answer:
{ "table": null, "rows_checked": 16, "tables_checked": 5, "total": 2, "violations": [ { "path": "datanotes/main/public/genre/Silent film.md", "table": "main.public.genre", "violations": [ { "constraint": "parent_by_level", "code": "check", "message": "violates CHECK parent_by_level: {\"or\":[{\"kind\":\"cinema\",\"parent\":{\"exists\":false}},{\"kind\":\"era\",\"parent.kind\":\"cinema\"},{\"kind\":\"genre\",\"parent.kind\":\"era\"}]}" } ] }, { "path": "datanotes/main/public/genre/Slapstick comedy.md", "table": "main.public.genre", "violations": [ { "constraint": "parent_by_level", "code": "check", "message": "violates CHECK parent_by_level: {\"or\":[{\"kind\":\"cinema\",\"parent\":{\"exists\":false}},{\"kind\":\"era\",\"parent.kind\":\"cinema\"},{\"kind\":\"genre\",\"parent.kind\":\"era\"}]}" } ] } ]}Fix the row through the engine:
update_row
{ "table": "genre", "path": "datanotes/main/public/genre/Silent film.md", "set": { "kind": "era" }}Answer:
{ "ok": true, "op": "update_row", "notes_changed": 1, "planned_edits": [ { "path": "datanotes/main/public/genre/Silent film.md", "set": { "kind": "era" }, "remove": [] } ], "commits": [{ "table": "main.public.genre", "version": 9, "operation": "UPDATE" }], "migration_id": "20261012T165223-update_row-update-genre"}Recompute generated columns after a hand edit
Section titled “Recompute generated columns after a hand edit”An edit made outside the engine, in an editor, through git or by a sync tool, can leave generated columns stale. check_table reports them with code generated, and recompute_columns brings them up to date.
The engine also recomputes them on its own about 3s after a hand edit, unless the autoRecompute setting is off. The examples on this page turn it off so you can see each step. Here you correct where by hand, because you saw His Girl Friday at the cinema, and label still says home.
You edit datanotes/main/public/watch/2026-10-03T2130.md by hand, outside the engine. The note now reads:
---table: main.public.watchid: 01a13107-882d-7a93-a182-8b82b267bc02film: His Girl Fridayoccurred_at: 2026-10-03T21:30where: cinemaday: 2026-10-03label: His Girl Friday (03/10 21:30, home)---check_table
{ "table": "watch" }Answer:
{ "table": "watch", "rows_checked": 1, "total": 1, "violations": [ { "path": "datanotes/main/public/watch/2026-10-03T2130.md", "violations": [ { "column": "label", "code": "generated", "message": "should be \"His Girl Friday (03/10 21:30, cinema)\" (computed)", "value": "His Girl Friday (03/10 21:30, home)" } ] } ]}recompute_columns
{ "table": "watch" }Answer:
{ "ok": true, "op": "recompute_columns", "notes_changed": 1, "planned_edits": [ { "path": "datanotes/main/public/watch/2026-10-03T2130.md", "set": { "label": "His Girl Friday (03/10 21:30, cinema)" }, "remove": [] } ], "commits": [{ "table": "main.public.watch", "version": 3, "operation": "RECOMPUTE" }], "migration_id": "20261012T165221-recompute_columns-recompute-id-day-label-of-watc"}select_rows
{ "table": "watch", "columns": ["where", "label"] }Answer:
{ "table": "main.public.watch", "total": 1, "rows": [ { "path": "datanotes/main/public/watch/2026-10-03T2130.md", "where": "cinema", "label": "His Girl Friday (03/10 21:30, cinema)" } ]}Rename a note by hand
Section titled “Rename a note by hand”You can rename or move a note outside the engine: in a file manager, with git or with a sync tool. The engine then rewrites every link to the note’s old path, across the vault. It rewrites rows and plain notes alike, as it does when it renames a note itself. Here you rename the plain note Notes/Film night.md. A viewing of Charade, a row of the watch table, links to it in its text.
You rename Notes/Film night.md to Notes/Friday film night.md by hand, outside the engine.
The engine waits until the vault has been quiet for 4 seconds, so an editor or a sync tool finishes first. The journal then lists the repair as one operation:
list_migrations
{ "limit": 1 }Answer:
[ { "op": "repair_links", "summary": "repair the links after the hand move of Notes/Film night.md to Notes/Friday film night.md", "notes_changed": 0, "files": null }]The viewing now links to the new name:
read_note
{ "path": "datanotes/main/public/watch/2026-09-25T2100.md" }Answer:
---table: main.public.watchid: 01a13107-ebaa-7f9d-9ee7-44d950f6098doccurred_at: 2026-09-25T21:00film: "[[Charade]]"where: home---Part of [[Friday film night]]. Four of us on the sofa, and nobody guessed the ending.undo_migration takes the repair back. The change feed lists its changes after the move, each with the move in cause. Links that an editor such as Obsidian already rewrote stay as they are: the engine doesn’t write them twice. To leave links as they are after a hand move, set repairLinksOnHandMove to false in the engine’s settings.
Find front matter keys the table doesn’t declare
Section titled “Find front matter keys the table doesn’t declare”Suppose someone later adds a front matter key by hand. infer_table with a table lists the keys its rows use that the table doesn’t declare. Add them with add_column, or remove them from the rows.
You edit datanotes/main/public/film/Metropolis.md by hand, outside the engine. The note now reads:
---table: main.public.filmid: 01a13107-abdf-7118-af41-764cb14d66a6year: 1927minutes: 153status: to watchcast: - "[[Brigitte Helm]]"director: Fritz Langtitle: Metropolis---infer_table
{ "table": "film" }Answer:
{ "files": 6, "paths": [ "datanotes/main/public/film/Battleship Potemkin.md", "datanotes/main/public/film/Charade.md", "datanotes/main/public/film/His Girl Friday.md", "datanotes/main/public/film/Metropolis.md", "datanotes/main/public/film/Nosferatu.md", "datanotes/main/public/film/The Gold Rush.md" ], "skipped": [], "columns": [ { "name": "cast", "type": "list", "description": null, "stats": { "present": 1, "filled": 1, "distinct": 1, "types": { "list": 1 }, "examples": [["[[Brigitte Helm]]"]] }, "items": "link" } ], "hints": [ "add these with add_column, each with a description of what it holds (null here; dense: false unless every row should carry the front matter key), or drop those front matter keys from the rows" ]}Find rows whose front matter can’t be read
Section titled “Find rows whose front matter can’t be read”A hand edit can break a note’s YAML, such as a title with a colon in it. The engine can’t read the row any more, so select_rows and every other read leave it out. The engine reports it in three places instead:
- Change feed: an event of kind
unreadable check_table: a violation with codeunreadabledescribe_database: the table’sunreadable_count
Here you give Sherlock Jr. a title with a colon by hand.
list_changes
{ "tables": ["film"] }You edit datanotes/main/public/film/Sherlock Jr..md by hand, outside the engine. The note now reads:
---table: main.public.filmid: 01a13107-9676-75a6-85a8-7ecb30cb8170title: Sherlock Jr.: the projectionistyear: 1924status: watchedtags: - seen-2026rating: 4---The change feed reports the broken note:
list_changes
{ "since": 37, "tables": ["film"], "kinds": ["unreadable"], "wait_ms": 10000 }Answer:
[ { "kind": "unreadable", "path": "datanotes/main/public/film/Sherlock Jr..md", "violations": [ { "path": "datanotes/main/public/film/Sherlock Jr..md", "violations": [ { "code": "unreadable", "message": "the front matter cannot be read, so the row is left out of every read: Nested mappings are not allowed in compact mappings at line 3, column 8:" } ] } ] }]check_table lists it with the rows that break the schema:
check_table
{ "table": "film" }Answer:
[ { "path": "datanotes/main/public/film/Sherlock Jr..md", "violations": [ { "code": "unreadable", "message": "the front matter cannot be read, so the row is left out of every read: Nested mappings are not allowed in compact mappings at line 3, column 8:" } ] }]describe_database counts it apart from row_count:
describe_database (no arguments)
Answer:
{ "name": "film", "qualified": "main.public.film", "row_count": 2, "description": "Classic films I watched or want to watch", "unreadable_count": 1}Quote the title to fix the note. The row is back:
You edit datanotes/main/public/film/Sherlock Jr..md by hand, outside the engine. The note now reads:
---table: main.public.filmid: 01a13107-9676-75a6-85a8-7ecb30cb8170title: "Sherlock Jr.: the projectionist"year: 1924status: watchedtags: - seen-2026rating: 4---select_rows
{ "table": "film", "columns": ["title"], "where": { "title": { "contains": "Sherlock" } }}Answer:
{ "table": "main.public.film", "total": 1, "rows": [ { "path": "datanotes/main/public/film/Sherlock Jr..md", "title": "Sherlock Jr.: the projectionist" } ]}