Content Moderation State Machine
When to Use
When you are provisioning an editorial workflow with core
content_moderation+workflows, and need the exact config shape: what a state is, what a transition is, how permissions are derived, and how moderation gets switched on for a content type. Read this before authoring aworkflows.workflow.*object or enabling moderation on a bundle that may already hold content.
Foundation
content_moderation is a workflows plugin. The config object is workflows.workflow.{id} with type: content_moderation and a type_settings block:
states— each keyed by a machine name withlabel,weight, and the two moderation flags:published(does content in this state show to the public?) anddefault_revision(does entering this state make this revision the canonical one?).content_moderation.stateextends the baseworkflows.statewith exactly those two flags.transitions— each withlabel,from(a list of state ids),to(one state id), andweight. Transitions are the edges; permissions gate them.entity_types— a map ofentity_type_id: [bundle, …]; the bundles moderation is enabled on.default_moderation_state— the state a brand-new entity starts in (typicallydraft).
Transition permissions are generated, one per transition, as use {workflow_id} transition {transition_id}, and checked when a user tries to move content across that edge. This recipe/guide emits those IDs; a role recipe grants them (see Editorial Role & Permission Model).
Decision
Do not hardcode one canonical state set — two real, shipped variants differ:
| Variant | States | Transitions | Where it ships |
|---|---|---|---|
Core editorial |
draft (unpublished), published, archived (unpublished, default_revision) |
create_new_draft, publish, archive, archived_draft, archived_published |
core/recipes/editorial_workflow, Standard profile, demo_umami |
Drupal CMS basic_editorial |
draft, published, unpublished (no archived) |
create_new_draft, publish, unpublish |
drupal_cms_content_type_base |
Pick the leaner basic_editorial shape for most sites; add archived only when the site genuinely needs a distinct terminal archive state separate from "unpublished." Never assume archived is present when writing transitions or verifiers.
Pattern
A minimal three-state workflow, config-first, with entity_types left empty at authoring time:
# workflows.workflow.editorial.yml
id: editorial
label: Editorial
type: content_moderation
type_settings:
states:
draft:
label: Draft
weight: -5
published: false
default_revision: false
published:
label: Published
weight: 0
published: true
default_revision: true
archived:
label: Archived
weight: 5
published: false
default_revision: true
transitions:
create_new_draft: { label: 'Create New Draft', from: [draft, published], to: draft, weight: 0 }
publish: { label: 'Publish', from: [draft, published], to: published, weight: 1 }
archive: { label: 'Archive', from: [published], to: archived, weight: 2 }
default_moderation_state: draft
entity_types: {}
Enable moderation on a bundle through the add_moderation config action, not by hand-editing entity_types. The action is derived per entity type (addNodeTypes, addTaxonomyVocabularies, …); each call wires the handlers and adds the bundle to the workflow:
# In a recipe.yml
config:
actions:
workflows.workflow.editorial:
addNodeTypes: [article, page] # or '*' for all node bundles
Hand-editing type_settings.entity_types bypasses the handler wiring the action performs and is treated as a smell by the paired recipe verifier.
Common Mistakes
- Wrong: Assuming every workflow has a
draft → published → archivedshape → Right: Drupal CMS'sbasic_editorialhas noarchived; read the target workflow before writing transitions - Wrong: Adding bundles by editing
type_settings.entity_typesdirectly → Right: Use theadd_moderationconfig action (addNodeTypes) so moderation handlers are wired correctly - Wrong: Confusing
publishedanddefault_revision→ Right:publishedcontrols public visibility;default_revisioncontrols which revision is canonical (an unpublishedarchivedstate still setsdefault_revision: true) - Wrong: Granting transition permissions inside the workflow config → Right: The workflow emits transition IDs; a role recipe grants
use {workflow} transition {id}(separation of concerns) - Wrong: Enabling moderation on a bundle that already has content and calling it done → Right: Existing revisions have no moderation state until resaved — see Enabling Moderation on Existing Content
See Also
- Enabling Moderation on Existing Content — the backfill gotcha
- Editorial Role & Permission Model — who gets each
use {workflow} transition {id}permission - Reference: Content Moderation