Workspaces

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

Workspaces let an editorial team prepare a group of related content changes in isolation, look at the result in the full context of the site, and then make all of them live in a single operation. A museum that is opening a new exhibition can edit the exhibition page, its gallery entries, a new menu link, new taxonomy terms and a reusable block in one workspace, review them together, and publish them at a planned moment, while visitors keep seeing the old site until then. This page explains what a workspace is, how the two core modules divide the work, how to create, switch, preview, merge and publish workspaces, which entity types take part, what happens inside the database during publishing, which permissions apply, how developers react to workspace events and add support to their own entity types, and the limitations that surprise people in production.

Facts on this page were checked against the Drupal documentation, the change records and the core source on 2026-10-30. The rest of this sub-section is written against Drupal 11.4.x, Drush 13/14 and PHP 8.3 or later, and the examples use the fictitious Meridian Museum site introduced in the sub-section index. Workspaces work with the revision system described in Entity API and complement, rather than replace, the state-based review flow of Content moderation, workflows and revisions.

What a workspace is, and what it is not

A workspace is a named container of pending changes. The site always has one special workspace called Live, which is what anonymous visitors see. Every other workspace is a content entity of type workspace that records which entity revisions were created inside it. While a user has a workspace active, every page that user loads shows Live content with that workspace’s changes layered on top, including lists, menus and path aliases, so the result can be checked as a whole. Nothing in the workspace is visible to Live visitors until the workspace is published.

Three misunderstandings are common:

  • A workspace is not a copy of the site. The Drupal.org overview page for the module describes a workspace as a copy of the live site with its own configuration and content. The module itself is described in its workspaces.info.yml as providing "an API for staging and previewing content in a full site context". Only content entities are staged. Configuration (content types, fields, views, text formats, block placement) is never staged by workspaces. Moving configuration between environments is the job of configuration management and deployment pipelines.

  • A workspace is not an environment. It lives in the same database as Live. Staging servers, local copies and production are separate sites; see settings.php and environments.

  • A workspace is not a moderation state. Content moderation decides whether a single revision is draft, in review or published inside one site view. A workspace groups many revisions of many entities and controls when they become visible together. The two can be used side by side, but they answer different questions.

Older books such as Drupal 10 Development Cookbook (2023) introduce Workspaces as one module with its screens inside it. Since Drupal 11.1 the user interface lives in a separate module (see the next section), so enabling only workspaces on a new 11.4 site gives an API with no screens. Drupal 10 Masterclass (2023) does not cover Workspaces, so a team relying on that book for content staging has to start from the sources listed in the references.

Two modules since Drupal 11.1

Drupal 11.1.0 split the module. The change record "Workspaces UI module" (introduced in 11.1.0) describes a new workspaces_ui module that adds the routes and toolbar integration of Workspaces, with the goal of allowing alternative interfaces in core or contributed projects. The two modules, taken from their *.info.yml files, are:

Module Description Contents

workspaces ("Workspaces")

Provides an API for staging and previewing content in a full site context. Depends only on user.

The workspace entity type, the tracker that records which revisions belong to which workspace, the workspace manager and negotiators, entity query and entity repository decorators, cache context, the publisher and merger, events, validation constraints, cron cleanup and the permissions.

workspaces_ui ("Workspaces UI")

Provides a user interface for creating and managing workspaces. Depends on workspaces. Its configure key points to the workspace list.

The administration list at /admin/config/workflow/workspaces, the add, edit, activate, publish, merge and delete forms, the "Switch to Live" form, the workspace switcher block and toolbar entry, and the list and view builders.

Sites that already used Workspaces when they updated to 11.1 get workspaces_ui enabled by an update. New sites must enable both modules from the Extend page or with Drush:

# Run from the project root of the Meridian Museum site (DDEV).
ddev drush pm:install workspaces workspaces_ui -y
ddev drush config:get core.extension module.workspaces_ui

