Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c901a9b
Add activeVersionId to workflow entity
dariacodes Oct 24, 2025
8e956c4
Refactor activate workflow logic to use active version
dariacodes Oct 26, 2025
ee7ddff
Fix activateAll and deactivateAll CLI commands
dariacodes Oct 26, 2025
bbc3af0
Fix migration to only use existing version ids
dariacodes Oct 26, 2025
343299d
Add tests for the public endpoint, account for active being read-only
dariacodes Oct 26, 2025
232d38e
Add tests for internal update endpoint
dariacodes Oct 26, 2025
194b5fa
Fix executeErrorWorkflow to use active version on activation failure
dariacodes Oct 27, 2025
950d726
Fix production webhooks to execute active version instead of draft
dariacodes Oct 27, 2025
20e35f2
Update migration to not check for version existence anymore (all user…
dariacodes Oct 29, 2025
69120a7
Remove fallbacks to current version
dariacodes Oct 29, 2025
1bfc817
Merge branch 'master' into ado-4277-refactor
dariacodes Oct 29, 2025
346a926
Roll back versionId change for V1 activate
dariacodes Nov 10, 2025
446cc96
Update migration timestamp
dariacodes Nov 10, 2025
b3b1c7a
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 10, 2025
48fd32c
Add activeVersion object to response of getWorkflow(s) V1
dariacodes Nov 10, 2025
d73e641
Remove tests with versionId (will be moved to V2)
dariacodes Nov 10, 2025
0ba99da
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
3b9de5c
Fix import
dariacodes Nov 11, 2025
b421d38
Remove WF history license artifacts from tests
dariacodes Nov 11, 2025
43322c1
Update active version on PATCH only when needed
dariacodes Nov 11, 2025
8e67761
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
2fdc885
Improve tests
dariacodes Nov 11, 2025
7c35aa0
Fix import order
dariacodes Nov 11, 2025
f66b162
Remove an obsolete check
dariacodes Nov 11, 2025
9625aec
Remove duplicate tests
dariacodes Nov 11, 2025
e6907e7
Add missing relation
dariacodes Nov 11, 2025
2736a89
Fix api schema
dariacodes Nov 11, 2025
f84d1f4
Move active version flags to ref for api schema
dariacodes Nov 11, 2025
fb1c30f
Set active version on workflow creation (e.g. through import)
dariacodes Nov 11, 2025
3d3b70d
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
c5f374f
Update migration to never remove active versions from workflow history
dariacodes Nov 13, 2025
5a44c1d
Add activeVersionId to workflow entity, update queries
dariacodes Nov 13, 2025
8269116
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 13, 2025
dc802f1
Fix entity truncate order in tests
dariacodes Nov 13, 2025
6f9130c
Update pruning logic to not delete active versions as well
dariacodes Nov 13, 2025
7939fb0
Improve cli workflow update commands tests
dariacodes Nov 13, 2025
a4dc093
Improve workflow repository tests
dariacodes Nov 13, 2025
acb8fcf
Move version saving up during workflow creation
dariacodes Nov 13, 2025
8b3afb7
Attempt to solve foreign key circular dependencies for MySQL and MariaDB
dariacodes Nov 13, 2025
cea6e0f
Fix quotes in activateAll query
dariacodes Nov 13, 2025
56f92c0
Add more truncation in tests
dariacodes Nov 13, 2025
56c30cc
Truncate junction tables
dariacodes Nov 13, 2025
0368bdc
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 14, 2025
5bea05f
Use activeVersionId on backend where active is used
dariacodes Nov 18, 2025
f0ef29a
Use activeVersionId on frontend where active is used
dariacodes Nov 18, 2025
c2361e7
Update tests
dariacodes Nov 18, 2025
e4f0ff1
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 18, 2025
1f86751
Fix lint issue
dariacodes Nov 18, 2025
123db55
Fix mcp tests
dariacodes Nov 18, 2025
110e5ae
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 18, 2025
3760bcb
Fix roll-back value
dariacodes Nov 18, 2025
92ab28c
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 20, 2025
dc154a3
Make the check in job processor more explicit
dariacodes Nov 20, 2025
0f8bcce
Use only activeVersionId in source control import
dariacodes Nov 20, 2025
41062b7
Return correct active status for execution workflow data
dariacodes Nov 20, 2025
fe653e7
Improve checks for active workflow manager
dariacodes Nov 20, 2025
b02abf0
Fix test active workflow manager test mocks
dariacodes Nov 20, 2025
1c4975b
Remove activeVersionId from WorkflowDataUpdate
dariacodes Nov 20, 2025
fd139f9
Use the right workflow version in execute additional data
dariacodes Nov 20, 2025
2e5a093
Remove activeVersionId filter from useWorkflowHelpers
dariacodes Nov 20, 2025
e6b74ca
Fix comments
dariacodes Nov 20, 2025
aa8a29f
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 20, 2025
4721f7e
Add a test checking that current version is used for manual executions
dariacodes Nov 20, 2025
fe34ed7
Rename util
dariacodes Nov 20, 2025
ba03d02
Update db workflow repo test
dariacodes Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/@n8n/backend-test-utils/src/db/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ProjectRepository,
SharedWorkflowRepository,
WorkflowRepository,
WorkflowHistoryRepository,
} from '@n8n/db';
import { Container } from '@n8n/di';
import type { WorkflowSharingRole } from '@n8n/permissions';
Expand Down Expand Up @@ -185,3 +186,33 @@ export async function getAllSharedWorkflows() {

export const getWorkflowById = async (id: string) =>
await Container.get(WorkflowRepository).findOneBy({ id });

/**
* Create a workflow history record for a workflow
* @param workflow workflow to create history for
* @param user user who created the version (optional)
*/
export async function createWorkflowHistory(workflow: IWorkflowDb, user?: User): Promise<void> {
await Container.get(WorkflowHistoryRepository).insert({
workflowId: workflow.id,
versionId: workflow.versionId,
nodes: workflow.nodes,
connections: workflow.connections,
authors: user?.email ?? '[email protected]',
});
}

/**
* Set the active version for a workflow
* @param workflowId workflow ID
* @param versionId version ID to set as active
*/
export async function setActiveVersion(workflowId: string, versionId: string): Promise<void> {
const workflowHistory = await Container.get(WorkflowHistoryRepository).findOneOrFail({
where: { workflowId, versionId },
});

await Container.get(WorkflowRepository).update(workflowId, {
activeVersion: workflowHistory,
});
}
5 changes: 5 additions & 0 deletions packages/@n8n/db/src/entities/workflow-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { SharedWorkflow } from './shared-workflow';
import type { TagEntity } from './tag-entity';
import type { TestRun } from './test-run.ee';
import type { ISimplifiedPinData, IWorkflowDb } from './types-db';
import type { WorkflowHistory } from './workflow-history';
import type { WorkflowStatistics } from './workflow-statistics';
import type { WorkflowTagMapping } from './workflow-tag-mapping';
import { objectRetriever, sqlite } from '../utils/transformers';
Expand Down Expand Up @@ -103,6 +104,10 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
@Column({ length: 36 })
versionId: string;

@ManyToOne('WorkflowHistory', { nullable: true })
@JoinColumn({ name: 'activeVersionId', referencedColumnName: 'versionId' })
activeVersion: WorkflowHistory | null;

@Column({ default: 1 })
versionCounter: number;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';

const WORKFLOWS_TABLE_NAME = 'workflow_entity';
const WORKFLOW_HISTORY_TABLE_NAME = 'workflow_history';

export class AddActiveVersionIdColumn1762819200000 implements ReversibleMigration {
async up({
schemaBuilder: { addColumns, column, addForeignKey },
queryRunner,
escape,
}: MigrationContext) {
const workflowsTableName = escape.tableName(WORKFLOWS_TABLE_NAME);

await addColumns(WORKFLOWS_TABLE_NAME, [column('activeVersionId').varchar(36)]);

await addForeignKey(
WORKFLOWS_TABLE_NAME,
'activeVersionId',
[WORKFLOW_HISTORY_TABLE_NAME, 'versionId'],
undefined,
'SET NULL',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having this asRESTRICT would make more sense. I don't think we want to remove a version that is active

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this constraint created problems for truncating tables in MySQL and MariaDB. How do you feel about this fix?

);

// For existing ACTIVE workflows, set activeVersionId = versionId
const versionIdColumn = escape.columnName('versionId');
const activeColumn = escape.columnName('active');
const activeVersionIdColumn = escape.columnName('activeVersionId');

await queryRunner.query(
`UPDATE ${workflowsTableName}
SET ${activeVersionIdColumn} = ${versionIdColumn}
WHERE ${activeColumn} = true`,
);
}

async down({ schemaBuilder: { dropColumns, dropForeignKey } }: MigrationContext) {
await dropForeignKey(WORKFLOWS_TABLE_NAME, 'activeVersionId', [
WORKFLOW_HISTORY_TABLE_NAME,
'versionId',
]);
await dropColumns(WORKFLOWS_TABLE_NAME, ['activeVersionId']);
}
}
2 changes: 2 additions & 0 deletions packages/@n8n/db/src/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@
import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000000-CreateWorkflowDependencyTable';
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddActiveVersionIdColumn1762819200000 } from './../common/1762819200000-AddActiveVersionIdColumn';
import type { Migration } from '../migration-types';

