Content Lifecycle

On this page

EmDash keeps the published version of an entry separate from unpublished changes. The admin panel, REST API, command-line interface (CLI), and Model Context Protocol (MCP) tools use the same lifecycle rules.

An entry can be draft, scheduled, or published. A published entry can also have a draft and a future schedule: visitors continue to receive the live revision until the scheduled draft is published. Trash is separate from status. A trashed entry keeps its lifecycle metadata but is excluded from ordinary content reads.

State transitions

The following table is the canonical contract for content state, revision pointers, and publication timestamps. “No change” means the operation preserves the stored value.

ActionStarting stateResultRevision effectpublishedAtscheduledAtRepeated call
Save changesAny active entryStatus does not changeOn a revision-enabled collection, replaces the draft revision while the live revision remains publicNo changeNo changeA supplied _rev refuses a stale save; omitting it makes the REST write unconditional
PublishDraft, scheduled, or publishedPublishedPromotes the draft revision to live and clears the draft pointerSet on first publish; preserved on later publishes unless an authorized caller overrides itClearedPreserves the live content and publication time, but returns a new _rev
Publish when dueScheduled, or published with a scheduled draftPublishedSame as publishUses the scheduled time on first publish; preserves the existing value when publishing a new draft over live contentClearedA later scheduler pass skips an entry whose schedule was already cleared
UnpublishAny active entryDraftClears the live pointer; preserves the existing draft or creates one from the live revisionPreservedClearedAn entry that is already a plain draft does not change
ScheduleDraft, scheduled, or publishedA draft becomes scheduled; a published entry stays publishedNo changeNo changeSet to the requested future timeReplaces the existing schedule and returns a new _rev
UnscheduleScheduled, or published with a scheduleA scheduled entry becomes draft; a published entry stays publishedNo changeNo changeClearedAn entry without a schedule does not change
Discard draftAny active entryStatus does not changeClears the draft pointer; the live revision remains unchangedNo changeNo changeAn entry without a draft does not change
Move to TrashAny active entryTrashed and absent from ordinary readsPreservedPreservedPreservedA request for an entry already in Trash returns not found
Restore from TrashTrashedDraftClears the live pointer; preserves any draft pointerPreservedClearedRequires an entry that is still in Trash
Delete permanentlyTrashedRemovedRemoves the entry and its revisionsRemovedRemovedCannot be repeated or undone
Restore a revisionAny active entryStatus does not changeOn a revision-enabled collection, replaces the draft with a copy of the selected revision; the live revision remains unchangedNo changeNo changeCreates a new revision and returns a new _rev

On a collection without revision support, saves and publishes use the content row instead of live and draft pointers. Restoring a revision writes the selected field values directly to that row.

Restoring a revision on a revision-enabled collection does not publish it. Publish the entry after reviewing the restored draft.

Permissions and write protection

Permissions depend on ownership. An Author can act on an entry they own; an Editor can perform the same action on any entry. Permanent deletion requires an Admin.

Setting publishedAt during publish requires content:publish_any, even when the caller owns the entry.

The REST API accepts _rev as an optional optimistic-concurrency precondition where shown. MCP tools require it for the same operations so an agent must read the entry before changing it. A stale token returns CONFLICT. Admin, REST, and MCP writes protected by an entry lock return ENTRY_LOCKED unless an authorized request uses the supported lock override.

ActionPermissionREST _revMCP _revEntry lockHooks
Save changescontent:edit_own or content:edit_anyOptionalRequiredEnforcedcontent:beforeSave, content:afterSave
Publishcontent:publish_own or content:publish_anyOptionalRequiredEnforcedcontent:beforePublish, content:afterPublish
Unpublishcontent:publish_own or content:publish_anyOptionalRequiredEnforcedcontent:beforeUnpublish, content:afterUnpublish
Schedulecontent:publish_own or content:publish_anyOptionalRequiredEnforcedcontent:beforeSchedule, content:afterSchedule
Unschedulecontent:publish_own or content:publish_anyNot acceptedNot acceptedEnforcedcontent:afterUnschedule
Discard draftcontent:edit_own or content:edit_anyOptionalRequiredEnforcedNone
Move to Trashcontent:delete_own or content:delete_anyNot acceptedNot acceptedEnforcedcontent:beforeDelete, content:afterDelete
Restore from Trashcontent:edit_own or content:edit_anyNot acceptedNot acceptedNot enforcedcontent:afterRestore
Delete permanentlycontent:delete_permanentNot acceptedNot acceptedNot enforcedcontent:afterDelete
Restore a revisioncontent:edit_own or content:edit_anyNot acceptedNot acceptedEnforcedNone

The content:afterDelete event sets permanent to false when an entry moves to Trash and true after permanent deletion. Successful after-hooks run after the state change and may run after the response has been sent. A plugin can reject save, publish, unpublish, schedule, or move-to-Trash from the corresponding before-hook.

When scheduled publication reaches its due time, it uses the publish hooks. If a content:beforePublish hook rejects that scheduled attempt, EmDash clears the schedule and runs content:afterUnschedule.

Conflicts and retries

Use the _rev returned by each read or write for the next protected operation. EmDash refuses an outdated token instead of replacing a concurrent change. Read the entry again, review the newer state, and then decide whether to retry.

An error response does not always prove that no state changed. If a connection ends before the client receives a response, read the entry before retrying a lifecycle operation. This also prevents a retry from replacing work completed by another editor.

Revision restore commits the restored content and its audit revision together. If either write fails, EmDash preserves the content and revision history from before the request.

See the REST API reference for HTTP request and response schemas, the MCP server reference for tool inputs, and the hook reference for event payloads.