# History operations

> Operations on the journal, a table's versions, the change feed and webhooks: undo, time travel, restore and vacuum.

Operations on the journal, a table’s versions, the change feed and webhooks: undo, time travel, restore and vacuum. The [overview](https://datanotes.dev/reference/operations/) lists every operation by kind. The MCP server, `datanotes mcp`, offers each operation as an MCP tool of the same name, and `engineTools()` of `datanotes/mcp` gives the same definitions to [your own MCP server](https://datanotes.dev/develop/build-on-datanotes/#build-your-own-mcp-server). `DatanotesClient.call(name, args)` runs one from code. Over HTTP each one is `POST /datanotes/operations/<name>/`, with its arguments as the JSON body: the [HTTP API reference](https://datanotes.dev/api/) lists them.

Every write is validated. Writes to rows and to the schema are journaled and undone with `undo_migration`; `configure_database`, the webhook operations, `vacuum_table` and `sync_semantic_index` are not journaled.

## `list_migrations`

**List Migrations** · read-only

The operation journal, newest first: `total` (the matching operations) and `migrations`, each with `migration_id` (for undo_migration), `op`, `summary`, `table` (when it was about one), `executed_at`, `notes_changed`, `files_changed`, `undone`, `undoes`, `files_failed`, `by` (the agent), and `members` (a batch’s operations) or `batch` (the batch of an operation). Filters: `op` (e.g. “update_rows”), `table` (substring of the table name, e.g. “personal.tasks.task”), `since` (ISO date). `limit` default 50. The journal is kept in the engine’s store, outside the vault (`store:journal/`); entries older than 90 days are dropped.

Examples: [Hand edits](https://datanotes.dev/guides/data-quality/hand-edits/), [Journal and undo](https://datanotes.dev/guides/history/journal-and-undo/).

| Parameter | Type       | Required | Default | Description |
| --------- | ---------- | -------- | ------- | ----------- |
| `limit`   | integer \\ | null     |         | `null`      |
| `op`      | string \\  | null     |         | `null`      |
| `table`   | string \\  | null     |         | `null`      |
| `since`   | string \\  | null     |         | `null`      |

## `undo_migration`

**Undo Migration** · destructive write

Revert a journaled table operation by its `migration_id`. Refused (nothing written) when any touched note changed since; the conflicts are listed. Also refused when the revert would leave rows breaking a CHECK that follows links or a `ref_where` / `ref_by` (rows it writes, or rows that link to them), listed like the refusals of other writes with the way out (fix the row, or drop the rule). The rules are those of the schema the revert puts back: undoing a rename_value / rename_column puts the old names back with the rows, and a rule the revert puts back must hold on every row. A `batch` is checked as a whole first (nothing written when refused), then reverted operation by operation, from the last; when one finds a note changed since, those after it stay reverted and a retry skips them. An undo is journaled like any operation: undoing it (the `migration_id` of its answer; `undoes` names the operation undone) is a redo, whatever the operation (a batch included). The revert is a new version (`UNDO`) in the transaction log; undoing a table’s rename or move takes its whole log back to the old name, the versions made under the new one included.

The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).

Examples: [Columns and types](https://datanotes.dev/guides/database-objects/columns/), [Views](https://datanotes.dev/guides/database-objects/views/), [Journal and undo](https://datanotes.dev/guides/history/journal-and-undo/), [Restore a table or the database](https://datanotes.dev/guides/history/restore/).

| Parameter      | Type   | Required | Default | Description |
| -------------- | ------ | -------- | ------- | ----------- |
| `migration_id` | string | yes      |         |             |

## `describe_history`

**Describe History** · read-only

DESCRIBE HISTORY: the table’s versions, newest first — `version`, `timestamp`, `operation` (CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, …; `EXTERNAL_EDIT` = changed outside the engine: an editor, git, sync), `operation_metrics` and `migration_id` (undo). An EXTERNAL_EDIT’s `operation_metrics.violations` counts the rules this table’s rows break because of the hand edits it holds (each once): the edited rows whose front matter changed and the rows of the table linking to them, a removed or renamed row included (rows of other tables count in their table; a body-only edit is not checked); every rule its rows break when its definition was edited in metastore.yaml. Absent when nothing was checked, or when the edits were logged right before an engine write (the change feed has them); check_table lists the rows invalid now. Works for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows` (time travel), `table_changes` or `restore_table`. Without `table`: the history of the whole database — the commits of every table with a log (dropped ones included), newest first, each with its `table` (the result’s `tables` lists the logged tables). An operation that changed several tables (a cascade, inverse links, restore_database) commits to each with the same `migration_id`, and the operations of a `batch` carry its id as `batch`: their commits list in `tables` every table that operation (or batch) committed to. Pick a moment for `restore_database` from it (`timestamp`, or `before_migration` with a `migration_id` or a `batch`). `limit` (default 50) counts commits; `since` (ISO date or date-time): only commits at or after it.

Examples: [Table history and time travel](https://datanotes.dev/guides/history/table-history/), [Restore a table or the database](https://datanotes.dev/guides/history/restore/).

| Parameter | Type       | Required | Default | Description |
| --------- | ---------- | -------- | ------- | ----------- |
| `table`   | string \\  | null     |         | `null`      |
| `limit`   | integer \\ | null     |         | `null`      |
| `since`   | string \\  | null     |         | `null`      |

## `table_changes`

**Table Changes** · read-only

Change data feed between two versions (both included, end defaults to latest): one entry per row change with its values plus `_change_type` (insert, delete, update_preimage, update_postimage, metadata), `_commit_version`, `_commit_timestamp`, `_operation`, `path`. Use it to answer “what changed since …” without diffing full tables.

Examples: [Table history and time travel](https://datanotes.dev/guides/history/table-history/).

| Parameter       | Type       | Required | Default | Description |
| --------------- | ---------- | -------- | ------- | ----------- |
| `table`         | string     | yes      |         |             |
| `start_version` | integer    | yes      |         |             |
| `end_version`   | integer \\ | null     |         | `null`      |

## `restore_table`

**Restore Table** · destructive write

RESTORE TABLE to a `version` or `timestamp`: rows and definition return to that state — rows added since are deleted, changed rows get their old content, removed rows come back (a dropped table is re-created). History is not rewritten: the restore is a new version, and `undo_migration` reverts it. Refused (rows listed, with the way out) when the rows it writes, or rows that link to them, would break a CHECK that follows links or a `ref_where` / `ref_by` (as the restored definition has them: a rule added since does not hold the old rows). Run with `dry_run` first and show the user what changes.

The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).

Examples: [Restore a table or the database](https://datanotes.dev/guides/history/restore/).

| Parameter   | Type       | Required | Default | Description |
| ----------- | ---------- | -------- | ------- | ----------- |
| `table`     | string     | yes      |         |             |
| `version`   | integer \\ | null     |         | `null`      |
| `timestamp` | string \\  | null     |         | `null`      |
| `dry_run`   | boolean    |          | `false` |             |

## `restore_database`

**Restore Database** · destructive write

RESTORE DATABASE to a `timestamp` (each table as of its last version committed at or before it) or to `before_migration` (a `migration_id` or a `batch`: every table as it was just before that operation ran — to rewind an agent’s run). Every table that keeps a transaction log goes back, rows and definition: rows added since are deleted, changed rows get their old content, removed rows come back; a table created since is dropped with its rows moved to its archive folder (as drop_table with mode “archive”: nothing is hard-deleted), a table dropped since comes back. Tables already as they were are left alone (`unchanged`, no new version). One operation: each restored table gets a RESTORE version, all with one `migration_id`, and `undo_migration` on it reverts them all. Refused as a whole, nothing written (rows listed, with the way out), when a row it leaves would break a CHECK that follows links or a `ref_where` / `ref_by`, across tables, as the restored definitions have them. Tables without a log (history off, or vacuumed before that time) cannot be rewound: they stay as they are, listed in `skipped` with the reason (tell the user). Views, bases and the database settings are not rewound (the logs keep tables), nor a rename or move of a table (undo_migration on it does that). Returns `restored_to`, `tables` (per table: `action` restore / drop / recreate, `version`, `creates`, `texts`, `deletes`, `archived`), `unchanged` and `skipped`. Run with `dry_run` first and show the user what changes; describe_history without `table` shows the moments.

The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).

Examples: [Restore a table or the database](https://datanotes.dev/guides/history/restore/).

| Parameter          | Type      | Required | Default | Description |
| ------------------ | --------- | -------- | ------- | ----------- |
| `timestamp`        | string \\ | null     |         | `null`      |
| `before_migration` | string \\ | null     |         | `null`      |
| `dry_run`          | boolean   |          | `false` |             |

## `vacuum_table`

**Vacuum Table** · destructive write

Delete transaction-log history older than `retain_from_version` (default: keep only the latest version); time travel before it becomes impossible. Rows are not touched. `dry_run` defaults to true: it lists the log files that would go. Only on the user’s explicit request.

Examples: [Vacuum old history](https://datanotes.dev/guides/history/vacuum/).

| Parameter             | Type       | Required | Default | Description |
| --------------------- | ---------- | -------- | ------- | ----------- |
| `table`               | string     | yes      |         |             |
| `retain_from_version` | integer \\ | null     |         | `null`      |
| `dry_run`             | boolean    |          | `true`  |             |

## `list_changes`

**List Changes** · read-only

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`.

Examples: [Hand edits](https://datanotes.dev/guides/data-quality/hand-edits/), [Change data feed](https://datanotes.dev/guides/history/change-data-feed/).

| Parameter       | Type          | Required      | Default       | Description |
| --------------- | ------------- | ------------- | ------------- | ----------- |
| `since`         | integer       |               | `0`           |             |
| `tables`        | string\[] \\  | null          |               | `null`      |
| `kinds`         | `"insert"` \\ | `"update"` \\ | `"delete"` \\ | `"move"` \\ |
| `columns`       | string\[] \\  | null          |               | `null`      |
| `where`         | object \\     | null          |               | `null`      |
| `exclude_actor` | string \\     | null          |               | `null`      |
| `limit`         | integer       |               | `500`         |             |
| `wait_ms`       | integer       |               | `0`           |             |

## `list_webhooks`

**List Webhooks** · read-only

Webhooks of the change feed: id, url, filter (tables, ops, columns, exclude_actor), `cursor` (last change delivered), `status` (ok, failing, disabled), `last_delivery`, `last_error`. Secrets are not shown.

Examples: [Change data feed](https://datanotes.dev/guides/history/change-data-feed/).

## `create_webhook`

**Create Webhook** · destructive write

Push row changes to a URL (a service that would rather be called than poll `list_changes`): batches of up to 100 events POSTed as `{webhook, events, last}`, signed with `X-Datanotes-Signature: sha256=<HMAC of the body>`, retried with backoff until a 2xx. Events are those of `list_changes` (hand edits with their `violations`; the hand edits the service has seen are recorded before it stops). Starts from now unless `since`. Returns the webhook with its `secret` — shown only here: give it to the receiver. Only on the user’s explicit request: a webhook sends the database’s changes (and, with `since`, its past ones) to that address.

Examples: [Change data feed](https://datanotes.dev/guides/history/change-data-feed/).

| Parameter       | Type          | Required      | Default       | Description |
| --------------- | ------------- | ------------- | ------------- | ----------- |
| `url`           | string        | yes           |               |             |
| `tables`        | string\[] \\  | null          |               | `null`      |
| `kinds`         | `"insert"` \\ | `"update"` \\ | `"delete"` \\ | `"move"` \\ |
| `columns`       | string\[] \\  | null          |               | `null`      |
| `exclude_actor` | string \\     | null          |               | `null`      |
| `where`         | object \\     | null          |               | `null`      |
| `since`         | integer \\    | null          |               | `null`      |

## `delete_webhook`

**Delete Webhook** · destructive write

Stop and remove a webhook (by `id` from list_webhooks).

Examples: [Change data feed](https://datanotes.dev/guides/history/change-data-feed/).

| Parameter | Type   | Required | Default | Description |
| --------- | ------ | -------- | ------- | ----------- |
| `id`      | string | yes      |         |             |
