Introducing the concept of workspaces. Content entities always belong to a workspace (there is one main exception, which is the user entity type). A workspace is a silo/container of content on a site. However, this phase only introduces the underlying concept with one single workspace available, without many supporting APIs around it (see later phases). This phase will not change any UIs or behaviours in core.

See Workspace module for the current contrib implementation.

  • Define the workspace entity itself
  • Introduce the workspace reference field for all content entity types
  • Extend storage handlers to work with workspaces

Architecture overview

  • Workspaces are content entities.
  • A "live" and "stage" workspace is created on installation.
  • The default workspace (the one anonymous users see) is set in as a parameter.
  • Workspaces depend on revisionable and publishable content, therefore only Nodes are supported.
  • No queries, entity loads, or anything are altered for the default workspace.
  • Content is linked to a workspace via a ContentWorkspace entity (similar to Content Moderation's ContentModerationState entity).
  • All content added on non-default workspaces is saved as unpublished. The real published states is saved against the ContentWorkspace entity.
  • There is a computed entity_reference field "workspace" added to all entity types that can belong to a workspace.
  • An entity query alter is performed on non-default workspaces to join the ContentWorkspace entity field revision table and only load entities which are in the active (current) or default workspace.
  • A Views query alter is done in the a similar way to the entity query alter, although here we also take into account the published status from the ContentWorksapce entity if the View has a published filter.
  • In hook_entity_load we switch out the entity revision for the correct one for the current active workspace based on data stored in the ContentWorkspace entity. The published status is also switched if needed.
  • Each workspace can have an upstream, this is like a git upstream, when deploying content is replicated to the upstream.
  • Upstreams are plugins. In the workspace module these are derived from workspaces. Contrib could hook into this to make upstreams anything, other sites, non-drupal apps, etc.
  • Replication is handled by a tagged service, again, this allows contrib to hook in and provide different replication.
  • The default replicator looks for changes between two workspaces then loops through all these revisions and saves them on the target workspace.
  • Each replication creates a replication log entity. For the default replicator this is based on an MD5 hash of the source and target workspaces, so when replicating again between those two workspaces we know what replications have already happened.
  • All entity updates create an entity in the sequence index. This index is also used in replication two find the changes since the last replication.
  • The user's active workspace is determined via a negotiator, so it can either come from the container parameter (default workspace), a session value, or a url query parameter.
  • There is complex access checks which will allow you to lock down users to only create content in specific workspaces.
  • The active workspace is switched by either using the toolbar.module integration, or enabling a switcher block.


timmillwood created an issue. See original summary.

timmillwood’s picture

Opening this issue to discuss the implementation details for workspaces.

In the Multiversion and Workspace modules we define workspaces as a content entity, then add a workspace field to all entity types to denote which workspace an entity belongs to, then every query has to check the workspace field.

I can't see this passing as a core solution, there are many alternatives, but none that get around adding a condition to each entity query.

timmillwood’s picture

Issue tags: +Workflow Initiative
catch’s picture

For CPS there is a similar concept (called 'site versions'). The way it gets around the condition on every query was the following:

1. There is a hard-coded, 'published' site version. When viewing the 'published' site (i.e. not previewing), any query alters are skipped.

2. When previewing the site in a site version, you have to join on the tracking table, can't really avoid that.

3. Additionally, only entities modified in a site version are tracked in that table - it uses a CASE IF WHEN clause in the query alter to either get the default revision for untracked entities or the draft revision for entities in the workspace.

I'm not sure what the storage should look like for core, but conceptually this means the best performance when not previewing (i.e. no change at all), and allows for workspace storage to only track changes, not every entity. (After a site version is published the full history is stored, which does/has caused problems with scaling, but there are issues discussing that in the CPS queue).

Publishing a site version is then taking the 'target' revision for each entity and making it the default revision (then changing status of the site version itself) - this means the full process of publishing is usually a handful of entity saves, which can happen in a single transaction.

Fabianx’s picture

+1 to #4.

The query alter only when not in the 'published' workspace, did work very well for us.

Also a table + COALESCE() on the join did also wonders for us.

I think COALESCE is supported also for other DB engines.


Uhm and cloning entities per workspace is a no-go for core. Should just use revisions and track those, but always work on 'live' if not overridden in the workspace (e.g. similar to an always rebased branch).

That also worked very well and scalable for us in CPS.

timmillwood’s picture

Coming back to this (21 days later) I want to make sure I fully understand how this all works.

  • The "Live" workspace behaves exactly how standard Drupal works, all the same queries, all the same entity api.
  • When content is added to the "stage" workspace how do we prevent it from appearing on "Live"?
  • If an entity is added on Workspace "A" and another added on Workspace "B" we can join the tracking table to know which workspace should show which entity, but on live we would not join the tracking table, so what would stop it showing all entities?

Then... where do we add the under laying concept of workspace? is adding it in a module stable enough? or should it go into core, much like translations are. Yes, to do translations you need the translation modules, but the entity API can do translations without it.

catch’s picture

When content is added to the "stage" workspace how do we prevent it from appearing on "Live"?

1. Save an initial, default revision. Regardless of the 'published' status of the entity, set it to unpublished. This way it can never show up on the live site.

2. Save a draft revision, with the published status however it was set on the entity, this one is attached to the workspace. Since this will be 'published' in the workspace (if it was set to published on the form), it'll show up.

CPS does this, except the revision saving is tricky because Drupal 7 doesn't allow you to save a revision without updating the base table, but the end result is the same.

This requires published/unpublished support for all entities that can be put into workspaces.

Not sure on the where question. First inclination is to put it in a module though, its not really needed except for whole-site-preview and publishing-sets-of-content-at-once, and sites can be built without either of those.

timmillwood’s picture

So if all content is added as unpublished on all workspaces no matter which workspace the content was added, wouldn't this get confusing to people viewing /admin/content?

Fabianx’s picture

#8: A further part that CPS does here is to remove unpublished content (unpublished on the base entity) from listings while outside of a changeset / site version (e.g. in the live changeset).

And because you cannot just un-publish something, but need to go via the workflow, that works very well.

At least that is how I remember this works.

timmillwood’s picture

I think for this to work we need to be able to unpublished all of the things. #2810381: Add generic status field to ContentEntityBase is looking to introduce a generic 'status' base field, which we can then add to more entity types.

My current thinking is:

  • Workspace content entity to define workspaces
  • All content would belong in the "live" workspace
  • When content is created or updated in a "non-live" workspace we update a table (or entity) to denote which workspace the new entity/revision belongs to.
  • Replicating content from a non-live to a live workspace is just a case of removing the entry from the table (or entity), at which point the entity will "belong" to the live workspace.
catch’s picture

Replicating content from a non-live to a live workspace is just a case of removing the entry from the table (or entity), at which point the entity will "belong" to the live workspace.

For reference the way CPS does this is the following:

1. Publishing a workspace means 'make the revision associated with the workspace/site version the default one' - which either publishes a draft revision or publishes the entity in general if it's new). Ideally that should happen in a single transaction.

2. The workspace/site version gets 'archived' - so the historical information of what was in it is available. (CPS also records the state of all entities on the site at the time it was published, for historical review and rollback which I don't think we should do in core since it's an implementation nightmare).

timmillwood’s picture

Today I committed a 8.x-2.x branch of the Workspace module. I hope this will form the basis for the core experimental module.

So far I have:

  • Moved parts of Multiversion into Workspace
  • Removed all dependencies
  • Confirmed workspace types can be added
  • Confirmed workspaces can be added
  • Confirmed workspaces can be switched between using the toolbar
  • Confirmed the active workspace state is retained


  • More tidy up
  • Make tests pass
  • Mark content as belonging to a workspace

During this time I also need to work out how replicating content will actually work. The CPS approaches above will play a big part in this. I would also like to use the services we have in the Replication module to determine differences between workspaces. This will allow us to be flexible enough for multi-site replication as well as the core single site preview functionality.

To mark content as belonging to a workspace I'd like to use the Content Moderation approach of a content entity and computed entity reference fields. Thus adding a ContentWorkspace entity which references the content entity and the workspace it belongs to.

For the CPS approach to fully work we need BlockContent, Term, and MenuLinkContent entities (at a minimum) to be unpublishable, #2810381: Add generic status field to ContentEntityBase will go a long way to making this happen.

timmillwood’s picture

The 8.2.x branch of Workspace module now has a ContentWorkspace entity type which gets updated when an entity is created or updated. The workspace field is a computed field which returns the Workspace via ContentWorkspace, or the default Workspace.

Trying to work out the best way of getting entities to only show in their correct workspace.

timmillwood’s picture

Status: Active » Needs review
131.05 KB

Here's an initial patch for Workspace module.

It will fail on a couple of things (like hook_help), but I will work on tidy ups today.

What it does do is introduce WorkspaceType and Workspace entities, it then has toolbar integration to switch between workspaces. Every entity that is added gets a ContentWorkspace entity to map it to a workspace, much like in Content Moderation how we map an entity to it's moderation state.

One thing I'd like to get done before commit is only showing entities which belong to the current workspace, but still looking for the best way to do this. @catch? @Fabianx?

Replication is something I think should be added as a follow-up, yes, the module is pretty useless without replication, but to do it properly it has a lot of dependencies.

Status: Needs review » Needs work

The last submitted patch, 14: 2784921-14.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
132.72 KB
3.44 KB

Trying to fix some of the failures from #14

Status: Needs review » Needs work

The last submitted patch, 16: 2784921-16.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
133.77 KB
1.05 KB

This should fix the \Drupal\system\Tests\Module\InstallUninstallTest issue.

After speaking to @amateescu and @dixon there was concerns about getting Workspace module in without replication. Personally I'd like to get it in as a starting block so we can then work on multiple little patches, rather than one big one. Although I can understand the concerns with having a module in core that doesn't really do much.

dawehner’s picture

Well, one question I would ask myself here. What is the concrete usecase we solve with this additional experimental module. Can endusers somehow profit from it? Having an experimental module without any enduser features is indeed a big weird. I'm wondering whether we could implement a mini version of replication, or some other feature which workspaces would allow, even if it would be thrown away later.

timmillwood’s picture

@dawehner - I think a mini version of replication would be cool, I remember we did do this in the early days of the contrib module and it did have some big bugs, but is specific use cases it worked ok. So maybe this could be one approach.

The other approach is just develop it in contrib for now, then push to core as one big patch when ready. To do replication properly (following the couchdb protocol) there are many dependencies.

timmillwood’s picture

Status: Needs review » Needs work

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

timmillwood’s picture

Status: Needs work » Needs review

I have been working on the workspace module for core in the 8.x-2.x branch of contrib, I'd happily take reviews there

It's still in pretty early stages, but taking influence from CPS no queries are altered on the "live" workspace, on all other workspace we alter queries, and hook into the node load to only show entities that are live or belong to the workspace. Basic replication of content between workspaces is also in place with a test to back this up.

xjm’s picture

Status: Needs review » Postponed

@timmillwood commented on #2755073: WI: Content Moderation module roadmap that the Workflow team wanted to target this for 8.4.

It's a ways down the roadmap in #2721129: Workflow Initiative and hadn't been discussed with release managers yet. We'd like to see Content Moderation and Workflow stabilize before we add additional workflow features to core. We'd also like to ensure bugs like #2856363: Path alias changes for draft revisions immediately leak into live site and issues with forward revisions are addressed before this is considered for core (they are probably also blockers for Content Moderation being stable anyway).

So, marking postponed for now based on that. Thanks @timmillwood!

xjm’s picture

FWIW, #1239558: Deleting a node with revisions does not release file usage was also a must-have for Phase A and not complete, and this is Phase F. (Also the revision upgrade path is still listed as Phase A, but I'm unclear on the status of that.)

timmillwood’s picture

Status: Postponed » Needs review
150.45 KB
203.55 KB

Time for an update, lets see if this works.

Status: Needs review » Needs work

The last submitted patch, 26: 2784921-26.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
458 bytes
203.64 KB

Let's give this another go.

Status: Needs review » Needs work

The last submitted patch, 28: 2784921-28.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review

ok, fixed this test fail in contrib. Will add a new patch here in due course, but until then... please review!

timmillwood’s picture

Issue summary: View changes