# Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes

`POST /datanotes/operations/list_changes/` (operation `list_changes`)

Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes
and from hand edits (`source: external`). Each has `table`, `kind` (insert, update, delete, move,
metastore, unreadable), `op` (the operation that made it), `path`, `row_id`, `changes` ({column: {before, after}}), `actor`, `migration_id`. Hand edits are compared in batches (0.7 s after the last vault change, at most 5 s after the first), in the order they happened, renames included. A hand edit that creates, changes, renames or removes a row is checked against the tables' rules (as check_table does) with the rows that link to it: the last event of each row in the batch has `violations` ([{path, violations: [{code, column | constraint (the CHECK's name), index, message}]}]), what the edit breaks (a rule a row already broke is not listed); empty when it breaks nothing, absent when nothing was checked (engine events, a check that failed). A hand edit of metastore.yaml that changes tables' entries checks every row of those tables: on its `metastore` event. At most 100 per event; `violations_total` then counts them all. A note renamed or moved by hand (a row or a plain note) has the links to its old path rewritten across the vault once the vault is quiet (4 s without a change; not while git pulls, rebases or merges; setting repairLinksOnHandMove, on by default), and its move event does not report them as broken_link: that repair's events (`op: repair_links`) follow the batch and name the moves in `cause` ([{path, old_path, seq}], `seq` of the row's move event). A hand edit that leaves a row's front matter unreadable (broken YAML) is a `kind: unreadable` event with one violation of code `unreadable`: every read leaves the row out until the note is fixed, and the fix is an update of the row's last readable state (an insert for a new note). Agents' tool calls (reported by MCP servers) are `kind: tool_call` events (`call`: tool, args, ok, duration_ms, value, served_by), returned only when `kinds` includes tool_call. Pass the returned
`last` as `since` next time (`head` is the newest number); `wait_ms` (max 60000) waits for a new change (long poll); `limit` max 5000. `gap: true` means
older changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.

## Request body

`application/json`

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `since` | `integer` | no | `0` |  |
| `tables` | `string[] \| null` | no | `null` |  |
| `kinds` | `("insert" \| "update" \| "delete" \| "move" \| "metastore" \| "tool_call" \| "unreadable")[] \| null` | no | `null` |  |
| `columns` | `string[] \| null` | no | `null` |  |
| `where` | `object \| null` | no | `null` |  |
| `exclude_actor` | `string \| null` | no | `null` |  |
| `limit` | `integer` | no | `500` |  |
| `wait_ms` | `integer` | no | `0` |  |

## Responses

| Status | Description | Body |
| --- | --- | --- |
| 200 | Done. | `object` (application/json) |
| 400 | Bad input: the body is not a JSON object, or an argument is unknown, missing or of the wrong type. | `Error` (application/json) |
| 401 | Missing or wrong bearer token. | `Error` (application/json) |
| 500 | Unexpected error. | `Error` (application/json) |

The schemas named here are in [openapi.json](https://datanotes.dev/openapi.json) (`components.schemas`).