A follow-up issue, "Move the remaining UI bits to the workspaces_ui module", moved the remaining UI code, "a few form classes and entity handlers", to workspaces_ui; it was committed to the 11.x development branch in January 2026 and brings no user-visible change. The core workspaces.services.yml records the moved classes in a workspaces.moved_classes parameter (for example Drupal\workspaces\Form\WorkspaceForm now resolves to Drupal\workspaces_ui\Form\WorkspaceForm), so custom code that referred to the old class names keeps working through that mapping, but should be updated to the new names. Modules that need a workspace-aware experience without the stock screens, such as a decoupled editor, can depend on workspaces alone and build their own interface on the API.

The Meridian Museum scenario

The museum plans the Deep Sea exhibition, to open on a Saturday at 10:00. During the preceding fortnight the Content Editor, the Curator and the Reviewer prepare:

  • a new exhibition page and three event items

  • eight collection_object records marked on_display

  • two new gallery taxonomy terms

  • a reusable custom block with the opening-day banner

  • a menu link and a path alias such as /exhibitions/deep-sea

  • a changed Visit page with new opening hours

None of it may appear on the live site before opening day, and everything must appear together. A workspace called Deep Sea opening (machine name deep_sea_opening) holds all of it. Because the same people also correct typos on live pages during the fortnight, the team has to understand exactly which edits conflict; that is covered in the limitations section.

Setting up access

The workspaces module defines these permissions in workspaces.permissions.yml:

Permission Meaning

administer workspaces

Full control. Also required to publish a workspace and to merge a sub-workspace into its parent.

create workspace

Create a new workspace.

view own workspace, view any workspace

See and switch to workspaces. Viewing the list at /admin/config/workflow/workspaces needs view any workspace, administer workspaces or edit any workspace.

edit own workspace, edit any workspace

Edit the workspace entity itself (label, owner), not the content inside it.

delete own workspace, delete any workspace

Delete a workspace.

bypass entity access own workspace

"Bypass content entity access in own workspace": lets the workspace owner edit content in that workspace regardless of the normal entity access rules for individual entities.

Two points deserve emphasis. First, the permissions govern the workspace container; the user also needs the normal entity permissions (for example edit any exhibition content) to change content inside it, unless the bypass permission applies. Second, administer workspaces effectively decides what goes live and when, and it carries the "restrict access" implications of any permission that publishes content, so grant it only to the roles that own releases (for the museum, the Administrator and a senior Reviewer role). The role and permission screens are described in Users, roles and permissions.

# Grant the Curator the right to create and see workspaces, but not to publish them.
ddev drush role:perm:add curator 'create workspace,view any workspace,edit own workspace'
ddev drush role:perm:add reviewer 'view any workspace,bypass entity access own workspace'
ddev drush role:perm:add administrator 'administer workspaces'

Creating, switching and previewing

Creating a workspace

With workspaces_ui enabled, a Workspaces entry appears under Configuration, Workflow (/admin/config/workflow/workspaces), and the toolbar shows the active workspace. The Add workspace form asks for a Workspace name (label) and a Workspace ID, the machine name. The ID is limited to lowercase letters, digits and underscores (the field constraint is the regular expression /^[a-z0-9_]+$/, maximum 128 characters) and must be unique. Other fields of the entity are the owner (uid), a read-only Provider (the default provider; contributed modules can add providers) and a Parent workspace. The parent can be chosen only when the workspace is created.

Switching the active workspace

The Drupal.org usage page for the module describes the interaction. An indicator at the top of the screen shows the current workspace, and clicking it opens options for switching. Choosing another workspace asks for confirmation. From that moment until the user switches back to Live, content is created and edited inside the chosen workspace. The same Activate action exists in the list at /admin/config/workflow/workspaces/manage/{workspace}/activate, and the "Switch to Live" form at /admin/config/workflow/workspaces/switch-to-live returns to the live site (available to logged-in users with an active workspace).

Internally the workspace manager (service workspaces.manager) asks tagged negotiators which workspace applies to the request. Two ship with core:

