Draft, review, and publish
Edit on a branch, open a merge request, get approval, merge, and publish.
This guide walks the editorial workflow for one entry: create a draft branch, edit it, open a merge request, approve it, merge, and publish. It assumes a pages collection (see Quickstart) and uses the server API; the same methods exist on the client.
Create an entry and a draft branch
A new root starts on main. Branch off it to draft changes safely:
const root = await cms.api.pages.createRoot({
body: { slug: 'about', properties: { title: 'About' } },
});
const draft = await cms.api.pages.createBranch({
body: { rootId: root.rootId, name: 'draft', sourceBranchId: root.branchId },
});Edit on the draft
Add and update blocks on the draft branch. Each change is a commit:
await cms.api.pages.createBlock({
body: {
rootId: root.rootId,
branchId: draft.branch.id,
parentBlockId: root.rootId,
type: 'hero',
properties: { headline: 'Welcome' },
},
});Open a merge request
Request to integrate the draft into main. The response reports any conflicts:
const mr = await cms.api.pages.createMergeRequest({
body: {
sourceBranchId: draft.branchId,
targetBranchId: root.branchId,
title: 'Add hero',
},
});If mr.hasConflicts is true, resolve the conflicts before merging: each conflict is settled as source, target, or a manual version through applyConflictResolutions. Edits that touched different properties of the same block merge automatically and never show up here — a conflict means both sides changed the same property, disagreed on children order or type, or one side deleted the block. See Merges for the model.
Approve and merge
Approvals are opt-in. By default no approval is required to merge, but once you open an approval request it is honored: a merge is blocked while any request on it is still pending, and only proceeds once every request is approved. So the moment a reviewer is asked to sign off, executeMerge will not bypass them. Request sign-off, have a reviewer approve, then execute the merge:
const { approvals } = await cms.api.pages.requestApproval({
body: {
mergeRequestId: mr.mergeRequest.id,
requestedReviewers: ['reviewer-1'],
},
});
await cms.api.pages.submitApproval({
body: { approvalId: approvals[0].id },
});
await cms.api.pages.executeMerge({
body: { mergeRequestId: mr.mergeRequest.id },
});The actor on requestApproval and submitApproval comes from the request's auth context, so you no longer pass requestedBy or reviewedBy in the body.
Make review mandatory
The pending-request gate above is per merge request: it only bites when someone actually opens an approval request. To require review across the board, opt into branch protection. Three flags, all false by default, turn the workflow into a review-gated one:
const cms = createCMS({
// ...
branchProtection: {
// Block direct edits to a published branch. Content must move through a
// branch, a merge request, and a re-publish rather than being edited live.
protectPublishedBranches: true,
// Require an approval before a merge can execute, even when no request was
// opened. Without an approved request, executeMerge throws.
requireApprovalToMerge: true,
// Require an approval before publishBranch can make a branch live.
requireApprovalBeforePublish: true,
},
});You can also set branchProtection per collection to override the global config field by field. With requireApprovalToMerge on, a merge with no approval request throws MERGE_APPROVAL_REQUIRED; with a request that is not fully approved it throws APPROVALS_NOT_FULLY_APPROVED, which is the same error the default pending-request gate raises.
Publish
Publish the target branch to make it live. getPublishedContent then returns it:
await cms.api.pages.publishBranch({
body: { rootId: root.rootId, branchId: root.branchId },
});Background on the model is in Branches and Merges. Exact signatures are in the Server API.