How n8n Versions Workflows and Stores Them in the Database
n8n stores every workflow change as an immutable snapshot in the workflow_history table while maintaining the current editable state in WorkflowEntity, using UUID-based versionId pointers and an activeVersionId to track which version executes in production.
The n8n workflow automation platform implements a robust versioning system that persists every modification to your automations. According to the n8n-io/n8n source code, this system relies on a dual-table database architecture that separates the current working workflow from immutable historical snapshots, enabling precise version control, rollback capabilities, and efficient storage management.
The Two-Table Architecture for Workflow Versioning
n8n’s versioning system splits workflow data across two distinct database entities: one for the current mutable state and one for immutable historical records.
WorkflowEntity: The Current Workflow State
The current workflow definition lives in packages/@n8n/db/src/entities/workflow-entity.ts, which defines three critical versioning columns on lines 100-105:
versionId: A UUID pointing to the latest stored version in historyactiveVersionId: A UUID pointing to the version currently used by executions (the "published" version)versionCounter: An incremental integer for UI ordering and display
When a user saves a draft or publishes a workflow, the system updates versionId to a new UUID and increments versionCounter. The activeVersionId only changes when a specific version is explicitly activated for production use.
WorkflowHistory: Immutable Version Snapshots
Every historical version resides in packages/@n8n/db/src/entities/workflow-history.ts (lines 11-35). This entity stores:
versionId: The primary key (UUID) linking back toWorkflowEntitynodes: Complete JSON definition of the workflow nodesconnections: JSON mapping of node connectionsauthors: Metadata about who made the changeautosaved: Boolean flag distinguishing automatic saves from manual publishes
This table treats every row as immutable—once inserted, the snapshot never changes, creating a complete audit trail of workflow evolution.
Creating New Workflow Versions
The WorkflowHistoryService.saveVersion method in packages/cli/src/workflows/workflow-history/workflow-history.service.ts (lines 27-35) handles persisting new versions. This method executes an INSERT operation for every save action, whether manual or automatic.
await workflowHistoryService.saveVersion(
user, // User object or string author name
{
versionId: uuid(), // New UUID for the version
nodes: workflow.nodes,
connections: workflow.connections,
},
workflowId, // ID of the WorkflowEntity
false, // autosaved = false for manual saves
);
Each call generates a new row in workflow_history with a unique versionId, while the parent WorkflowEntity updates its versionId pointer to match. This ensures the current workflow always references its latest historical snapshot.
Querying Workflow Versions
Retrieving versions uses optimized queries that distinguish between metadata browsing and full content loading.
Listing Version Metadata
For pagination and browsing, WorkflowHistoryService.getList (lines 28-59) queries only lightweight metadata, explicitly excluding the large JSON columns (nodes and connections):
const versions = await workflowHistoryService.getList(
user,
workflowId,
take = 20, // pagination limit
skip = 0,
);
// Returns: [{ versionId, authors, createdAt, name, description }, ...]
This approach prevents memory bloat when displaying version history in the UI.
Fetching Complete Version Snapshots
To load a specific version for restoration or inspection, WorkflowHistoryService.getVersion (lines 62-85) retrieves the full workflow definition:
const version = await workflowHistoryService.getVersion(
user,
workflowId,
versionId,
{ includePublishHistory: true },
);
// version.nodes and version.connections contain the complete workflow JSON
This method looks up the row by composite key (workflowId + versionId) and optionally loads publish history relationships.
Activating Specific Versions for Execution
Versioning separates editing from execution through the activeVersionId pointer. When you activate a specific version, the system updates WorkflowEntity.activeVersionId to reference that historical snapshot and emits a workflow-version-updated event.
// API endpoint internal logic
await workflowsController.activate(workflowId, versionId);
// Updates WorkflowEntity.activeVersionId = versionId
// Emits workflow-version-updated event for downstream services
This design allows you to edit draft versions while maintaining a stable, published version for active executions.
Pruning Old Versions to Manage Storage
To prevent unlimited database growth, n8n implements automatic compaction through WorkflowHistoryRepository.deleteEarlierThanExceptCurrentAndActive in packages/@n8n/db/src/repositories/workflow-history.repository.ts (lines 26-53).
await workflowHistoryRepository.deleteEarlierThanExceptCurrentAndActive(
new Date(Date.now() - 30 * DAY_MS), // cutoff date
preserveNamedVersions = true, // keep user-named versions
);
This method removes records older than the specified cutoff while preserving:
- The current version (referenced by
WorkflowEntity.versionId) - The active version (referenced by
WorkflowEntity.activeVersionId) - Any named versions (those with user-given names/labels)
Background services invoke this pruning logic to maintain performance while retaining critical history.
Summary
- Dual-table architecture:
WorkflowEntitystores current pointers whileworkflow_historystores immutable snapshots - UUID versioning: Every save generates a new
versionIdlinking the current entity to its historical record - Execution isolation: The
activeVersionIdpointer determines which version runs in production, separate from the draft editing cycle - Optimized queries: Listing operations exclude heavy JSON columns, while specific version lookups return complete node definitions
- Automatic cleanup: Background pruning removes old unnamed versions while preserving current, active, and named snapshots
Frequently Asked Questions
What is the difference between versionId and activeVersionId in n8n?
The versionId column in WorkflowEntity always points to the most recently saved version of a workflow, representing the current draft state. The activeVersionId specifically references the version that is currently "published" and used by execution engines. This separation allows you to save draft changes without affecting production workflows.
How does n8n store workflow versions to enable rollback?
n8n stores every version as an immutable row in the workflow_history table containing complete nodes and connections JSON. When you request a rollback, the system retrieves the specific versionId from WorkflowHistoryService.getVersion and restores those JSON definitions to the current workflow entity, effectively reverting to that historical state.
Does n8n automatically delete old workflow versions?
Yes, n8n includes automatic pruning through the WorkflowHistoryRepository.deleteEarlierThanExceptCurrentAndActive method. This background process removes versions older than a configurable threshold (typically 30 days) while preserving the current version, the active version, and any versions with user-assigned names.
How can I retrieve a specific historical version of a workflow?
Use the WorkflowHistoryService.getVersion method, passing the workflowId and specific versionId. Set includePublishHistory: true in the options parameter if you need publication metadata. This returns the complete workflow definition including all nodes and connections stored in that snapshot.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →