diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index bff4b9354..dab693ffe 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -2274,6 +2274,7 @@ export type WorkflowAction = { __typename?: 'WorkflowAction'; id: Scalars['UUID']; name: Scalars['String']; + nextStepIds?: Maybe>; settings: Scalars['JSON']; type: Scalars['String']; valid: Scalars['Boolean']; @@ -2893,7 +2894,7 @@ export type CreateWorkflowVersionStepMutationVariables = Exact<{ }>; -export type CreateWorkflowVersionStepMutation = { __typename?: 'Mutation', createWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } }; +export type CreateWorkflowVersionStepMutation = { __typename?: 'Mutation', createWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array | null } }; export type DeactivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2907,7 +2908,7 @@ export type DeleteWorkflowVersionStepMutationVariables = Exact<{ }>; -export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } }; +export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array | null } }; export type RunWorkflowVersionMutationVariables = Exact<{ input: RunWorkflowVersionInput; @@ -2921,14 +2922,14 @@ export type UpdateWorkflowRunStepMutationVariables = Exact<{ }>; -export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } }; +export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array | null } }; export type UpdateWorkflowVersionStepMutationVariables = Exact<{ input: UpdateWorkflowVersionStepInput; }>; -export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } }; +export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array | null } }; export type SubmitFormStepMutationVariables = Exact<{ input: SubmitFormStepInput; @@ -5741,6 +5742,7 @@ export const CreateWorkflowVersionStepDocument = gql` type settings valid + nextStepIds } } `; @@ -5809,6 +5811,7 @@ export const DeleteWorkflowVersionStepDocument = gql` type settings valid + nextStepIds } } `; @@ -5879,6 +5882,7 @@ export const UpdateWorkflowRunStepDocument = gql` type settings valid + nextStepIds } } `; @@ -5916,6 +5920,7 @@ export const UpdateWorkflowVersionStepDocument = gql` type settings valid + nextStepIds } } `; diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/createWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/createWorkflowVersionStep.ts index d3d74b76f..0bea0011e 100644 --- a/packages/twenty-front/src/modules/workflow/graphql/mutations/createWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/createWorkflowVersionStep.ts @@ -8,6 +8,7 @@ export const CREATE_WORKFLOW_VERSION_STEP = gql` type settings valid + nextStepIds } } `; diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/deleteWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/deleteWorkflowVersionStep.ts index a8e745f2a..e324f88a9 100644 --- a/packages/twenty-front/src/modules/workflow/graphql/mutations/deleteWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/deleteWorkflowVersionStep.ts @@ -8,6 +8,7 @@ export const DELETE_WORKFLOW_VERSION_STEP = gql` type settings valid + nextStepIds } } `; diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowRunStep.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowRunStep.ts index 8a4040d6b..a5d36a56e 100644 --- a/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowRunStep.ts +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowRunStep.ts @@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_RUN_STEP = gql` type settings valid + nextStepIds } } `; diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowVersionStep.ts index 81674f490..78bf10a25 100644 --- a/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/updateWorkflowVersionStep.ts @@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_VERSION_STEP = gql` type settings valid + nextStepIds } } `; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeleteWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/hooks/useDeleteWorkflowVersionStep.ts index 837985d1b..dfc2ec9c7 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useDeleteWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useDeleteWorkflowVersionStep.ts @@ -6,13 +6,13 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep'; import { WorkflowVersion } from '@/workflow/types/Workflow'; import { useApolloClient, useMutation } from '@apollo/client'; +import { isDefined } from 'twenty-shared/utils'; import { DeleteWorkflowVersionStepInput, DeleteWorkflowVersionStepMutation, DeleteWorkflowVersionStepMutationVariables, WorkflowAction, } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; export const useDeleteWorkflowVersionStep = () => { const apolloClient = useApolloClient(); @@ -47,9 +47,16 @@ export const useDeleteWorkflowVersionStep = () => { const newCachedRecord = { ...cachedRecord, - steps: (cachedRecord.steps || []).filter( - (step: WorkflowAction) => step.id !== deletedStep.id, - ), + steps: (cachedRecord.steps || []) + .filter((step: WorkflowAction) => step.id !== deletedStep.id) + .map((step) => { + return { + ...step, + nextStepIds: step.nextStepIds?.filter( + (nextStepId) => nextStepId !== deletedStep.id, + ), + }; + }), }; const recordGqlFields = { diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts index bb245d772..baf534625 100644 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -21,6 +21,7 @@ export const baseWorkflowActionSchema = z.object({ id: z.string(), name: z.string(), valid: z.boolean(), + nextStepIds: z.array(z.string()).optional().nullable(), }); export const baseTriggerSchema = z.object({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx index 6686e63a0..3237b5f44 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx @@ -3,7 +3,7 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; -import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStep'; +import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStepId'; import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext'; import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep'; import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts index 9fda81ec5..e098fbc21 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts @@ -6,12 +6,12 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep'; import { WorkflowVersion } from '@/workflow/types/Workflow'; import { useApolloClient, useMutation } from '@apollo/client'; +import { isDefined } from 'twenty-shared/utils'; import { CreateWorkflowVersionStepInput, CreateWorkflowVersionStepMutation, CreateWorkflowVersionStepMutationVariables, } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; export const useCreateWorkflowVersionStep = () => { const apolloClient = useApolloClient(); @@ -34,6 +34,7 @@ export const useCreateWorkflowVersionStep = () => { const result = await mutate({ variables: { input }, }); + const createdStep = result?.data?.createWorkflowVersionStep; if (!isDefined(createdStep)) { return; @@ -42,18 +43,31 @@ export const useCreateWorkflowVersionStep = () => { const cachedRecord = getRecordFromCache( input.workflowVersionId, ); + if (!isDefined(cachedRecord)) { return; } + const updatedExistingSteps = + cachedRecord.steps?.map((step) => { + if (step.id === input.parentStepId) { + return { + ...step, + nextStepIds: [...(step.nextStepIds || []), createdStep.id], + }; + } + return step; + }) ?? []; + const newCachedRecord = { ...cachedRecord, - steps: [...(cachedRecord.steps || []), createdStep], + steps: [...updatedExistingSteps, createdStep], }; const recordGqlFields = { steps: true, }; + updateRecordFromCache({ objectMetadataItems, objectMetadataItem, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowPreviousSteps.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowPreviousSteps.test.ts new file mode 100644 index 000000000..118f7bb5b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowPreviousSteps.test.ts @@ -0,0 +1,111 @@ +import { WorkflowStep } from '@/workflow/types/Workflow'; +import { getPreviousSteps } from '../getWorkflowPreviousSteps'; + +const mockWorkflow: WorkflowStep[] = [ + { + id: 'step1', + name: 'First Step', + type: 'CODE', + valid: true, + nextStepIds: ['step2', 'step3'], + settings: { + input: { + serverlessFunctionId: 'func1', + serverlessFunctionVersion: '1.0.0', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: true }, + }, + }, + }, + { + id: 'step2', + name: 'Second Step', + type: 'CODE', + valid: true, + nextStepIds: ['step4'], + settings: { + input: { + serverlessFunctionId: 'func2', + serverlessFunctionVersion: '1.0.0', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: true }, + }, + }, + }, + { + id: 'step3', + name: 'Third Step', + type: 'CODE', + valid: true, + nextStepIds: ['step4'], + settings: { + input: { + serverlessFunctionId: 'func3', + serverlessFunctionVersion: '1.0.0', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: true }, + }, + }, + }, + { + id: 'step4', + name: 'Fourth Step', + type: 'CODE', + valid: true, + nextStepIds: [], + settings: { + input: { + serverlessFunctionId: 'func4', + serverlessFunctionVersion: '1.0.0', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: true }, + }, + }, + }, +]; + +describe('getWorkflowPreviousSteps', () => { + it('should return empty array when there are no previous steps', () => { + const result = getPreviousSteps(mockWorkflow, 'step1'); + expect(result).toEqual([]); + }); + + it('should return direct previous steps', () => { + const result = getPreviousSteps(mockWorkflow, 'step2'); + expect(result).toEqual([mockWorkflow[0]]); + }); + + it('should return all previous steps including indirect ones', () => { + const result = getPreviousSteps(mockWorkflow, 'step4'); + expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]); + }); + + it('should handle circular dependencies', () => { + const circularWorkflow = [...mockWorkflow]; + circularWorkflow[3].nextStepIds = ['step1']; // Make step4 point back to step1 + + const result = getPreviousSteps(circularWorkflow, 'step4'); + expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]); + }); + + it('should handle non-existent step ID', () => { + const result = getPreviousSteps(mockWorkflow, 'non-existent-step'); + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowRunStepContext.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowRunStepContext.test.ts index 58645bb44..148ab2fab 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowRunStepContext.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getWorkflowRunStepContext.test.ts @@ -55,6 +55,7 @@ describe('getWorkflowRunStepContext', () => { outputSchema: {}, }, valid: true, + nextStepIds: ['step2'], }, { id: 'step2', @@ -72,6 +73,7 @@ describe('getWorkflowRunStepContext', () => { outputSchema: {}, }, valid: true, + nextStepIds: [], }, ], } satisfies WorkflowRunFlow; @@ -195,6 +197,7 @@ describe('getWorkflowRunStepContext', () => { outputSchema: {}, }, valid: true, + nextStepIds: ['step2'], }, { id: 'step2', @@ -212,6 +215,7 @@ describe('getWorkflowRunStepContext', () => { outputSchema: {}, }, valid: true, + nextStepIds: ['step3'], }, { id: 'step3', @@ -229,6 +233,7 @@ describe('getWorkflowRunStepContext', () => { outputSchema: {}, }, valid: true, + nextStepIds: [], }, ], } satisfies WorkflowRunFlow; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStepId.ts similarity index 70% rename from packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStep.ts rename to packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStepId.ts index 5bf8bbf65..d9fc9b814 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousStepId.ts @@ -16,10 +16,7 @@ export const getWorkflowPreviousStepId = ({ return TRIGGER_STEP_ID; } - const stepIndex = steps.findIndex((step) => step.id === stepId); - if (stepIndex === -1) { - throw new Error('Step not found'); - } + const previousStep = steps.find((step) => step.nextStepIds?.includes(stepId)); - return steps[stepIndex - 1].id; + return previousStep?.id; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts new file mode 100644 index 000000000..269c24cc1 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts @@ -0,0 +1,23 @@ +import { WorkflowStep } from '@/workflow/types/Workflow'; + +export const getPreviousSteps = ( + steps: WorkflowStep[], + currentStepId: string, + visitedSteps: Set = new Set([currentStepId]), +): WorkflowStep[] => { + const parentSteps = steps + .filter((step) => step.nextStepIds?.includes(currentStepId)) + .filter((step) => !visitedSteps.has(step.id)); + + const grandParentSteps = parentSteps + .map((step) => { + if (visitedSteps.has(step.id)) { + return []; + } + visitedSteps.add(step.id); + return getPreviousSteps(steps, step.id, visitedSteps); + }) + .flat(); + + return [...grandParentSteps, ...parentSteps]; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowRunStepContext.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowRunStepContext.ts index 2bfea30d5..9b0fa3ed9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowRunStepContext.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowRunStepContext.ts @@ -1,4 +1,5 @@ import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow'; +import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; export const getWorkflowRunStepContext = ({ @@ -10,29 +11,26 @@ export const getWorkflowRunStepContext = ({ context: WorkflowRunContext; flow: WorkflowRunFlow; }) => { - const stepContext: Array<{ id: string; name: string; context: any }> = []; - if (stepId === TRIGGER_STEP_ID) { - return stepContext; + return []; } - stepContext.push({ - id: TRIGGER_STEP_ID, - name: flow.trigger.name ?? 'Trigger', - context: context[TRIGGER_STEP_ID], - }); + const previousSteps = getPreviousSteps(flow.steps, stepId); - for (const step of flow.steps) { - if (step.id === stepId) { - break; - } - - stepContext.push({ + const previousStepsContext = previousSteps.map((step) => { + return { id: step.id, name: step.name, context: context[step.id], - }); - } + }; + }); - return stepContext; + return [ + { + id: TRIGGER_STEP_ID, + name: flow.trigger.name ?? 'Trigger', + context: context[TRIGGER_STEP_ID], + }, + ...previousStepsContext, + ]; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts index acc0180ab..6cd13b324 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts @@ -1,6 +1,7 @@ import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector'; import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow'; +import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { OutputSchema, @@ -18,17 +19,12 @@ export const useAvailableVariablesInWorkflowStep = ({ }): StepOutputSchema[] => { const workflowSelectedNode = useWorkflowSelectedNodeOrThrow(); const flow = useFlowOrThrow(); - const steps = flow.steps ?? []; - const previousStepIds: string[] = []; - - for (const step of steps) { - if (step.id === workflowSelectedNode) { - break; - } - previousStepIds.push(step.id); - } + const previousStepIds: string[] = getPreviousSteps( + steps, + workflowSelectedNode, + ).map((step) => step.id); const availableStepsOutputSchema: StepOutputSchema[] = useRecoilValue( stepsOutputSchemaFamilySelector({ diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-step.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-step.dto.ts index 4ca547ac0..85717b99d 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-step.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-step.dto.ts @@ -21,4 +21,7 @@ export class WorkflowActionDTO { @Field(() => Boolean) valid: boolean; + + @Field(() => [UUIDScalarType], { nullable: true }) + nextStepIds?: string[]; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts index 3edd9a101..82aefc7ff 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts @@ -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', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-executor-input.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-executor-input.ts index 29d8e959a..84e061dba 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-executor-input.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-executor-input.ts @@ -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; workflowRunId: string; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action.ts index 4e67ddddc..2ee659004 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action.ts @@ -22,11 +22,18 @@ export class CodeWorkflowAction implements WorkflowExecutor { ) {} async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action.ts index bf592a068..bed01343c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action.ts @@ -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 { - 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( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts index 20a4533f4..a95b31d62 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts @@ -75,11 +75,18 @@ export class SendEmailWorkflowAction implements WorkflowExecutor { } async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index 17794d17a..e24d634a0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -42,12 +42,18 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor { ) {} async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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', diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts index 1eb3db79c..a1ef48461 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts @@ -35,12 +35,18 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor { ) {} async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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', diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 17cbc92f7..f10e41368 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -45,11 +45,18 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { ) {} async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index 012518f88..ed5d57f0e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -42,11 +42,18 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor { ) {} async execute({ - currentStepIndex, + currentStepId, steps, context, }: WorkflowExecutorInput): Promise { - 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( 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 4083991f0..5ddad1d8a 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 @@ -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, }); 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 1e3148624..e3d2edbac 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 @@ -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 { - 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, diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index e4d84509b..c4b054d29 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -5,7 +5,6 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; @@ -31,7 +30,6 @@ export class RunWorkflowJob { private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, private readonly throttlerService: ThrottlerService, private readonly twentyConfigService: TwentyConfigService, - private readonly twentyORMManager: TwentyORMManager, ) {} @Process(RunWorkflowJob.name) @@ -109,7 +107,7 @@ export class RunWorkflowJob { await this.executeWorkflow({ workflowRunId, - currentStepIndex: 0, + currentStepId: workflowVersion.steps[0].id, steps: workflowVersion.steps, context, }); @@ -134,20 +132,31 @@ export class RunWorkflowJob { ); } - const lastExecutedStepIndex = workflowRun.output?.flow?.steps?.findIndex( + const lastExecutedStep = workflowRun.output?.flow?.steps?.find( (step) => step.id === lastExecutedStepId, ); - if (lastExecutedStepIndex === undefined) { + if (!lastExecutedStep) { throw new WorkflowRunException( 'Last executed step not found', WorkflowRunExceptionCode.INVALID_INPUT, ); } + const nextStepId = lastExecutedStep.nextStepIds?.[0]; + + if (!nextStepId) { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workflowRunId, + status: WorkflowRunStatus.COMPLETED, + }); + + return; + } + await this.executeWorkflow({ workflowRunId, - currentStepIndex: lastExecutedStepIndex + 1, + currentStepId: nextStepId, steps: workflowRun.output?.flow?.steps ?? [], context: workflowRun.context ?? {}, }); @@ -155,19 +164,19 @@ export class RunWorkflowJob { private async executeWorkflow({ workflowRunId, - currentStepIndex, + currentStepId, steps, context, }: { workflowRunId: string; - currentStepIndex: number; + currentStepId: string; steps: WorkflowAction[]; context: Record; }) { const { error, pendingEvent } = await this.workflowExecutorWorkspaceService.execute({ workflowRunId, - currentStepIndex, + currentStepId, steps, context, });