Check failure on line 115 in packages/@n8n/db/src/migrations/mysqldb/index.ts

View workflow job for this annotation

GitHub Actions / Lint / Lint

`../migration-types` type import should occur before import of `./../common/1762819200000-AddActiveVersionIdColumn`

export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
Expand Down Expand Up @@ -227,4 +228,5 @@
AddWorkflowDescriptionColumn1762177736257,
CreateOAuthEntities1760116750277,
BackfillMissingWorkflowHistoryRecords1762763704614,
AddActiveVersionIdColumn1762819200000,
];
2 changes: 2 additions & 0 deletions packages/@n8n/db/src/migrations/postgresdb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000
import { DropUnusedChatHubColumns1760965142113 } from '../common/1760965142113-DropUnusedChatHubColumns';
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddActiveVersionIdColumn1762819200000 } from '../common/1762819200000-AddActiveVersionIdColumn';
import type { Migration } from '../migration-types';

export const postgresMigrations: Migration[] = [
Expand Down Expand Up @@ -225,4 +226,5 @@ export const postgresMigrations: Migration[] = [
AddWorkflowDescriptionColumn1762177736257,
CreateOAuthEntities1760116750277,
BackfillMissingWorkflowHistoryRecords1762763704614,
AddActiveVersionIdColumn1762819200000,
];
2 changes: 2 additions & 0 deletions packages/@n8n/db/src/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000
import { DropUnusedChatHubColumns1760965142113 } from '../common/1760965142113-DropUnusedChatHubColumns';
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddActiveVersionIdColumn1762819200000 } from '../common/1762819200000-AddActiveVersionIdColumn';
import type { Migration } from '../migration-types';

