Start using next step ids (#11683)

- update workflow executor
- update next step ids on step creation/deletion
- use these in workflow run
- use these in variables
This commit is contained in:
Thomas Trompette
2025-04-29 16:29:25 +02:00
committed by GitHub
parent 19f46a0091
commit d8b2e1fb34
28 changed files with 325 additions and 105 deletions
@@ -9,4 +9,5 @@ export class WorkflowStepExecutorException extends CustomException {
export enum WorkflowStepExecutorExceptionCode {
SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND',
INVALID_STEP_TYPE = 'INVALID_STEP_TYPE',
STEP_NOT_FOUND = 'STEP_NOT_FOUND',
}
@@ -1,7 +1,7 @@
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export type WorkflowExecutorInput = {
currentStepIndex: number;
currentStepId: string;
steps: WorkflowAction[];
context: Record<string, unknown>;
workflowRunId: string;
@@ -22,11 +22,18 @@ export class CodeWorkflowAction implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowCodeAction(step)) {
throw new WorkflowStepExecutorException(
@@ -13,10 +13,17 @@ import { isWorkflowFormAction } from 'src/modules/workflow/workflow-executor/wor
@Injectable()
export class FormWorkflowAction implements WorkflowExecutor {
async execute({
currentStepIndex,
currentStepId,
steps,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowFormAction(step)) {
throw new WorkflowStepExecutorException(
@@ -75,11 +75,18 @@ export class SendEmailWorkflowAction implements WorkflowExecutor {
}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowSendEmailAction(step)) {
throw new WorkflowStepExecutorException(
@@ -42,12 +42,18 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowCreateRecordAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not a create record action',
@@ -35,12 +35,18 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowDeleteRecordAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not a delete record action',
@@ -45,11 +45,18 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowFindRecordsAction(step)) {
throw new WorkflowStepExecutorException(
@@ -42,11 +42,18 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps[currentStepIndex];
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowUpdateRecordAction(step)) {
throw new WorkflowStepExecutorException(
@@ -107,6 +107,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false },
},
},
nextStepIds: ['step-2'],
},
{
id: 'step-2',
@@ -117,6 +118,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false },
},
},
nextStepIds: [],
},
] as WorkflowAction[];
@@ -124,7 +126,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
// No steps to execute
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 2,
currentStepId: 'step-2',
steps: mockSteps,
context: mockContext,
});
@@ -145,7 +147,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: mockSteps,
context: mockContext,
});
@@ -156,7 +158,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
);
expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: mockSteps,
context: mockContext,
attemptCount: 1,
@@ -199,7 +201,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: mockSteps,
context: mockContext,
});
@@ -231,7 +233,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: mockSteps,
context: mockContext,
});
@@ -265,6 +267,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false },
},
},
nextStepIds: ['step-2'],
},
{
id: 'step-2',
@@ -284,7 +287,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: stepsWithContinueOnFailure,
context: mockContext,
});
@@ -330,7 +333,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: stepsWithRetryOnFailure,
context: mockContext,
});
@@ -367,7 +370,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: stepsWithRetryOnFailure,
context: mockContext,
attemptCount: 3, // MAX_RETRIES_ON_FAILURE is 3
@@ -394,7 +397,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({
workflowRunId: mockWorkflowRunId,
currentStepIndex: 0,
currentStepId: 'step-1',
steps: mockSteps,
context: mockContext,
});
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
@@ -38,22 +40,20 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
) {}
async execute({
currentStepIndex,
currentStepId,
steps,
context,
attemptCount = 1,
workflowRunId,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
if (currentStepIndex >= steps.length) {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
return {
result: {
success: true,
},
error: 'Step not found',
};
}
const step = steps[currentStepIndex];
const workflowExecutor = this.workflowExecutorFactory.get(step.type);
let actionOutput: WorkflowExecutorOutput;
@@ -80,7 +80,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
try {
actionOutput = await workflowExecutor.execute({
currentStepIndex,
currentStepId,
steps,
context,
attemptCount,
@@ -111,11 +111,17 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
return actionOutput;
}
if (actionOutput.result) {
const updatedContext = {
...context,
[step.id]: actionOutput.result,
};
const shouldContinue =
isDefined(actionOutput.result) ||
step.settings.errorHandlingOptions.continueOnFailure.value;
if (shouldContinue) {
const updatedContext = isDefined(actionOutput.result)
? {
...context,
[step.id]: actionOutput.result,
}
: context;
await this.workflowRunWorkspaceService.saveWorkflowRunState({
workflowRunId,
@@ -123,36 +129,26 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
context: updatedContext,
});
if (!isDefined(step.nextStepIds?.[0])) {
return actionOutput;
}
// TODO: handle multiple next steps
return await this.execute({
workflowRunId,
currentStepIndex: currentStepIndex + 1,
currentStepId: step.nextStepIds[0],
steps,
context: updatedContext,
});
}
if (step.settings.errorHandlingOptions.continueOnFailure.value) {
await this.workflowRunWorkspaceService.saveWorkflowRunState({
workflowRunId,
stepOutput,
context,
});
return await this.execute({
workflowRunId,
currentStepIndex: currentStepIndex + 1,
steps,
context,
});
}
if (
step.settings.errorHandlingOptions.retryOnFailure.value &&
attemptCount < MAX_RETRIES_ON_FAILURE
) {
return await this.execute({
workflowRunId,
currentStepIndex,
currentStepId,
steps,
context,
attemptCount: attemptCount + 1,