Service Priority Behaviour

workspaces.negotiator.query_parameter

100

Applies when the request has string workspace and token query parameters. The token is checked with hash_equals() against a token computed by the negotiator, so a reader cannot activate a workspace by guessing its machine name. A persist parameter (default true) decides whether the choice is remembered in the session.

workspaces.negotiator.session

50

Reads the workspace stored in the user’s session by the switcher.

The query-parameter negotiator is what makes shareable preview links possible: a reviewer without an account who receives a tokenised URL can see the staged site for the duration of the visit. Treat such links like passwords, because anyone holding the URL sees unpublished content, and do not post them in public channels.

Because the active workspace changes what a page contains, Drupal adds the workspace cache context to the render cache metadata of affected content. Custom render arrays that show entity data should keep their cache metadata correct, as explained in Render API and cache metadata, and reverse proxies must not serve one workspace’s page to another visitor; see Performance and caching. Page caching for anonymous users is unaffected, because anonymous visitors are always on Live.

What the editor sees

Inside a non-default workspace the canonical route of an entity shows the workspace’s revision. For that reason core removes the latest-version link template of supported entity types: there is no separate "latest version" tab, because the workspace’s own revision is displayed by default. Forms warn when the entity is locked by another workspace (see the limitations). Entity queries, entity loads, Views listings and menu trees are all workspace-aware, because the module decorates the entity.repository and entity.query.sql services and the menu.tree_storage service.

Sub-workspaces and merging

Since Drupal 8.8 a workspace can have a parent. The change record "Added the ability to create sub-workspaces in order to enable parent/child (branching) workflows for content" explains that a sub-workspace automatically inherits all the content of its parent, and describes branching setups such as local environments feeding a shared development workspace, which in turn feeds a stage workspace.

The rules enforced by the code in the workspaces module are:

  • only a top-level workspace (one without a parent) can be published to Live; WorkspacePublisher::publish() throws a WorkspacePublishException with the message "Only top-level workspaces can be published." for any other

  • a sub-workspace can be merged only into its own parent; WorkspaceMerger throws an \InvalidArgumentException otherwise

  • merging moves the differing revisions from the source to the target workspace, clears them from the source, and updates the descendants of the target as well

  • the merge form lives at /admin/config/workflow/workspaces/manage/{source_workspace}/{target_workspace}/merge and requires administer workspaces

For the museum, a useful layout is a top-level deep_sea_opening workspace and one sub-workspace per team, for example deep_sea_curators and deep_sea_web. Each team works in its own branch and the editor in chief merges both into deep_sea_opening, reviews the combined result and publishes only the parent.

flowchart TD L[(Live workspace)] --> P[deep_sea_opening<br/>top-level workspace] P --> C[deep_sea_curators<br/>sub-workspace] P --> W[deep_sea_web<br/>sub-workspace] C -- merge into parent --> P W -- merge into parent --> P P -- publish --> L

Which entities are supported

Workspaces rely on the revision system: a change made inside a workspace is stored as a new, non-default revision tagged with that workspace, and publishing turns those revisions into the default ones. For that reason only entity types that are both revisionable and publishable can belong to a workspace.

The module encodes this in WorkspacesHooks and EntityTypeInfo (hook implementations in src/Hook, a good example of the class-based hooks covered in Hooks and OOP hooks). During entity_type_build every entity type that does not already declare a workspace handler is examined:

  • an entity type whose class implements EntityPublishedInterface and which is revisionable receives DefaultWorkspaceHandler, which supports every entity

  • the block_content entity type receives BlockContentWorkspaceHandler, which supports only reusable blocks, because non-reusable (inline) blocks are tracked as part of the entity they belong to, such as a Layout Builder layout

  • the file entity type and every internal entity type receive IgnoredWorkspaceHandler; they can be created and changed inside a workspace without being tracked, which is why an image uploaded inside a workspace is a normal live file

  • the workspace entity type itself is declared with the ignored handler

