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 history
  • activeVersionId: 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 to WorkflowEntity
  • nodes: Complete JSON definition of the workflow nodes
  • connections: JSON mapping of node connections
  • authors: Metadata about who made the change
  • autosaved: 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: WorkflowEntity stores current pointers while workflow_history stores immutable snapshots
  • UUID versioning: Every save generates a new versionId linking the current entity to its historical record
  • Execution isolation: The activeVersionId pointer 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →