From 4aa910f625f47f3dd36bd9948906cc7c49ec22b2 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 15 Sep 2026 18:39:19 +0200 Subject: [PATCH] feat(workflow): mirror the workflow row to core synchronously on activation --- .../services/workflow-core-sync.service.ts | 22 +++ .../workflow-tool.workspace-service.ts | 3 + .../tools/create-complete-workflow.tool.ts | 6 + .../types/workflow-tool-dependencies.type.ts | 2 + .../workflow-tools/workflow-tools.module.ts | 2 + .../workflow-trigger.module.ts | 2 + .../workflow-trigger.workspace-service.ts | 7 + ...activation-core-mirror.integration-spec.ts | 137 ++++++++++++++++++ 8 files changed, 181 insertions(+) create mode 100644 packages/twenty-server/test/integration/graphql/suites/workflow/workflow-activation-core-mirror.integration-spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-core-sync.service.ts b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-core-sync.service.ts index b4b0235e083..9cd7114a71e 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-core-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-core-sync.service.ts @@ -167,6 +167,28 @@ export class WorkflowCoreSyncService { ); } + async mirrorWorkflowById( + workspaceId: string, + workflowId: string, + ): Promise { + const workflow = await this.workspaceOrmManager.executeInWorkspaceContext( + async () => { + return await this.workspaceOrmManager + .getRepository('workflow', { + shouldBypassPermissionChecks: true, + }) + .findOne({ where: { id: workflowId } }); + }, + buildSystemAuthContext(workspaceId), + ); + + if (!isDefined(workflow)) { + return; + } + + await this.upsertToCore(workspaceId, [workflow]); + } + async deleteFromCore( workspaceId: string, coreWorkflowIds: string[], diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 86acdf15255..ae2363916b6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; +import { WorkflowCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-core-sync.service'; import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -60,6 +61,7 @@ export class WorkflowToolWorkspaceService { agentService: AgentService, workflowCommonService: WorkflowCommonWorkspaceService, workflowVersionCoreSyncService: WorkflowVersionCoreSyncService, + workflowCoreSyncService: WorkflowCoreSyncService, ) { this.deps = { workflowVersionStepService, @@ -76,6 +78,7 @@ export class WorkflowToolWorkspaceService { agentService, workflowCommonService, workflowVersionCoreSyncService, + workflowCoreSyncService, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts index 04c4bb52715..8e988dca00b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts @@ -60,6 +60,7 @@ type CreateCompleteWorkflowToolDeps = Pick< | 'workspaceOrmManager' | 'recordPositionService' | 'workflowVersionCoreSyncService' + | 'workflowCoreSyncService' >; type CreateCompleteWorkflowToolContext = WorkflowToolContext & { @@ -324,4 +325,9 @@ const updateWorkflowStatus = async ({ lastPublishedVersionId: workflowVersionId, }); }, authContext); + + await deps.workflowCoreSyncService.mirrorWorkflowById( + context.workspaceId, + workflowId, + ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts index 2bf02d53551..577c4b8678a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts @@ -1,4 +1,5 @@ import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; +import type { WorkflowCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-core-sync.service'; import type { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; import type { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; import type { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; @@ -28,6 +29,7 @@ export type WorkflowToolDependencies = { agentService: AgentService; workflowCommonService: WorkflowCommonWorkspaceService; workflowVersionCoreSyncService: WorkflowVersionCoreSyncService; + workflowCoreSyncService: WorkflowCoreSyncService; }; export type WorkflowToolContext = { diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts index bca6621d143..e1e19ea5389 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts @@ -5,6 +5,7 @@ import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provid import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; +import { WorkflowCoreModule } from 'src/engine/core-modules/workflow/workflow-core.module'; import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module'; @@ -33,6 +34,7 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace WorkspaceManyOrAllFlatEntityMapsCacheModule, AiAgentModule, WorkflowVersionCoreModule, + WorkflowCoreModule, ], providers: [ WorkflowToolWorkspaceService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts index 4de5f6fa66f..5db22fb3354 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts @@ -6,6 +6,7 @@ import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache- import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module'; import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; +import { WorkflowCoreModule } from 'src/engine/core-modules/workflow/workflow-core.module'; import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module'; @@ -27,6 +28,7 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t FeatureFlagModule, LogicFunctionModule, WorkflowVersionCoreModule, + WorkflowCoreModule, WorkflowVersionValidationModule, ], providers: [WorkflowTriggerWorkspaceService, WorkflowTriggerJob], diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 23e2744f6ef..5c4a6c4ddb3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -11,6 +11,7 @@ import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decora import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { buildCoreDispatchIds } from 'src/engine/core-modules/workflow/utils/build-core-dispatch-ids.util'; +import { WorkflowCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-core-sync.service'; import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service'; import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; @@ -68,6 +69,7 @@ export class WorkflowTriggerWorkspaceService { private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly commandMenuItemService: CommandMenuItemService, private readonly workflowVersionCoreSyncService: WorkflowVersionCoreSyncService, + private readonly workflowCoreSyncService: WorkflowCoreSyncService, @InjectCacheStorage(CacheStorageNamespace.ModuleWorkflow) private readonly cacheStorageService: CacheStorageService, ) {} @@ -368,6 +370,11 @@ export class WorkflowTriggerWorkspaceService { }, ); + await this.workflowCoreSyncService.mirrorWorkflowById( + workspaceId, + workflow.id, + ); + await this.createOrUpdateCommandMenuItem( workflow, workflowVersion, diff --git a/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-activation-core-mirror.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-activation-core-mirror.integration-spec.ts new file mode 100644 index 00000000000..35f9502a84a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-activation-core-mirror.integration-spec.ts @@ -0,0 +1,137 @@ +import { updateWorkflowVersionTrigger } from 'test/integration/graphql/suites/workflow/utils/update-workflow-version-trigger.util'; +import { workflowGraphqlRequest } from 'test/integration/graphql/suites/workflow/utils/workflow-graphql-request.util'; + +const GET_CORE_WORKFLOW_QUERY = ` + query GetCoreWorkflow($workspaceWorkflowId: UUID!) { + coreWorkflow(workspaceWorkflowId: $workspaceWorkflowId) { + id + statuses + lastPublishedVersionId + } + } +`; + +describe('workflow activation mirrors the core workflow row synchronously (e2e)', () => { + let workspaceWorkflowId: string; + let workflowVersionId: string; + + beforeAll(async () => { + const createResponse = await workflowGraphqlRequest(` + mutation { + createCoreWorkflow(input: { name: "Activation Core Mirror" }) { + id + workspaceWorkflowId + } + } + `); + + expect(createResponse.body.errors).toBeUndefined(); + + workspaceWorkflowId = + createResponse.body.data.createCoreWorkflow.workspaceWorkflowId; + + const versionsResponse = await workflowGraphqlRequest( + ` + query GetWorkflow($id: UUID!) { + workflow(filter: { id: { eq: $id } }) { + versions { + edges { + node { + id + } + } + } + } + } + `, + { id: workspaceWorkflowId }, + ); + + expect(versionsResponse.body.errors).toBeUndefined(); + + workflowVersionId = + versionsResponse.body.data.workflow.versions.edges[0].node.id; + + await updateWorkflowVersionTrigger({ + workflowVersionId, + trigger: { + name: 'Webhook Trigger', + type: 'WEBHOOK', + settings: { + outputSchema: {}, + httpMethod: 'GET', + authentication: null, + }, + nextStepIds: [], + position: { x: 0, y: 0 }, + }, + }); + + const createStepResponse = await workflowGraphqlRequest( + ` + mutation CreateWorkflowVersionStep( + $input: CreateWorkflowVersionStepInput! + ) { + createWorkflowVersionStep(input: $input) { + stepsDiff + } + } + `, + { + input: { + workflowVersionId, + stepType: 'CODE', + parentStepId: 'trigger', + position: { x: 200, y: 0 }, + }, + }, + ); + + expect(createStepResponse.body.errors).toBeUndefined(); + }); + + afterAll(async () => { + await workflowGraphqlRequest( + ` + mutation DestroyWorkflow($id: ID!) { + destroyWorkflow(id: $id) { + id + } + } + `, + { id: workspaceWorkflowId }, + ); + }); + + it('starts with no published version on the core row', async () => { + const response = await workflowGraphqlRequest(GET_CORE_WORKFLOW_QUERY, { + workspaceWorkflowId, + }); + + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.coreWorkflow.lastPublishedVersionId).toBeNull(); + }); + + it('exposes the published version on the core row as soon as activation returns', async () => { + const activateResponse = await workflowGraphqlRequest( + ` + mutation ActivateWorkflowVersion($workflowVersionId: UUID!) { + activateWorkflowVersion(workflowVersionId: $workflowVersionId) + } + `, + { workflowVersionId }, + ); + + expect(activateResponse.body.errors).toBeUndefined(); + + const response = await workflowGraphqlRequest(GET_CORE_WORKFLOW_QUERY, { + workspaceWorkflowId, + }); + + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.coreWorkflow.lastPublishedVersionId).toBe( + workflowVersionId, + ); + expect(response.body.data.coreWorkflow.statuses).toContain('ACTIVE'); + }); +});