Skip to content

Record history

Every field change on a record is logged: what changed, from what to what, who it was for, and what made the change.

The source label is recorded on every change and returned by GET /workspaces/{ws}/databases/{db}/records/{rec}/versions/changes (and by the MCP get_history tool), and it now renders in the record’s activity panel too, as a small badge next to the actor’s name.

human renders no badge at all. It’s the default a reader already assumes, and marking every ordinary edit would bury the exceptions the badge exists to surface — agent, automation and mcp each get their own labelled badge.

A row with no source recorded gets its own explicit badge, never a silent fold into human. Anything written before this tracking shipped, or from an insert site an early rollout missed, has source: null — and shows as “Source not recorded” rather than reading as an ordinary human edit. Treating “unknown” and “human” as the same badge is the single easiest way to get this wrong, so the two stay visually distinct.

History answers two questions that are easy to confuse, and keeps them as separate columns.

Who — always a person. A member’s name is on every row, including rows an agent or an automation wrote. That is deliberate: an agent acts as the member who ran it, bounded by that member’s permissions, so the person who authorised the change is the person accountable for it. Agents never appear as actors and never accumulate a permission surface of their own.

What made it — one of four:

Badge Means
human Somebody typed it, in the app.
agent An agent run produced it — including Tyron.
automation An automation rule or a button action.
mcp A write that arrived over the API with a personal access token.

So “Ada changed Status to Done · automation” is not a contradiction. It means a rule Ada owns fired, and the change is hers in the sense that matters for permissions and accountability.

An agent write names the agent, not just its owner

Section titled “An agent write names the agent, not just its owner”

An agent-badged row carries a third fact alongside who and what: which configured Agent (from your workspace’s Agents database) actually made the write — its name at the moment it wrote, snapshotted so a later rename or deletion never rewrites what a historical row appears to say.

The human owner is resolved from the credential, live, every request — never from a stale token field and never something the agent supplies about itself. An agent-scoped token whose Agent record has been deleted, or whose owner is no longer an active member, is refused outright — there is no fallback identity an agent’s writes fall back to instead. A person is never blocked by this; only an agent-scoped credential is.

Query one agent’s activity in bulk — GET /workspaces/:ws/agents/:agent/activity (from/to date range) or the list_agent_activity MCP tool — for “show me everything this agent did last week,” rather than reading one record’s history at a time.

It is derived from how the request authenticated, never claimed by the caller:

  • A browser session → human.
  • The agent runtime → agent.
  • The automation executor → automation.
  • An API token → agent if the token was minted as an agent’s, mcp otherwise.

That last one is the interesting case. The label lives on the token row, not in a request header, precisely because a header is forgeable by any client — and provenance that its own subject can claim is not provenance.

An ordinary personal access token reads as mcp — and so does an AI connected through OAuth (for example Claude), which is not badged human even though a person signed in to connect it. If you write records from your own script with a PAT, those changes are badged mcp — not because they came from an MCP client, but because an unmarked token is treated as one. The badge tells you a change came in over the API with a token; it does not tell you which program held it.

human means a browser session and nothing else, so it is a reliable answer to “was a person at the keyboard” — which is the question the badge exists to answer.

An activity feed showing a plain human edit next to an MCP-badged record creation

Above: a record created by a script authenticating with a personal access token — badged mcp — next to an ordinary edit made in the app, which carries no badge at all.

Every change is stored exactly as it was written — a select’s option id, not its label. That faithfulness is the one thing a change log cannot give up: a stored label would quietly rewrite itself every time somebody renamed an option.

The translating happens when you read it, so history shows you the same thing the record shows you: field names, option labels, values rendered by type. Both old_value / new_value (the raw stored values) and old_display / new_display (rendered for a human) come back, so you can have either.

Deleted fields and deleted options still render by name. A field outliving the field is the whole point of a change log, and a row that renders as a bare uuid because its column was removed is exactly the moment you start doubting the history.

A record’s ⋯ menu → History opens a dialog with three tabs:

  • Changes — the field-by-field log above: who, what made it, and the before and after of each field. A field that has since changed again offers Revert this field, which writes the old value back as an ordinary edit (so the revert shows up in the log too).
  • Versions — whole-record snapshots. Each shows who made it and what made it — a person or an agent badge, readable at a glance without decoding an id.
  • Document — the record’s rich-text description, which has its own history (below).

Pick a version and Restore shows a diff preview first — exactly what would change from the record as it is now — before you confirm. The restore is itself a change: it appears in the log like any other, badged by whatever made it, and can be undone the same way. Over the API a version restores with POST …/versions/{version}/restore, and a read-only preview is GET …/versions/{version}.

A record’s rich-text description keeps its own versions, separate from its fields. Each time the description is saved after the first, the previous content is kept; the Document tab lists them (newest first, with the same person/agent badge), previews any one as a block-level diff against the current text, and restores it. Restoring a description version is reversible too — the text you restored over is saved first.

  • This is not the editing-conflict counter. A save that was based on a stale copy of the description is still refused with a 409, exactly as before; history is a separate record of past content.
  • The same retention rule as the field log (below) — one policy, not two.
  • Over MCP: get_history with kind: "document_versions" lists them, and restore_document_version restores one. There is no preview tool — an agent lists, restores, and can undo.

History is plan-gated and capture only runs while it’s on: Free keeps none (nothing is captured, so there is nothing to restore), Pro keeps 1 day, Business 7 days, Enterprise 30 days. The History dialog states the policy in general terms rather than showing your exact number, since the figure lives on the billing side that only admins can read. This is record history, not a workspace backup — it won’t bring back a deleted database; see trash and restore for that.

Everything above is scoped to one record. GET /workspaces/{ws}/audit-log (the MCP list_audit_log tool) reads the same underlying data — admin-only, across every user and every record in the workspace, filterable by actor, by one entity, and by date range (default: the last 30 days).

  • In the app: Settings → Audit log, visible to admins only. Filter by Actor, by one record, and by a From / To date, and use Load more to page back. A non-admin who opens the address sees only “Only workspace admins can view the audit log”, and the page makes no request for the data. It also reads from the same API and MCP tool above.
  • A removed member still appears by name on their historical rows, the same no-FK design this whole page is built on.
  • Known, stated gap: structural deletions aren’t captured. Deleting a database, a view, or a space doesn’t yet write a row here — only record-level create/update/delete/restore does. “Who deleted this record” is answerable today; “who deleted this database” isn’t yet, and that’s a named, separate piece of work, not an oversight quietly worked around.