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

View File

@ -2274,6 +2274,7 @@ export type WorkflowAction = {
__typename?: 'WorkflowAction'; __typename?: 'WorkflowAction';
id: Scalars['UUID']; id: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
settings: Scalars['JSON']; settings: Scalars['JSON'];
type: Scalars['String']; type: Scalars['String'];
valid: Scalars['Boolean']; 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<any> | null } };
export type DeactivateWorkflowVersionMutationVariables = Exact<{ export type DeactivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; 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<any> | null } };
export type RunWorkflowVersionMutationVariables = Exact<{ export type RunWorkflowVersionMutationVariables = Exact<{
input: RunWorkflowVersionInput; 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<any> | null } };
export type UpdateWorkflowVersionStepMutationVariables = Exact<{ export type UpdateWorkflowVersionStepMutationVariables = Exact<{
input: UpdateWorkflowVersionStepInput; 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<any> | null } };
export type SubmitFormStepMutationVariables = Exact<{ export type SubmitFormStepMutationVariables = Exact<{
input: SubmitFormStepInput; input: SubmitFormStepInput;
@ -5741,6 +5742,7 @@ export const CreateWorkflowVersionStepDocument = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;
@ -5809,6 +5811,7 @@ export const DeleteWorkflowVersionStepDocument = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;
@ -5879,6 +5882,7 @@ export const UpdateWorkflowRunStepDocument = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;
@ -5916,6 +5920,7 @@ export const UpdateWorkflowVersionStepDocument = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;

View File

@ -8,6 +8,7 @@ export const CREATE_WORKFLOW_VERSION_STEP = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;

View File

@ -8,6 +8,7 @@ export const DELETE_WORKFLOW_VERSION_STEP = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;

View File

@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_RUN_STEP = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;

View File

@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_VERSION_STEP = gql`
type type
settings settings
valid valid
nextStepIds
} }
} }
`; `;

View File

@ -6,13 +6,13 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep'; import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
import { WorkflowVersion } from '@/workflow/types/Workflow'; import { WorkflowVersion } from '@/workflow/types/Workflow';
import { useApolloClient, useMutation } from '@apollo/client'; import { useApolloClient, useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { import {
DeleteWorkflowVersionStepInput, DeleteWorkflowVersionStepInput,
DeleteWorkflowVersionStepMutation, DeleteWorkflowVersionStepMutation,
DeleteWorkflowVersionStepMutationVariables, DeleteWorkflowVersionStepMutationVariables,
WorkflowAction, WorkflowAction,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
export const useDeleteWorkflowVersionStep = () => { export const useDeleteWorkflowVersionStep = () => {
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -47,9 +47,16 @@ export const useDeleteWorkflowVersionStep = () => {
const newCachedRecord = { const newCachedRecord = {
...cachedRecord, ...cachedRecord,
steps: (cachedRecord.steps || []).filter( steps: (cachedRecord.steps || [])
(step: WorkflowAction) => step.id !== deletedStep.id, .filter((step: WorkflowAction) => step.id !== deletedStep.id)
), .map((step) => {
return {
...step,
nextStepIds: step.nextStepIds?.filter(
(nextStepId) => nextStepId !== deletedStep.id,
),
};
}),
}; };
const recordGqlFields = { const recordGqlFields = {

View File

@ -21,6 +21,7 @@ export const baseWorkflowActionSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
valid: z.boolean(), valid: z.boolean(),
nextStepIds: z.array(z.string()).optional().nullable(),
}); });
export const baseTriggerSchema = z.object({ export const baseTriggerSchema = z.object({

View File

@ -3,7 +3,7 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer'; import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; 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 { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep'; import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow'; import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';

View File

@ -6,12 +6,12 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep'; import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep';
import { WorkflowVersion } from '@/workflow/types/Workflow'; import { WorkflowVersion } from '@/workflow/types/Workflow';
import { useApolloClient, useMutation } from '@apollo/client'; import { useApolloClient, useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { import {
CreateWorkflowVersionStepInput, CreateWorkflowVersionStepInput,
CreateWorkflowVersionStepMutation, CreateWorkflowVersionStepMutation,
CreateWorkflowVersionStepMutationVariables, CreateWorkflowVersionStepMutationVariables,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
export const useCreateWorkflowVersionStep = () => { export const useCreateWorkflowVersionStep = () => {
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -34,6 +34,7 @@ export const useCreateWorkflowVersionStep = () => {
const result = await mutate({ const result = await mutate({
variables: { input }, variables: { input },
}); });
const createdStep = result?.data?.createWorkflowVersionStep; const createdStep = result?.data?.createWorkflowVersionStep;
if (!isDefined(createdStep)) { if (!isDefined(createdStep)) {
return; return;
@ -42,18 +43,31 @@ export const useCreateWorkflowVersionStep = () => {
const cachedRecord = getRecordFromCache<WorkflowVersion>( const cachedRecord = getRecordFromCache<WorkflowVersion>(
input.workflowVersionId, input.workflowVersionId,
); );
if (!isDefined(cachedRecord)) { if (!isDefined(cachedRecord)) {
return; return;
} }
const updatedExistingSteps =
cachedRecord.steps?.map((step) => {
if (step.id === input.parentStepId) {
return {
...step,
nextStepIds: [...(step.nextStepIds || []), createdStep.id],
};
}
return step;
}) ?? [];
const newCachedRecord = { const newCachedRecord = {
...cachedRecord, ...cachedRecord,
steps: [...(cachedRecord.steps || []), createdStep], steps: [...updatedExistingSteps, createdStep],
}; };
const recordGqlFields = { const recordGqlFields = {
steps: true, steps: true,
}; };
updateRecordFromCache({ updateRecordFromCache({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,

View File

@ -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([]);
});
});

View File

@ -55,6 +55,7 @@ describe('getWorkflowRunStepContext', () => {
outputSchema: {}, outputSchema: {},
}, },
valid: true, valid: true,
nextStepIds: ['step2'],
}, },
{ {
id: 'step2', id: 'step2',
@ -72,6 +73,7 @@ describe('getWorkflowRunStepContext', () => {
outputSchema: {}, outputSchema: {},
}, },
valid: true, valid: true,
nextStepIds: [],
}, },
], ],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;
@ -195,6 +197,7 @@ describe('getWorkflowRunStepContext', () => {
outputSchema: {}, outputSchema: {},
}, },
valid: true, valid: true,
nextStepIds: ['step2'],
}, },
{ {
id: 'step2', id: 'step2',
@ -212,6 +215,7 @@ describe('getWorkflowRunStepContext', () => {
outputSchema: {}, outputSchema: {},
}, },
valid: true, valid: true,
nextStepIds: ['step3'],
}, },
{ {
id: 'step3', id: 'step3',
@ -229,6 +233,7 @@ describe('getWorkflowRunStepContext', () => {
outputSchema: {}, outputSchema: {},
}, },
valid: true, valid: true,
nextStepIds: [],
}, },
], ],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;

View File

@ -16,10 +16,7 @@ export const getWorkflowPreviousStepId = ({
return TRIGGER_STEP_ID; return TRIGGER_STEP_ID;
} }
const stepIndex = steps.findIndex((step) => step.id === stepId); const previousStep = steps.find((step) => step.nextStepIds?.includes(stepId));
if (stepIndex === -1) {
throw new Error('Step not found');
}
return steps[stepIndex - 1].id; return previousStep?.id;
}; };

View File

@ -0,0 +1,23 @@
import { WorkflowStep } from '@/workflow/types/Workflow';
export const getPreviousSteps = (
steps: WorkflowStep[],
currentStepId: string,
visitedSteps: Set<string> = 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];
};

View File

@ -1,4 +1,5 @@
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow'; 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'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
export const getWorkflowRunStepContext = ({ export const getWorkflowRunStepContext = ({
@ -10,29 +11,26 @@ export const getWorkflowRunStepContext = ({
context: WorkflowRunContext; context: WorkflowRunContext;
flow: WorkflowRunFlow; flow: WorkflowRunFlow;
}) => { }) => {
const stepContext: Array<{ id: string; name: string; context: any }> = [];
if (stepId === TRIGGER_STEP_ID) { if (stepId === TRIGGER_STEP_ID) {
return stepContext; return [];
} }
stepContext.push({ const previousSteps = getPreviousSteps(flow.steps, stepId);
id: TRIGGER_STEP_ID,
name: flow.trigger.name ?? 'Trigger',
context: context[TRIGGER_STEP_ID],
});
for (const step of flow.steps) { const previousStepsContext = previousSteps.map((step) => {
if (step.id === stepId) { return {
break;
}
stepContext.push({
id: step.id, id: step.id,
name: step.name, name: step.name,
context: context[step.id], context: context[step.id],
}); };
} });
return stepContext; return [
{
id: TRIGGER_STEP_ID,
name: flow.trigger.name ?? 'Trigger',
context: context[TRIGGER_STEP_ID],
},
...previousStepsContext,
];
}; };

View File

@ -1,6 +1,7 @@
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector'; import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow'; 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 { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { import {
OutputSchema, OutputSchema,
@ -18,17 +19,12 @@ export const useAvailableVariablesInWorkflowStep = ({
}): StepOutputSchema[] => { }): StepOutputSchema[] => {
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow(); const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
const flow = useFlowOrThrow(); const flow = useFlowOrThrow();
const steps = flow.steps ?? []; const steps = flow.steps ?? [];
const previousStepIds: string[] = []; const previousStepIds: string[] = getPreviousSteps(
steps,
for (const step of steps) { workflowSelectedNode,
if (step.id === workflowSelectedNode) { ).map((step) => step.id);
break;
}
previousStepIds.push(step.id);
}
const availableStepsOutputSchema: StepOutputSchema[] = useRecoilValue( const availableStepsOutputSchema: StepOutputSchema[] = useRecoilValue(
stepsOutputSchemaFamilySelector({ stepsOutputSchemaFamilySelector({

View File

@ -21,4 +21,7 @@ export class WorkflowActionDTO {
@Field(() => Boolean) @Field(() => Boolean)
valid: boolean; valid: boolean;
@Field(() => [UUIDScalarType], { nullable: true })
nextStepIds?: string[];
} }

View File

@ -9,4 +9,5 @@ export class WorkflowStepExecutorException extends CustomException {
export enum WorkflowStepExecutorExceptionCode { export enum WorkflowStepExecutorExceptionCode {
SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND', SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND',
INVALID_STEP_TYPE = 'INVALID_STEP_TYPE', INVALID_STEP_TYPE = 'INVALID_STEP_TYPE',
STEP_NOT_FOUND = 'STEP_NOT_FOUND',
} }

View File

@ -1,7 +1,7 @@
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export type WorkflowExecutorInput = { export type WorkflowExecutorInput = {
currentStepIndex: number; currentStepId: string;
steps: WorkflowAction[]; steps: WorkflowAction[];
context: Record<string, unknown>; context: Record<string, unknown>;
workflowRunId: string; workflowRunId: string;

View File

@ -22,11 +22,18 @@ export class CodeWorkflowAction implements WorkflowExecutor {
) {} ) {}
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowCodeAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(

View File

@ -13,10 +13,17 @@ import { isWorkflowFormAction } from 'src/modules/workflow/workflow-executor/wor
@Injectable() @Injectable()
export class FormWorkflowAction implements WorkflowExecutor { export class FormWorkflowAction implements WorkflowExecutor {
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowFormAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(

View File

@ -75,11 +75,18 @@ export class SendEmailWorkflowAction implements WorkflowExecutor {
} }
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowSendEmailAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(

View File

@ -42,12 +42,18 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor {
) {} ) {}
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowCreateRecordAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(
'Step is not a create record action', 'Step is not a create record action',

View File

@ -35,12 +35,18 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor {
) {} ) {}
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowDeleteRecordAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(
'Step is not a delete record action', 'Step is not a delete record action',

View File

@ -45,11 +45,18 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor {
) {} ) {}
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowFindRecordsAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(

View File

@ -42,11 +42,18 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor {
) {} ) {}
async execute({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: 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)) { if (!isWorkflowUpdateRecordAction(step)) {
throw new WorkflowStepExecutorException( throw new WorkflowStepExecutorException(

View File

@ -107,6 +107,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false }, retryOnFailure: { value: false },
}, },
}, },
nextStepIds: ['step-2'],
}, },
{ {
id: 'step-2', id: 'step-2',
@ -117,6 +118,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false }, retryOnFailure: { value: false },
}, },
}, },
nextStepIds: [],
}, },
] as WorkflowAction[]; ] as WorkflowAction[];
@ -124,7 +126,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
// No steps to execute // No steps to execute
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 2, currentStepId: 'step-2',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
}); });
@ -145,7 +147,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
}); });
@ -156,7 +158,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
); );
expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({ expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
attemptCount: 1, attemptCount: 1,
@ -199,7 +201,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
}); });
@ -231,7 +233,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
}); });
@ -265,6 +267,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
retryOnFailure: { value: false }, retryOnFailure: { value: false },
}, },
}, },
nextStepIds: ['step-2'],
}, },
{ {
id: 'step-2', id: 'step-2',
@ -284,7 +287,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: stepsWithContinueOnFailure, steps: stepsWithContinueOnFailure,
context: mockContext, context: mockContext,
}); });
@ -330,7 +333,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
await service.execute({ await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: stepsWithRetryOnFailure, steps: stepsWithRetryOnFailure,
context: mockContext, context: mockContext,
}); });
@ -367,7 +370,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: stepsWithRetryOnFailure, steps: stepsWithRetryOnFailure,
context: mockContext, context: mockContext,
attemptCount: 3, // MAX_RETRIES_ON_FAILURE is 3 attemptCount: 3, // MAX_RETRIES_ON_FAILURE is 3
@ -394,7 +397,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
const result = await service.execute({ const result = await service.execute({
workflowRunId: mockWorkflowRunId, workflowRunId: mockWorkflowRunId,
currentStepIndex: 0, currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: mockContext,
}); });

View File

@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface'; 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'; 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({ async execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
attemptCount = 1, attemptCount = 1,
workflowRunId, workflowRunId,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> { }: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
if (currentStepIndex >= steps.length) { const step = steps.find((step) => step.id === currentStepId);
if (!step) {
return { return {
result: { error: 'Step not found',
success: true,
},
}; };
} }
const step = steps[currentStepIndex];
const workflowExecutor = this.workflowExecutorFactory.get(step.type); const workflowExecutor = this.workflowExecutorFactory.get(step.type);
let actionOutput: WorkflowExecutorOutput; let actionOutput: WorkflowExecutorOutput;
@ -80,7 +80,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
try { try {
actionOutput = await workflowExecutor.execute({ actionOutput = await workflowExecutor.execute({
currentStepIndex, currentStepId,
steps, steps,
context, context,
attemptCount, attemptCount,
@ -111,11 +111,17 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
return actionOutput; return actionOutput;
} }
if (actionOutput.result) { const shouldContinue =
const updatedContext = { isDefined(actionOutput.result) ||
...context, step.settings.errorHandlingOptions.continueOnFailure.value;
[step.id]: actionOutput.result,
}; if (shouldContinue) {
const updatedContext = isDefined(actionOutput.result)
? {
...context,
[step.id]: actionOutput.result,
}
: context;
await this.workflowRunWorkspaceService.saveWorkflowRunState({ await this.workflowRunWorkspaceService.saveWorkflowRunState({
workflowRunId, workflowRunId,
@ -123,36 +129,26 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor {
context: updatedContext, context: updatedContext,
}); });
if (!isDefined(step.nextStepIds?.[0])) {
return actionOutput;
}
// TODO: handle multiple next steps
return await this.execute({ return await this.execute({
workflowRunId, workflowRunId,
currentStepIndex: currentStepIndex + 1, currentStepId: step.nextStepIds[0],
steps, steps,
context: updatedContext, 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 ( if (
step.settings.errorHandlingOptions.retryOnFailure.value && step.settings.errorHandlingOptions.retryOnFailure.value &&
attemptCount < MAX_RETRIES_ON_FAILURE attemptCount < MAX_RETRIES_ON_FAILURE
) { ) {
return await this.execute({ return await this.execute({
workflowRunId, workflowRunId,
currentStepIndex, currentStepId,
steps, steps,
context, context,
attemptCount: attemptCount + 1, attemptCount: attemptCount + 1,

View File

@ -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 { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.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 { 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 { 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'; 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 workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly throttlerService: ThrottlerService, private readonly throttlerService: ThrottlerService,
private readonly twentyConfigService: TwentyConfigService, private readonly twentyConfigService: TwentyConfigService,
private readonly twentyORMManager: TwentyORMManager,
) {} ) {}
@Process(RunWorkflowJob.name) @Process(RunWorkflowJob.name)
@ -109,7 +107,7 @@ export class RunWorkflowJob {
await this.executeWorkflow({ await this.executeWorkflow({
workflowRunId, workflowRunId,
currentStepIndex: 0, currentStepId: workflowVersion.steps[0].id,
steps: workflowVersion.steps, steps: workflowVersion.steps,
context, 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, (step) => step.id === lastExecutedStepId,
); );
if (lastExecutedStepIndex === undefined) { if (!lastExecutedStep) {
throw new WorkflowRunException( throw new WorkflowRunException(
'Last executed step not found', 'Last executed step not found',
WorkflowRunExceptionCode.INVALID_INPUT, WorkflowRunExceptionCode.INVALID_INPUT,
); );
} }
const nextStepId = lastExecutedStep.nextStepIds?.[0];
if (!nextStepId) {
await this.workflowRunWorkspaceService.endWorkflowRun({
workflowRunId,
status: WorkflowRunStatus.COMPLETED,
});
return;
}
await this.executeWorkflow({ await this.executeWorkflow({
workflowRunId, workflowRunId,
currentStepIndex: lastExecutedStepIndex + 1, currentStepId: nextStepId,
steps: workflowRun.output?.flow?.steps ?? [], steps: workflowRun.output?.flow?.steps ?? [],
context: workflowRun.context ?? {}, context: workflowRun.context ?? {},
}); });
@ -155,19 +164,19 @@ export class RunWorkflowJob {
private async executeWorkflow({ private async executeWorkflow({
workflowRunId, workflowRunId,
currentStepIndex, currentStepId,
steps, steps,
context, context,
}: { }: {
workflowRunId: string; workflowRunId: string;
currentStepIndex: number; currentStepId: string;
steps: WorkflowAction[]; steps: WorkflowAction[];
context: Record<string, any>; context: Record<string, any>;
}) { }) {
const { error, pendingEvent } = const { error, pendingEvent } =
await this.workflowExecutorWorkspaceService.execute({ await this.workflowExecutorWorkspaceService.execute({
workflowRunId, workflowRunId,
currentStepIndex, currentStepId,
steps, steps,
context, context,
}); });