From 31baf52528457c69e44984fe9915d058d3f3384f Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 10 Apr 2026 14:49:14 +0200 Subject: [PATCH] Workflow - Avoid billing skipped steps (#19547) As title --- ...orkflow-executor.workspace-service.spec.ts | 52 ++++++++++++++++++- .../workflow-executor.workspace-service.ts | 6 ++- .../workflow-runner.workspace-service.ts | 1 + 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts index 11f92fa2b6..907c96bbaf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts @@ -14,6 +14,8 @@ import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service' import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util'; +import { shouldFailSafely } from 'src/modules/workflow/workflow-executor/utils/should-fail-safely.util'; +import { shouldSkipStepExecution } from 'src/modules/workflow/workflow-executor/utils/should-skip-step-execution.util'; import { type WorkflowAction, WorkflowActionType, @@ -30,11 +32,25 @@ jest.mock( return { ...actual, - shouldExecuteStep: jest.fn().mockReturnValue(true), // default behavior + shouldExecuteStep: jest.fn().mockReturnValue(true), }; }, ); +jest.mock( + 'src/modules/workflow/workflow-executor/utils/should-fail-safely.util', + () => ({ + shouldFailSafely: jest.fn().mockReturnValue(false), + }), +); + +jest.mock( + 'src/modules/workflow/workflow-executor/utils/should-skip-step-execution.util', + () => ({ + shouldSkipStepExecution: jest.fn().mockReturnValue(false), + }), +); + describe('WorkflowExecutorWorkspaceService', () => { let service: WorkflowExecutorWorkspaceService; let workflowActionFactory: WorkflowActionFactory; @@ -358,6 +374,40 @@ describe('WorkflowExecutorWorkspaceService', () => { }); }); + it('should not emit billing event for skipped steps', async () => { + (shouldExecuteStep as jest.Mock).mockReturnValue(false); + (shouldSkipStepExecution as jest.Mock).mockReturnValue(true); + + await service.executeFromSteps({ + workflowRunId: mockWorkflowRunId, + stepIds: ['step-1'], + workspaceId: mockWorkspaceId, + }); + + expect(workflowActionFactory.get).not.toHaveBeenCalled(); + expect(workspaceEventEmitter.emitCustomBatchEvent).not.toHaveBeenCalled(); + + (shouldExecuteStep as jest.Mock).mockReturnValue(true); + (shouldSkipStepExecution as jest.Mock).mockReturnValue(false); + }); + + it('should not emit billing event for fail-safely steps', async () => { + (shouldExecuteStep as jest.Mock).mockReturnValue(false); + (shouldFailSafely as jest.Mock).mockReturnValue(true); + + await service.executeFromSteps({ + workflowRunId: mockWorkflowRunId, + stepIds: ['step-1'], + workspaceId: mockWorkspaceId, + }); + + expect(workflowActionFactory.get).not.toHaveBeenCalled(); + expect(workspaceEventEmitter.emitCustomBatchEvent).not.toHaveBeenCalled(); + + (shouldExecuteStep as jest.Mock).mockReturnValue(true); + (shouldFailSafely as jest.Mock).mockReturnValue(false); + }); + it('should return if step should not be executed', async () => { (shouldExecuteStep as jest.Mock).mockReturnValueOnce(false); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index 212fb1715f..3d7a747023 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -177,7 +177,11 @@ export class WorkflowExecutorWorkspaceService { const isError = isDefined(actionOutput.error) && !actionOutput.shouldFailSafely; - if (!isError && !actionOutput.shouldFailSafely) { + if ( + !isError && + !actionOutput.shouldFailSafely && + !actionOutput.shouldSkipStepExecution + ) { this.sendWorkflowNodeRunEvent(workspaceId, workflowRun.workflowId); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts index a562bbd37e..a39cd51933 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts @@ -83,6 +83,7 @@ export class WorkflowRunnerWorkspaceService { const isHardThrottled = await this.checkHardThrottleLimit(workspaceId); if (isHardThrottled) { + this.logger.log(`Workflow throttled for workspace ${workspaceId}`); return this.createFailedWorkflowRun({ workspaceId, workflowVersionId,