An entity type that has a handler of its own is respected as declared. The service workspaces.information (WorkspaceInformation) answers the questions, and is the API to use:

Method Meaning

isEntityTypeSupported($entity_type)

Uses the type’s workspace handler; if none exists yet (for example while the module is being installed) it falls back to "implements EntityPublishedInterface and is revisionable".

isEntitySupported($entity)

Checks the type, then asks the entity’s own handler (this is how only reusable blocks are accepted).

getSupportedEntityTypes()

Returns all supported entity types, keyed by ID.

isEntityTypeIgnored($entity_type), isEntityIgnored($entity)

True when the handler is IgnoredWorkspaceHandler or the entity’s handler refuses the entity.

isEntityDeletable($entity, $workspace)

True when the entity was created in that workspace (its initial revision is tracked by it).

In a standard Drupal 11 installation the supported types are, in practice, nodes, taxonomy terms, media items, custom menu links, path aliases and reusable custom blocks. Because the rule is generic, contributed modules and custom entity types join automatically when they are revisionable and publishable. Entity types that are not both, such as users, are not supported, and neither are configuration entities. The authoritative list for a given site comes from getSupportedEntityTypes():

ddev drush php:eval "echo implode(PHP_EOL, array_keys(\Drupal::service('workspaces.information')->getSupportedEntityTypes())) . PHP_EOL;"

For the museum, the custom collection_object entity (see Custom entity types) takes part in workspaces only if it is both revisionable and publishable. The entity type declaration below shows the two ingredients; the published key and EntityPublishedTrait supply the publishable side, and the revision table and keys supply the revisionable side:

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Entity;

use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionLogEntityTrait;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * A museum object; staged in workspaces because it is revisionable and publishable.
 */
#[ContentEntityType(
  id: 'collection_object',
  label: new TranslatableMarkup('Collection object'),
  base_table: 'collection_object',
  data_table: 'collection_object_field_data',
  revision_table: 'collection_object_revision',
  revision_data_table: 'collection_object_field_revision',
  entity_keys: [
    'id' => 'id',
    'revision' => 'revision_id',
    'uuid' => 'uuid',
    'label' => 'title',
    'published' => 'status',
  ],
  revision_metadata_keys: [
    'revision_user' => 'revision_user',
    'revision_created' => 'revision_created',
    'revision_log_message' => 'revision_log_message',
  ],
  admin_permission: 'administer collection_object',
)]
class CollectionObject extends ContentEntityBase implements EntityPublishedInterface {

  use EntityPublishedTrait;
  use RevisionLogEntityTrait;

  public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
    $fields = parent::baseFieldDefinitions($entity_type);
    $fields += static::publishedBaseFieldDefinitions($entity_type);
    $fields += static::revisionLogBaseFieldDefinitions($entity_type);
    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(new TranslatableMarkup('Title'))
      ->setRevisionable(TRUE)
      ->setRequired(TRUE);
    return $fields;
  }

}

If workspaces is enabled when such an entity type is installed, Drupal adds a workspace revision metadata field automatically. If workspaces is installed later, hook_module_preinstall() of the module updates every supported entity type definition, which adds the same revision field to each revision table. On a site with very large tables, that schema update is the part of enabling Workspaces that takes time; plan it for a maintenance window, as described in Update API and post-updates.

Two further consequences matter for content design:

  • References from a staged entity to an entity of an unsupported type that was created inside the workspace are rejected by an EntityReferenceSupportedNewEntities constraint that the module adds to the entity reference field type, because such an entity would exist on Live immediately while the referencing revision does not.

  • Path aliases are workspace-aware, and the module removes the PathAlias uniqueness constraint from the path field so that a workspace-specific revision may change an alias. See Menus and path aliases.

Publishing a workspace

Publishing is done from the activated workspace with the Publish content action, which opens a confirmation form (route entity.workspace.publish_form, /admin/config/workflow/workspaces/manage/{workspace}/publish) listing how many items will be published to Live. Programmatically, the entity method publish() hands the work to the publisher created by the workspaces.operation_factory service.