const sqliteMigrations: Migration[] = [
Expand Down Expand Up @@ -219,6 +220,7 @@ const sqliteMigrations: Migration[] = [
AddWorkflowDescriptionColumn1762177736257,
CreateOAuthEntities1760116750277,
BackfillMissingWorkflowHistoryRecords1762763704614,
AddActiveVersionIdColumn1762819200000,
];

export { sqliteMigrations };
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,15 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
where?: FindOptionsWhere<SharedWorkflow>;
includeTags?: boolean;
includeParentFolder?: boolean;
includeActiveVersion?: boolean;
em?: EntityManager;
} = {},
) {
const {
where = {},
includeTags = false,
includeParentFolder = false,
includeActiveVersion = false,
em = this.manager,
} = options;

Expand All @@ -169,6 +171,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
shared: { project: { projectRelations: { user: true } } },
tags: includeTags,
parentFolder: includeParentFolder,
activeVersion: includeActiveVersion,
},
},
});
Expand Down
7 changes: 4 additions & 3 deletions packages/@n8n/db/src/repositories/workflow.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async findById(workflowId: string) {
return await this.findOne({
where: { id: workflowId },
relations: { shared: { project: { projectRelations: true } } },
relations: { shared: { project: { projectRelations: true } }, activeVersion: true },
});
}

Expand Down Expand Up @@ -763,11 +763,12 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
}

async deactivateAll() {
return await this.update({ active: true }, { active: false });
return await this.update({ active: true }, { active: false, activeVersion: null });
}

// We're planning to remove this command in V2, so for now just set activeVersion to null so that executions would use the current version
async activateAll() {
return await this.update({ active: false }, { active: true });
return await this.update({ active: false }, { active: true, activeVersion: null });
}

async findByActiveState(activeState: boolean) {
Expand Down
73 changes: 72 additions & 1 deletion packages/cli/src/__tests__/active-workflow-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mockLogger } from '@n8n/backend-test-utils';
import type { WorkflowEntity, WorkflowRepository } from '@n8n/db';
import type { WorkflowEntity, WorkflowHistory, WorkflowRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended';
import type { InstanceSettings } from 'n8n-core';
import type {
Expand Down Expand Up @@ -165,4 +165,75 @@ describe('ActiveWorkflowManager', () => {
expect(getAllActiveIds).toHaveBeenCalledTimes(1);
});
});

