Dyrecteddyrected
Guides

Editorial Approval Workflow

Guide on setting up a publishing chain with capability gates and comment requirements.

Give your content a real review chain: editors draft and submit, publishers approve and publish or send work back with comments — and nothing goes public until someone with permission says so. This guide sets that up on a posts collection.


1. Define the Collection

Add publishingWorkflow() to the collection config. This one call replaces a hand-rolled status field with the full draft → submit → publish state machine:

import { defineCollection, publishingWorkflow } from "@dyrected/core"

export const Posts = defineCollection({
  slug: "posts",
  workflow: publishingWorkflow(),
  fields: [
    { name: "title", label: "Title", type: "text", required: true },
    { name: "body", label: "Body", type: "richText" },
  ]
})

2. Assign Capabilities to Roles

The publishing workflow maps roles (e.g. 'editor', 'publisher') to capabilities:

  • Editor: Can write drafts and perform 'submit' (Submit for review).
  • Publisher: Can write drafts, submit, 'publish' (Publish), 'reject' (Request changes), and 'unpublish' (Unpublish).

Make sure your auth users collection has a roles field containing either ['editor'] or ['publisher'].


3. Require Comments on Rejection

The 'reject' transition (Request changes) has requireComment: true. In the Admin UI, clicking Request changes opens a dialog asking for a revision comment (e.g., "Please fix spelling"). The comment is recorded in the history log:

const doc = await client.collection('posts').transition(postId, 'reject', {
  expectedRevision: currentRevision,
  comment: 'Needs better images.'
})

4. Move a post through the workflow

Every state change goes through the same transition() call — you pass the capability you want to apply. In the Admin UI these map to buttons, but here is the happy path in code so you can see the full loop.

An editor drafts a post and submits it for review:

await client.collection('posts').transition(postId, 'submit', {
  expectedRevision: currentRevision,
})

The post is now waiting for a publisher. A publisher who is happy with it publishes:

await client.collection('posts').transition(postId, 'publish', {
  expectedRevision: currentRevision,
})

At this point the public snapshot — the published copy of the post that anonymous visitors see — is live. If the publisher wants changes instead, they use 'reject' with a comment (shown above), which sends the post back to the editor. Later, 'unpublish' pulls a live post out of public view without deleting it.

The expectedRevision you pass is the revision you last read for the document. If someone else changed it in the meantime, the transition is rejected instead of silently clobbering their edit — so read the current revision right before you transition.

On this page