<?php

// In a drush script or a service that receives the factory by autowiring.
$workspace = \Drupal::entityTypeManager()->getStorage('workspace')->load('deep_sea_opening');
if ($workspace !== NULL && !$workspace->hasParent()) {
  $workspace->publish();
}

What WorkspacePublisher::publish() does, in order:

  1. Refuses to publish a workspace that has a parent (WorkspacePublishException).

  2. Checks for conflicts on the target. The check is currently a no-op, because a conflicting state cannot arise: an entity being edited in one workspace cannot be edited elsewhere (see the limitations).

  3. Reads the tracked revisions of the workspace from the tracker and dispatches a WorkspacePrePublishEvent. If a subscriber called stopPublishing(), a WorkspacePublishException carrying the reason is thrown.

  4. Starts a database transaction and, running outside any workspace, loads the tracked revisions and re-saves each one as the default revision, in "syncing" mode so that other modules do not treat it as a new edit. Each revision’s changed timestamp is set to the publishing time (from Drupal 11.3.0 onwards) and its workspace field is cleared, because a default revision belongs to Live.

  5. Raises the PHP time limit after every batch of entities (the entity_update_batch_size setting, default 50), so large workspaces are not killed by max_execution_time.

  6. Commits the transaction; on any exception it rolls back, logs the error and rethrows, so Live is never left half-updated.

  7. Dispatches a WorkspacePostPublishEvent.

The consequence for operations is that publishing is atomic for the database but not instantaneous for caches. After the commit, cache tags of the saved entities are invalidated as for any entity save, and a reverse proxy or CDN needs the tag purging described in Performance and caching. Before 11.3 the changed timestamp of published entities was left at the time of the last edit; the change record "Publishing a workspace will update the changed time for its entities" (11.3.0) documents the new behaviour, which also affects "recently updated" Views and sitemaps. In that release the constructor of WorkspacePublisher and WorkspaceOperationFactory gained a required time argument (the old signature is deprecated and fails in Drupal 12).

sequenceDiagram participant E as Editor participant W as Workspace deep_sea_opening participant P as WorkspacePublisher participant DB as Database E->>W: Edit exhibition, events, objects W->>DB: Save non-default revisions tagged with the workspace E->>W: Review the whole site in the workspace E->>P: Publish content P->>P: Dispatch WorkspacePrePublishEvent P->>DB: Transaction: mark tracked revisions as default P->>P: Dispatch WorkspacePostPublishEvent DB-->>E: Live now shows all changes together

Reacting to publishing in code

The pre-publish event lets a module veto publishing, for example while the nightly collection synchronisation (meridian:sync) is running. The events are dispatched by class name, so a subscriber lists the classes as event names. Event subscribers are explained in Events and subscribers.

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\EventSubscriber;

use Drupal\Core\State\StateInterface;
use Drupal\workspaces\Event\WorkspacePostPublishEvent;
use Drupal\workspaces\Event\WorkspacePrePublishEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Blocks publishing during a sync and logs every successful publish.
 */
final class WorkspacePublishSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly StateInterface $state,
    private readonly LoggerInterface $logger,
  ) {}

  public static function getSubscribedEvents(): array {
    return [
      WorkspacePrePublishEvent::class => 'onPrePublish',
      WorkspacePostPublishEvent::class => 'onPostPublish',
    ];
  }

  public function onPrePublish(WorkspacePrePublishEvent $event): void {
    if ($this->state->get('meridian_collection.sync_running', FALSE)) {
      $event->stopPublishing()
        ->setPublishingStoppedReason('The collection sync is running. Try again in a few minutes.');
    }
  }

  public function onPostPublish(WorkspacePostPublishEvent $event): void {
    $this->logger->notice('Workspace @id was published to Live.', [
      '@id' => $event->getWorkspace()->id(),
    ]);
  }

}
# meridian_collection.services.yml
services:
  _defaults:
    autowire: true
  meridian_collection.logger:
    parent: logger.channel_base
    arguments: ['meridian_collection']
  Drupal\meridian_collection\EventSubscriber\WorkspacePublishSubscriber:
    arguments:
      $logger: '@meridian_collection.logger'
    tags:
      - { name: event_subscriber }

