Skip to content

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.

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, where constraint names a CHECK; the event leaves out rules the row already broke
  • Log: a WARN line
  • History: when history is on, the EXTERNAL_EDIT version counts them in operation_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.genre
id: 01a13107-8fe6-746f-a5a7-fefb251465d8
name: Silent film
kind: cinema
parent: "[[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.watch
id: 01a13107-882d-7a93-a182-8b82b267bc02
film: His Girl Friday
occurred_at: 2026-10-03T21:30
where: cinema
day: 2026-10-03
label: 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)"
}
]
}

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.watch
id: 01a13107-ebaa-7f9d-9ee7-44d950f6098d
occurred_at: 2026-09-25T21:00
film: "[[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.film
id: 01a13107-abdf-7118-af41-764cb14d66a6
year: 1927
minutes: 153
status: to watch
cast:
- "[[Brigitte Helm]]"
director: Fritz Lang
title: 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 code unreadable
  • describe_database: the table’s unreadable_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.film
id: 01a13107-9676-75a6-85a8-7ecb30cb8170
title: Sherlock Jr.: the projectionist
year: 1924
status: watched
tags:
- seen-2026
rating: 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.film
id: 01a13107-9676-75a6-85a8-7ecb30cb8170
title: "Sherlock Jr.: the projectionist"
year: 1924
status: watched
tags:
- seen-2026
rating: 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"
}
]
}