describe('activateWorkflow', () => {
beforeEach(() => {
// Set up as leader to allow workflow activation
Object.assign(instanceSettings, { isLeader: true });
});

test('should use active version when calling executeErrorWorkflow on activation failure', async () => {
// Create different nodes for draft vs active version
const draftNodes = [
{
id: 'draft-node-1',
name: 'Draft Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
position: [0, 0] as [number, number],
parameters: {},
},
];

const activeNodes = [
{
id: 'active-node-1',
name: 'Active Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
position: [0, 0] as [number, number],
parameters: {},
},
];

const activeVersion = mock<WorkflowHistory>({
versionId: 'v1',
workflowId: 'workflow-1',
nodes: activeNodes,
connections: {},
authors: 'test-user',
createdAt: new Date(),
updatedAt: new Date(),
});

const workflowEntity = mock<WorkflowEntity>({
id: 'workflow-1',
name: 'Test Workflow',
active: true,
nodes: draftNodes,
connections: {},
activeVersion,
});

workflowRepository.findById.mockResolvedValue(workflowEntity);

// Mock the add method to throw an error (simulating activation failure)
jest.spyOn(activeWorkflowManager, 'add').mockRejectedValue(new Error('Authorization failed'));

const executeErrorWorkflowSpy = jest
.spyOn(activeWorkflowManager, 'executeErrorWorkflow')
.mockImplementation(() => {});

await activeWorkflowManager['activateWorkflow']('workflow-1', 'init');

expect(executeErrorWorkflowSpy).toHaveBeenCalled();

// Get the workflow data that was passed to executeErrorWorkflow
const callArgs = executeErrorWorkflowSpy.mock.calls[0];
const workflowData = callArgs[1];

expect(workflowData.nodes).toEqual(activeNodes);
expect(workflowData.nodes[0].name).toBe('Active Webhook');
});
});
});
39 changes: 34 additions & 5 deletions packages/cli/src/active-workflow-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,26 @@ export class ActiveWorkflowManager {
async clearWebhooks(workflowId: WorkflowId) {
const workflowData = await this.workflowRepository.findOne({
where: { id: workflowId },
relations: { activeVersion: true },
});

if (workflowData === null) {
throw new UnexpectedError('Could not find workflow', { extra: { workflowId } });
}

if (!workflowData.activeVersion) {
throw new UnexpectedError('Active version not found for workflow', {
extra: { workflowId },
});
}

const { nodes, connections } = workflowData.activeVersion;

const workflow = new Workflow({
id: workflowId,
name: workflowData.name,
nodes: workflowData.nodes,
connections: workflowData.connections,
nodes,
connections,
active: workflowData.active,
nodeTypes: this.nodeTypes,
staticData: workflowData.staticData,
Expand Down Expand Up @@ -488,8 +497,17 @@ export class ActiveWorkflowManager {
},
);

if (!dbWorkflow.activeVersion) {
throw new UnexpectedError('Active version not found for workflow', {
extra: { workflowId: dbWorkflow.id },
});
}

const { nodes, connections } = dbWorkflow.activeVersion;
const workflowForError = { ...dbWorkflow, nodes, connections };

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.executeErrorWorkflow(error, dbWorkflow, 'internal');
this.executeErrorWorkflow(error, workflowForError, 'internal');

// do not keep trying to activate on authorization error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Expand Down Expand Up @@ -577,11 +595,22 @@ export class ActiveWorkflowManager {
return added;
}

// Get workflow data from the active version
if (!dbWorkflow.activeVersion) {
throw new UnexpectedError('Active version not found for workflow', {
extra: { workflowId: dbWorkflow.id },
});
}

const { nodes, connections } = dbWorkflow.activeVersion;
dbWorkflow.nodes = nodes;
dbWorkflow.connections = connections;

workflow = new Workflow({
id: dbWorkflow.id,
name: dbWorkflow.name,
nodes: dbWorkflow.nodes,
connections: dbWorkflow.connections,
nodes,
connections,
active: dbWorkflow.active,
nodeTypes: this.nodeTypes,
staticData: dbWorkflow.staticData,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/public-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export declare namespace WorkflowRequest {
type Get = AuthenticatedRequest<{ id: string }, {}, {}, { excludePinnedData?: boolean }>;
type Delete = Get;
type Update = AuthenticatedRequest<{ id: string }, {}, WorkflowEntity, {}>;
type Activate = Get;
type Activate = AuthenticatedRequest<{ id: string }, {}, {}, {}>;
type GetTags = Get;
type UpdateTags = AuthenticatedRequest<{ id: string }, {}, TagEntity[]>;
type Transfer = AuthenticatedRequest<{ id: string }, {}, { destinationProjectId: string }>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
type: object
additionalProperties: false
properties:
versionId:
type: string
readOnly: true
description: Unique identifier for this workflow version
example: 7c6b9e3f-8d4a-4b2c-9f1e-6a5d3b8c7e4f
workflowId:
type: string
readOnly: true
description: The workflow this version belongs to
example: 2tUt1wbLX592XDdX
nodes:
type: array
readOnly: true
items:
$ref: './node.yml'
connections:
type: object
readOnly: true
example: { Jira: { main: [[{ node: 'Jira', type: 'main', index: 0 }]] } }
authors:
type: string
readOnly: true
description: Comma-separated list of author IDs who contributed to this version
example: 1,2,3
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
Loading
Loading