The WorkspaceSwitchEvent is dispatched when the active workspace changes; its getWorkspace(), getPreviousWorkspace() and isTemporary() methods let a module, for instance, clear a per-user cache when an editor moves between workspaces.

Running code inside or outside a workspace

Code that must read or write Live data regardless of the user’s active workspace, such as a cron job, wraps the work in executeOutsideWorkspace(); code that must act on a particular workspace uses executeInWorkspace(). Both are methods of workspaces.manager.

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceManagerInterface;

/**
 * Counts objects on display on the live site and in the opening workspace.
 */
final class OnDisplayCounter {

  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly WorkspaceManagerInterface $workspaceManager,
  ) {}

  public function count(?string $workspace_id = NULL): int {
    $run = function (): int {
      return (int) $this->entityTypeManager->getStorage('collection_object')
        ->getQuery()
        ->accessCheck(TRUE)
        ->condition('on_display', TRUE)
        ->count()
        ->execute();
    };
    return $workspace_id === NULL
      ? $this->workspaceManager->executeOutsideWorkspace($run)
      : $this->workspaceManager->executeInWorkspace($workspace_id, $run);
  }

}

Note the explicit accessCheck(TRUE) on the entity query: Drupal requires every entity query to state whether access is checked, and workspace-aware queries are no exception. See Database API and entity queries.

How changes are tracked

Every revision created in a workspace is recorded by the workspace tracker (service workspaces.tracker, interface WorkspaceTrackerInterface, tagged backend_overridable), backed by a table that maps a workspace to entity type, entity ID and revision ID. Two kinds of entries exist:

  • a pending revision of an entity that already exists on Live, tracked by its non-default revision

  • an initial revision, when the entity was created inside the workspace and has no Live counterpart yet; this is why isEntityDeletable() answers true only for entities created in the workspace, since an entity that exists on Live cannot be deleted from a workspace

Sub-workspaces see the tracked entities of their ancestors. Developers who used the older workspaces.association service must switch to workspaces.tracker: the change record "The workspaces.association service has been replaced by workspaces.tracker" (11.3.0) replaces WorkspaceAssociationInterface with WorkspaceTrackerInterface, renaming methods such as getTrackedEntities(), getAllTrackedRevisions(), getTrackedInitialRevisions() and deleteTrackedEntities(). The old service is removed in Drupal 12.0.

Deleting a workspace does not remove its revisions immediately. The workspace ID is queued in the state key workspace.deleted and hook_cron() of the module purges the associated revisions in batches of entity_update_batch_size items (default 50), newest first. A deleted workspace’s machine name cannot be reused until the purge finishes (the DeletedWorkspace constraint). That is why a healthy cron run matters on sites that use workspaces; see Cron, queues and maintenance.

Limitations and pitfalls

One workspace per entity at a time

An entity that has pending changes in one workspace cannot be edited in any other workspace, nor on Live. The EntityWorkspaceConflict constraint, added to every supported entity type, rejects the save with the message "The content is being edited in the @label workspace. As a result, your changes cannot be saved." The Drupal.org usage page lists this under "Limitations of workspaces": it is currently not possible to edit content on Live if the same entity has been edited in another workspace. A feature request, "Ability to edit content in Live while it’s also being edited in other workspaces", is open in the core queue.

For the museum this means that a typo on the Visit page cannot be fixed on Live while the Visit page is part of deep_sea_opening. The options are to fix it inside the workspace and publish early, to keep a short list of pages that are "frozen" for the release and agree who edits them, or to use sub-workspaces so that each team owns separate pages. The constraint is also the reason the publisher’s conflict check has nothing to do: conflicts are prevented, not resolved.

Configuration is not staged

Changing a content type, a view or a block placement while a workspace is active changes the single live configuration immediately. Do not use a workspace to rehearse a site-building change; use a staging environment with configuration management.

Files are not tracked

Uploaded files are ignored by the workspace system. A file uploaded in a workspace exists on Live at once, although only the staged revision refers to it. This avoids copying binaries around but means unpublished images can be guessed by URL if they are stored in the public file system. Use the private file system for embargoed assets, described in File system and private files.

Content moderation and publishing state

Entities edited in a non-default workspace are saved as pending revisions, and the real published flag is applied when the workspace is published. The relationship between workspaces and the draft, review and published states of Content moderation is under active development in core as part of a plan for a simplified content workflow; consult the current documentation before combining the two in a production workflow, and test the exact flow on a copy of the site. The editorial states themselves are covered in Content moderation, workflows and revisions.

Layout Builder, translations and decoupled front ends

Non-reusable blocks inside a Layout Builder layout are tracked with the entity that holds the layout, which is why reusable blocks are the only block content tracked individually. Translations are saved as part of the entity revision, so a workspace stages all translations of the entity; see Multilingual. A decoupled front end reads Live unless its requests carry a workspace; the headless site in Decoupled Drupal with Next.js would need the tokenised workspace and token query parameters or its own negotiator to preview staged content.

Common errors

Symptom Cause and fix

There is no Workspaces menu or switcher after enabling the module on 11.1 or later

Only workspaces is enabled. Enable workspaces_ui as well.

"The content is being edited in the …​ workspace" when saving

Another workspace holds a pending revision of the entity. Publish or discard that workspace’s change first.

A new image is visible on Live before publishing

Files are ignored by workspaces; use the private file system or publish the referencing entity before uploading.

A sub-workspace cannot be published

Only top-level workspaces are published. Merge it into its parent first.

A deleted workspace’s ID cannot be reused immediately

Revisions are purged by cron in batches; run cron until workspace.deleted is empty.

Custom code calls workspaces.association and logs deprecations

Switch to workspaces.tracker before Drupal 12.

Entity queries in a custom report show unexpected rows

The query was run with a workspace active. Use executeOutsideWorkspace() for Live-only data.

Security considerations

  • Treat administer workspaces as a release permission. It allows publishing every staged revision to Live and merging sub-workspaces.

  • Tokenised preview links grant read access to unpublished content. Use short-lived links, avoid sharing them in public channels, and revoke access by deleting or renaming the workspace; the negotiator compares the token with hash_equals() before honouring it.

  • bypass entity access own workspace ignores per-entity access rules for the workspace owner. Give it only to trusted roles, and review it together with the access policies described in Access control and permissions in code.

  • The staged content still passes the normal text format and entity access checks. Staging does not protect against XSS; see Security hardening.

  • Workspace publishing is audited only by the workspaces logger channel and the revision log. If auditing matters, add the subscriber shown above and keep the logs described in Logging, status report and troubleshooting.

Version notes

Version Change

8.8

Sub-workspaces (parent and child workspaces) and merging into the parent.

11.1

workspaces_ui split from workspaces; existing sites get it enabled by an update.

11.3

Publishing updates the changed time of the published entities. workspaces.association is deprecated in favour of workspaces.tracker, and the time argument of the publisher and operation factory becomes required in Drupal 12.

11.x development

The remaining UI classes move to workspaces_ui, with class mappings so older names keep resolving.

12.0

workspaces.association and the old constructor signatures are removed. Workspaces is not among the modules moved to contrib in the 12.0 plan, so staging remains in core.

Drupal 10 reaches end of life on 2026-12-09, and 10.6 is its last minor release; a team that builds a staging workflow on Workspaces should do so on Drupal 11.4 or later, where the module split and the publishing behaviour above apply. Drupal 12.0.0 is scheduled for the week of 2026-12-07; see Preparing for Drupal 12.