# Revisions and concurrency > Immutable entry snapshots, derived statuses, optimistic concurrency, and restore semantics. Every explicit save of an entry creates a revision: an immutable, complete snapshot of the entry's data at that moment. Revisions are never diffs and never mutated — updates append a new revision and move the entry's draft pointer. This is what makes agent output reviewable: every change is attributable, comparable, and restorable. ## The revision record ```json { "id": "rev_01...", "entryId": "ent_01...", "revisionNumber": 4, "collectionVersionId": "cver_01...", "data": { "title": "Launch day", "body": "..." }, "createdByType": "agent", "createdById": "key_01...", "changeSummary": "Tightened the intro paragraph", "createdAt": "2026-07-23T09:41:00.000Z" } ``` - `revisionNumber` increases from 1 per entry and is unique within the entry. - `data` is the full field document, validated against the deployed schema at write time. - `collectionVersionId` binds the revision to the [schema version](/concepts/collections) it was written under. Historical revisions stay bound to their original schema version even after later schema pushes; only current heads are re-validated when schemas change or when a [change set](/concepts/change-sets) publishes. - `changeSummary` is an optional caller-supplied note (max 500 characters) passed on create and update requests. ### Attribution `createdByType` records who produced the snapshot: | Actor type | Meaning | |---|---| | `user` | A dashboard session | | `api_key` | A scoped API key (SDK, CLI, or direct HTTP) | | `agent` | An agent acting through the [MCP server](/mcp/tools) or an agent-attributed credential | | `system` | Myna itself (migrations, background jobs) | `createdById` holds the concrete user or key id when applicable. ## Entry pointers and derived status An entry carries three revision pointers: `draftRevisionId` (latest saved snapshot), `publishedRevisionId` (what the public content API serves), and `lastPublishedRevisionId` (the most recently published snapshot, retained across unpublish). Status is never stored — it is derived from the pointers: | Status | Condition | |---|---| | `deleted` | `deletedAt` is set | | `draft` | No published revision and never published | | `unpublished` | No published revision, but a last-published revision exists | | `published` | Draft and published pointers are identical | | `changed` | Published, with newer draft edits on top | Publishing a change set points `publishedRevisionId` (and `lastPublishedRevisionId` and `draftRevisionId`) at the validated target revision. Later edits only advance `draftRevisionId`, moving the entry to `changed`. ## Optimistic concurrency Concurrent writers to the *same change set* are detected, not merged. `PATCH /v1/projects/:project/entries/:entry` accepts `expectedRevisionId` — the revision the client based its edit on. If it no longer matches the revision the write would actually build on, the write fails with `STALE_REVISION` (HTTP 409): ```json { "type": "https://myna.sh/errors/stale-revision", "title": "Stale revision", "status": 409, "code": "STALE_REVISION", "detail": "The entry was modified by another writer.", "expectedRevisionId": "rev_01A...", "currentRevisionId": "rev_01B..." } ``` Re-read the entry, rebase the edit, and retry. What the edit builds on depends on the change set. With a `changeSetId`, it is that change set's own proposal for the entry — or what is **published**, if that change set has not touched the entry yet. Without one, it is the entry's `draftRevisionId`. An entry may sit in [several open change sets](/concepts/change-sets), so "the current draft" is not a single revision, and an edit inside one change set never silently builds on another's unpublished work. Omitting `expectedRevisionId` performs a last-write-wins save. `STALE_REVISION` also appears as an item error code during [publish validation](/concepts/change-sets) when an entry was published after a change was staged (a stale base, settled by [rebasing](/concepts/change-sets)). See [error codes](/management-api/errors). ## Reading history ```bash $ myna entries revisions posts/launch # GET /v1/projects/:project/entries/:entry/revisions — newest first # GET /v1/projects/:project/entries/:entry/revisions/:revision ``` Both require `content:read`. Revision retention is plan-dependent: 30 days on Free, indefinite on Pro — see [plans](/platform/plans). ## Restore creates a new revision Restoring never rewrites history. `POST /v1/projects/:project/entries/:entry/revisions/:revision/restore` (scope `content:restore`) copies the historical revision's data into a **new** revision with the next revision number and the change summary `Restored from revision N`. The new revision: - becomes the entry's draft (`draftRevisionId`), clearing `deletedAt` if the entry was soft-deleted; - is bound to the collection's **current** schema version, not the historical one; - is staged as a `create` or `update` item on a change set, so nothing reaches the live site until that change set publishes. `POST /v1/projects/:project/entries/:entry/restore` is a shorthand that restores a soft-deleted entry from its last-published revision (falling back to its latest draft).