Managing content history is a core strength of the Drupal ecosystem. While the user interface makes it easy to check a box to 'Create new revision,' doing this via code requires a deeper understanding of the Entity API. Whether you are migrating data or automating content updates, knowing how to programmatically create node revisions is essential for maintaining a reliable audit trail.
In this guide, you will learn the correct methods to trigger revisions, how to handle metadata like log messages, and how to navigate the 'dragons' of Content Moderation and forward revisions.
The Standard Approach to Creating Revisions
A common mistake developers make is trying to set a revision property directly on a new node object or manually assigning a Node ID (NID). In modern Drupal (versions 8, 9, 10, and 11), the correct way to ensure an update creates a new entry in the revision table is by using the setNewRevision() method.
To create a revision for an existing node, you must first load the entity, apply your changes, and then explicitly tell Drupal to treat the save operation as a new revision.
// Load the existing node.
$node = \Drupal\node\Entity\Node::load($nid);
// Apply your changes.
$node->set('field_example', 'New Value');
// Explicitly trigger a new revision.
$node->setNewRevision(TRUE);
// Add metadata for the revision log.
$node->revision_log = 'Updated field_example via custom script.';
$node->setRevisionCreationTime(REQUST_TIME);
$node->setRevisionUserId($user_id);
$node->save();
Using $node->setNewRevision(TRUE) ensures that the existing NID is preserved while a new Revision ID (VID) is generated in the database.
Handling Multilingual Revisions
If your Drupal site is multilingual, you may need to create a revision specifically for a translated version of a node. This requires accessing the specific translation from the entity storage before applying the revision logic.
$storage = \Drupal::entityTypeManager()->getStorage('node');
$revision = $storage->load($nid);
// Get the specific translation.
$revision = $revision->getTranslation($languageCode);
$revision->setRevisionCreationTime(\Drupal::time()->getCurrentTime());
// Create the revision through storage.
$revision = $storage->createRevision($revision);
$revision->save();
Navigating Content Moderation and Forward Revisions
When the Content Moderation module (part of Workflows) is enabled, the process becomes significantly more complex. Simply calling setNewRevision(TRUE) on a default node can accidentally destroy "forward revisions"—those draft versions that exist but are not yet published.
If you have a published node (Revision A) and a pending draft (Revision B), saving a new revision based on Revision A might orphan Revision B. To handle this safely, you must check for the latest revision ID, not just the default one.
Robust Update Pattern for Moderated Content
Here is a comprehensive function to update a node while respecting both the default and forward revisions:
function my_module_programmatically_update_a_node($node, $field_name, $value, $log_message = '') {
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
// Update the current object.
$node->set($field_name, $value);
$node->setRevisionUserId(1); // Usually set to an admin for programmatic updates.
$node->setRevisionTranslationAffectedEnforced(TRUE);
$node->setRevisionCreationTime(\Drupal::time()->getRequestTime());
$node->setRevisionLogMessage('Updating default revision: ' . $log_message);
// Check for forward revisions.
$forward_vid = $node_storage->getLatestRevisionId($node->id());
if ($node->getRevisionId() !== $forward_vid &&
$forward_revision = $node_storage->loadRevision($forward_vid)) {
$forward_revision->set($field_name, $value);
$forward_revision->setRevisionTranslationAffectedEnforced(TRUE);
$forward_revision->setRevisionCreationTime(\Drupal::time()->getRequestTime());
$forward_revision->setRevisionLogMessage('Updating latest forward revision: ' . $log_message);
}
// Save the default revision first, then the forward revision.
$node->save();
if (!empty($forward_revision)) {
$forward_revision->save();
}
}
If you are using moderation states, remember to explicitly set the state value if you want the revision to remain a draft:
$node->moderation_state->value = 'draft';
Advanced: Updating Revisions In-Place
Sometimes you don't want to create a new revision, but rather fix data on an existing revision (for example, during a bulk cleanup). To do this, you must prevent Drupal's default behavior of forced revisioning.
By using setSyncing(TRUE), you tell Drupal that this is a synchronization operation, which often bypasses certain hooks and behaviors that force new revisions.
$node->setNewRevision(FALSE);
$node->setSyncing(TRUE);
$node->setRevisionLogMessage("Correcting data on existing revision.");
$node->save();
Example: Batch Updating Forward Revisions
If you have a stack of multiple forward revisions, you can iterate through them to ensure the data is consistent across the entire history:

$revs = $node_storage->revisionIds($node);
$forward_revs = [];
$popped = array_pop($revs);
while(count($revs) > 0 && $popped != $node->getRevisionId()){
$forward_revs[] = $popped;
$popped = array_pop($revs);
}
foreach(array_reverse($forward_revs) as $vid) {
$rev_node = $node_storage->loadRevision($vid);
$rev_node->setNewRevision(FALSE);
$rev_node->setSyncing(TRUE);
$rev_node->save();
}

Common Mistakes to Avoid
- Using
Node::create()for Updates: If you provide annidtoNode::create(), Drupal may try to insert a new row into the node table rather than updating the existing one. Always load the existing node first. - Ignoring the Revision Log: Revisions are useless if you don't know why they were created. Always populate
$node->revision_log(orsetRevisionLogMessage()). - Forgetting Translation Enforcement: If you notice your revisions aren't appearing in the "Revisions" tab for translated content, ensure you call
$node->setRevisionTranslationAffectedEnforced(TRUE).
Frequently Asked Questions
Why does $node->save() create a new node instead of a revision?
This usually happens if you use Node::create() with a manual ID. To update a node and create a revision, you must load the existing node entity from storage using Node::load($nid) before calling save().
How do I set the author of a specific revision?
Use the method $node->setRevisionUserId($uid). This allows you to attribute the change to a specific user (or the system/admin user) without changing the original author of the node itself.
Does creating a revision automatically publish it?
No. The publication status is controlled by $node->setPublished(TRUE/FALSE). If you are using Content Moderation, the status is controlled by the moderation_state field.
Wrapping Up
Programmatically creating revisions in Drupal is a powerful tool for maintaining data integrity. While the basic $node->setNewRevision(TRUE) works for simple sites, developers working with Content Moderation must be careful to account for forward revisions to avoid data loss. By following the patterns above, you can ensure your automated updates are clean, traceable, and compatible with Drupal's complex entity architecture.