diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 931dad172f..ec8bc488b3 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -385,6 +385,7 @@ enum EngineComponentKey { SEE_VERSION_WORKFLOW_RUN SEE_WORKFLOW_WORKFLOW_RUN STOP_WORKFLOW_RUN + RETRY_WORKFLOW_RUN SEE_RUNS_WORKFLOW_VERSION SEE_WORKFLOW_WORKFLOW_VERSION USE_AS_DRAFT_WORKFLOW_VERSION diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 7e459254e3..2d3c55affe 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -304,7 +304,7 @@ export interface CommandMenuItem { __typename: 'CommandMenuItem' } -export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS' +export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'RETRY_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS' export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK' @@ -8456,6 +8456,7 @@ export const enumEngineComponentKey = { SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const, SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const, STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const, + RETRY_WORKFLOW_RUN: 'RETRY_WORKFLOW_RUN' as const, SEE_RUNS_WORKFLOW_VERSION: 'SEE_RUNS_WORKFLOW_VERSION' as const, SEE_WORKFLOW_WORKFLOW_VERSION: 'SEE_WORKFLOW_WORKFLOW_VERSION' as const, USE_AS_DRAFT_WORKFLOW_VERSION: 'USE_AS_DRAFT_WORKFLOW_VERSION' as const, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f1ad3a1922..f6f1a5e392 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1533,6 +1533,7 @@ export enum EngineComponentKey { RESTORE_MULTIPLE_RECORDS = 'RESTORE_MULTIPLE_RECORDS', RESTORE_RECORDS = 'RESTORE_RECORDS', RESTORE_SINGLE_RECORD = 'RESTORE_SINGLE_RECORD', + RETRY_WORKFLOW_RUN = 'RETRY_WORKFLOW_RUN', SAVE_DASHBOARD_LAYOUT = 'SAVE_DASHBOARD_LAYOUT', SEARCH_RECORDS = 'SEARCH_RECORDS', SEARCH_RECORDS_FALLBACK = 'SEARCH_RECORDS_FALLBACK', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 2ab5ab9cb0..111e7a1751 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -142,6 +142,7 @@ export type Mutation = { dismissReconnectAccountBanner: Scalars['Boolean']['output']; duplicateWorkflow: WorkflowVersionDto; duplicateWorkflowVersionStep: WorkflowVersionStepChanges; + retryWorkflowRun: WorkflowRun; runWorkflowVersion: RunWorkflowVersion; stopWorkflowRun: WorkflowRun; submitFormStep: Scalars['Boolean']['output']; @@ -207,6 +208,11 @@ export type MutationDuplicateWorkflowVersionStepArgs = { }; +export type MutationRetryWorkflowRunArgs = { + workflowRunId: Scalars['UUID']['input']; +}; + + export type MutationRunWorkflowVersionArgs = { input: RunWorkflowVersionInput; }; @@ -732,6 +738,13 @@ export type DuplicateWorkflowVersionStepMutationVariables = Exact<{ export type DuplicateWorkflowVersionStepMutation = { __typename?: 'Mutation', duplicateWorkflowVersionStep: { __typename?: 'WorkflowVersionStepChanges', triggerDiff?: any | null, stepsDiff?: any | null } }; +export type RetryWorkflowRunMutationVariables = Exact<{ + workflowRunId: Scalars['UUID']['input']; +}>; + + +export type RetryWorkflowRunMutation = { __typename?: 'Mutation', retryWorkflowRun: { __typename: 'WorkflowRun', id: any, status: WorkflowRunStatusEnum } }; + export type RunWorkflowVersionMutationVariables = Exact<{ input: RunWorkflowVersionInput; }>; @@ -812,6 +825,7 @@ export const DeleteWorkflowVersionEdgeDocument = {"kind":"Document","definitions export const DeleteWorkflowVersionStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkflowVersionStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteWorkflowVersionStepInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteWorkflowVersionStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkflowDiffFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkflowDiffFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowVersionStepChanges"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerDiff"}},{"kind":"Field","name":{"kind":"Name","value":"stepsDiff"}}]}}]} as unknown as DocumentNode; export const DuplicateWorkflowDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DuplicateWorkflow"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DuplicateWorkflowInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duplicateWorkflow"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"trigger"}},{"kind":"Field","name":{"kind":"Name","value":"steps"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"workflowId"}}]}}]}}]} as unknown as DocumentNode; export const DuplicateWorkflowVersionStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DuplicateWorkflowVersionStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DuplicateWorkflowVersionStepInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duplicateWorkflowVersionStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkflowDiffFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkflowDiffFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowVersionStepChanges"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerDiff"}},{"kind":"Field","name":{"kind":"Name","value":"stepsDiff"}}]}}]} as unknown as DocumentNode; +export const RetryWorkflowRunDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryWorkflowRun"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workflowRunId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retryWorkflowRun"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workflowRunId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workflowRunId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]} as unknown as DocumentNode; export const RunWorkflowVersionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RunWorkflowVersion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RunWorkflowVersionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runWorkflowVersion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workflowRunId"}}]}}]}}]} as unknown as DocumentNode; export const StopWorkflowRunDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StopWorkflowRun"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workflowRunId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stopWorkflowRun"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workflowRunId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workflowRunId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]} as unknown as DocumentNode; export const UpdateWorkflowRunStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkflowRunStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkflowRunStepInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkflowRunStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"nextStepIds"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx index 75444480d8..bcc42b8032 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx @@ -28,6 +28,7 @@ import { ReplyToEmailThreadCommand } from '@/command-menu-item/engine-command/re import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand'; import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand'; import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand'; +import { RetryWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/RetryWorkflowRunSingleRecordCommand'; import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand'; import { SeeRunsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand'; import { SeeVersionsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand'; @@ -87,6 +88,9 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record< [EngineComponentKey.STOP_WORKFLOW_RUN]: ( ), + [EngineComponentKey.RETRY_WORKFLOW_RUN]: ( + + ), [EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: ( ), diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/RetryWorkflowRunSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/RetryWorkflowRunSingleRecordCommand.tsx new file mode 100644 index 0000000000..1e6486da2b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/RetryWorkflowRunSingleRecordCommand.tsx @@ -0,0 +1,11 @@ +import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; +import { useExecuteWorkflowRunBulkCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/hooks/useExecuteWorkflowRunBulkCommand'; +import { useRetryWorkflowRun } from '@/workflow/hooks/useRetryWorkflowRun'; + +export const RetryWorkflowRunSingleRecordCommand = () => { + const { retryWorkflowRun } = useRetryWorkflowRun(); + + const { execute } = useExecuteWorkflowRunBulkCommand(retryWorkflowRun); + + return ; +}; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx index 91a912d42a..bbbc137f18 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx @@ -1,36 +1,11 @@ import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; -import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi'; -import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; -import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; +import { useExecuteWorkflowRunBulkCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/hooks/useExecuteWorkflowRunBulkCommand'; import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; export const StopWorkflowRunSingleRecordCommand = () => { - const { targetedRecordsRule, graphqlFilter } = useHeadlessCommandContextApi(); - - const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({ - objectNameSingular: CoreObjectNameSingular.WorkflowRun, - filter: isDefined(graphqlFilter) ? graphqlFilter : undefined, - limit: DEFAULT_QUERY_PAGE_SIZE, - recordGqlFields: { id: true }, - }); - const { stopWorkflowRun } = useStopWorkflowRun(); - const handleExecute = async () => { - if (targetedRecordsRule.mode === 'selection') { - for (const selectedRecordId of targetedRecordsRule.selectedRecordIds) { - await stopWorkflowRun(selectedRecordId); - } - } else { - const records = await fetchAllRecordIds(); + const { execute } = useExecuteWorkflowRunBulkCommand(stopWorkflowRun); - for (const record of records) { - await stopWorkflowRun(record.id); - } - } - }; - - return ; + return ; }; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/hooks/useExecuteWorkflowRunBulkCommand.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/hooks/useExecuteWorkflowRunBulkCommand.ts new file mode 100644 index 0000000000..b6ea267581 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/hooks/useExecuteWorkflowRunBulkCommand.ts @@ -0,0 +1,34 @@ +import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi'; +import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; +import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const useExecuteWorkflowRunBulkCommand = ( + action: (workflowRunId: string) => Promise, +) => { + const { targetedRecordsRule, graphqlFilter } = useHeadlessCommandContextApi(); + + const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({ + objectNameSingular: CoreObjectNameSingular.WorkflowRun, + filter: isDefined(graphqlFilter) ? graphqlFilter : undefined, + limit: DEFAULT_QUERY_PAGE_SIZE, + recordGqlFields: { id: true }, + }); + + const execute = async () => { + if (targetedRecordsRule.mode === 'selection') { + for (const selectedRecordId of targetedRecordsRule.selectedRecordIds) { + await action(selectedRecordId); + } + } else { + const records = await fetchAllRecordIds(); + + for (const record of records) { + await action(record.id); + } + } + }; + + return { execute }; +}; diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/retryWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/retryWorkflowRun.ts new file mode 100644 index 0000000000..bb7987cf4a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/retryWorkflowRun.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const RETRY_WORKFLOW_RUN = gql` + mutation RetryWorkflowRun($workflowRunId: UUID!) { + retryWorkflowRun(workflowRunId: $workflowRunId) { + id + status + __typename + } + } +`; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRetryWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useRetryWorkflowRun.ts new file mode 100644 index 0000000000..fad788c780 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useRetryWorkflowRun.ts @@ -0,0 +1,40 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; +import { useMutation } from '@apollo/client/react'; +import { RetryWorkflowRunDocument } from '~/generated/graphql'; + +export const useRetryWorkflowRun = () => { + const apolloCoreClient = useApolloCoreClient(); + const [mutate] = useMutation(RetryWorkflowRunDocument, { + client: apolloCoreClient, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.WorkflowRun, + }); + + const retryWorkflowRun = async (workflowRunId: string) => { + await mutate({ + variables: { + workflowRunId, + }, + }); + + dispatchObjectRecordOperationBrowserEvent({ + objectMetadataItem, + operation: { + type: 'update-one', + result: { + updateInput: { + recordId: workflowRunId, + updatedFields: [], + }, + }, + }, + }); + }; + + return { retryWorkflowRun }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx index 8ba5fb0ad5..0fb876c4da 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx @@ -123,13 +123,38 @@ export const WorkflowRunVisualizerEffect = ({ steps: workflowRunState.flow.steps, }); - const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } = + const { diagram: generatedWorkflowRunDiagram, stepToOpenByDefault } = generateWorkflowRunDiagram({ trigger: workflowRunState.flow.trigger, steps: workflowRunState.flow.steps, stepInfos: workflowRunState.stepInfos, }); + const previousNodesById = new Map( + (store.get(workflowDiagram)?.nodes ?? []).map((node) => [ + node.id, + node, + ]), + ); + + const baseWorkflowRunDiagram = { + ...generatedWorkflowRunDiagram, + nodes: generatedWorkflowRunDiagram.nodes.map((node) => { + const previousNode = previousNodesById.get(node.id); + + if (!isDefined(previousNode?.measured)) { + return node; + } + + return { + ...node, + measured: previousNode.measured, + width: previousNode.width, + height: previousNode.height, + }; + }), + }; + if (workflowDiagramStatus !== 'done') { store.set(workflowDiagramStatusState, 'computing-dimensions'); } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index d285bd27ca..f45d143cda 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -121,4 +121,16 @@ export class WorkflowTriggerResolver { workspace.id, ); } + + @Mutation(() => WorkflowRunDTO) + async retryWorkflowRun( + @AuthWorkspace() workspace: WorkspaceEntity, + @Args('workflowRunId', { type: () => UUIDScalarType }) + workflowRunId: string, + ) { + return this.workflowTriggerWorkspaceService.retryWorkflowRun( + workflowRunId, + workspace.id, + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts index d1918f96d8..c9846e19ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts @@ -36,6 +36,7 @@ export enum EngineComponentKey { SEE_VERSION_WORKFLOW_RUN = 'SEE_VERSION_WORKFLOW_RUN', SEE_WORKFLOW_WORKFLOW_RUN = 'SEE_WORKFLOW_WORKFLOW_RUN', STOP_WORKFLOW_RUN = 'STOP_WORKFLOW_RUN', + RETRY_WORKFLOW_RUN = 'RETRY_WORKFLOW_RUN', SEE_RUNS_WORKFLOW_VERSION = 'SEE_RUNS_WORKFLOW_VERSION', SEE_WORKFLOW_WORKFLOW_VERSION = 'SEE_WORKFLOW_WORKFLOW_VERSION', USE_AS_DRAFT_WORKFLOW_VERSION = 'USE_AS_DRAFT_WORKFLOW_VERSION', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts index 88f291f49c..2b32b4b7d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts @@ -544,12 +544,28 @@ export const STANDARD_COMMAND_MENU_ITEMS = { engineComponentKey: EngineComponentKey.STOP_WORKFLOW_RUN, hotKeys: null, }, + retryWorkflowRun: { + universalIdentifier: '99b97c50-b31b-411a-a532-ec05402123c0', + label: 'Retry', + icon: 'IconRefresh', + isPinned: true, + position: 36, + shortLabel: 'Retry', + availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, + conditionalAvailabilityExpression: + 'someEquals(selectedRecords, "status", "FAILED")', + availabilityObjectMetadataUniversalIdentifier: + STANDARD_OBJECTS.workflowRun.universalIdentifier, + frontComponentUniversalIdentifier: null, + engineComponentKey: EngineComponentKey.RETRY_WORKFLOW_RUN, + hotKeys: null, + }, seeRunsWorkflowVersion: { universalIdentifier: '44e305c7-4f0a-45ec-803f-6471b56455cb', label: 'See Runs', icon: 'IconHistoryToggle', isPinned: true, - position: 36, + position: 37, shortLabel: 'See Runs', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -565,7 +581,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'See Workflow', icon: 'IconSettingsAutomation', isPinned: true, - position: 37, + position: 38, shortLabel: 'See Workflow', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -581,7 +597,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Use as Draft', icon: 'IconPencil', isPinned: true, - position: 38, + position: 39, shortLabel: 'Use as Draft', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -597,7 +613,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'See Versions History', icon: 'IconVersions', isPinned: false, - position: 39, + position: 40, shortLabel: 'See Versions', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -613,7 +629,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Search', icon: 'IconSearch', isPinned: false, - position: 40, + position: 41, shortLabel: 'Search', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: null, @@ -627,7 +643,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Search', icon: 'IconSearch', isPinned: false, - position: 41, + position: 42, shortLabel: 'Search', availabilityType: CommandMenuItemAvailabilityType.FALLBACK, conditionalAvailabilityExpression: null, @@ -641,7 +657,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Ask AI', icon: 'IconSparkles', isPinned: false, - position: 42, + position: 43, shortLabel: 'Ask AI', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.AI', @@ -655,7 +671,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'View Previous AI Chats', icon: 'IconHistory', isPinned: false, - position: 43, + position: 44, shortLabel: 'Previous AI Chats', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.AI', @@ -669,7 +685,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Reply', icon: 'IconArrowBackUp', isPinned: true, - position: 44, + position: 45, shortLabel: 'Reply', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: 'numberOfSelectedRecords == 1', @@ -684,7 +700,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Compose Email', icon: 'IconMail', isPinned: false, - position: 45, + position: 46, shortLabel: 'Compose', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.SEND_EMAIL_TOOL', @@ -698,7 +714,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Settings', icon: 'IconSettings', isPinned: false, - position: 46, + position: 47, shortLabel: 'Settings', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: null, @@ -713,7 +729,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Experience Settings', icon: 'IconColorSwatch', isPinned: false, - position: 47, + position: 48, shortLabel: 'Experience', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: null, @@ -728,7 +744,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Accounts Settings', icon: 'IconAt', isPinned: false, - position: 48, + position: 49, shortLabel: 'Accounts', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS', @@ -743,7 +759,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Emails Settings', icon: 'IconMail', isPinned: false, - position: 49, + position: 50, shortLabel: 'Emails', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS', @@ -758,7 +774,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Calendars Settings', icon: 'IconCalendarEvent', isPinned: false, - position: 50, + position: 51, shortLabel: 'Calendars', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.CONNECTED_ACCOUNTS', @@ -773,7 +789,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to General Settings', icon: 'IconSettings', isPinned: false, - position: 51, + position: 52, shortLabel: 'General', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE', @@ -788,7 +804,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Data Model Settings', icon: 'IconHierarchy2', isPinned: false, - position: 52, + position: 53, shortLabel: 'Data Model', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.DATA_MODEL', @@ -803,7 +819,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Members Settings', icon: 'IconUsers', isPinned: false, - position: 53, + position: 54, shortLabel: 'Members', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE_MEMBERS', @@ -818,7 +834,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Roles Settings', icon: 'IconLock', isPinned: false, - position: 54, + position: 55, shortLabel: 'Roles', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE_MEMBERS', @@ -833,7 +849,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Domains Settings', icon: 'IconWorld', isPinned: false, - position: 55, + position: 56, shortLabel: 'Domains', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE', @@ -848,7 +864,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Billing Settings', icon: 'IconCurrencyDollar', isPinned: false, - position: 56, + position: 57, shortLabel: 'Billing', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE', @@ -863,7 +879,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to APIs & Webhooks Settings', icon: 'IconApi', isPinned: false, - position: 57, + position: 58, shortLabel: 'APIs & Webhooks', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.API_KEYS_AND_WEBHOOKS', @@ -878,7 +894,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Apps Settings', icon: 'IconPlug', isPinned: false, - position: 58, + position: 59, shortLabel: 'Apps', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.APPLICATIONS', @@ -893,7 +909,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to AI Settings', icon: 'IconSparkles', isPinned: false, - position: 59, + position: 60, shortLabel: 'AI', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.WORKSPACE', @@ -908,7 +924,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Security Settings', icon: 'IconKey', isPinned: false, - position: 60, + position: 61, shortLabel: 'Security', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.SECURITY', @@ -923,7 +939,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Admin Panel Settings', icon: 'IconServer', isPinned: false, - position: 61, + position: 62, shortLabel: 'Admin Panel', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: @@ -939,7 +955,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Go to Community Settings', icon: 'IconUsers', isPinned: false, - position: 62, + position: 63, shortLabel: 'Community', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: null, @@ -954,7 +970,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Send Email', icon: 'IconMail', isPinned: true, - position: 63, + position: 64, shortLabel: 'Send Email', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -970,7 +986,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Send Email', icon: 'IconMail', isPinned: true, - position: 64, + position: 65, shortLabel: 'Send Email', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: @@ -986,7 +1002,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { label: 'Send Email', icon: 'IconMail', isPinned: true, - position: 65, + position: 66, shortLabel: 'Send Email', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-iterator-initial-loop-step-ids.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-iterator-initial-loop-step-ids.util.ts new file mode 100644 index 0000000000..6a9a3a4858 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-iterator-initial-loop-step-ids.util.ts @@ -0,0 +1,25 @@ +import { isString } from '@sniptt/guards'; + +import { type WorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +export const getIteratorInitialLoopStepIds = ( + step: WorkflowIteratorAction, +): string[] => { + const initialLoopStepIds = step.settings.input.initialLoopStepIds; + + if (isString(initialLoopStepIds)) { + try { + const parsed: unknown = JSON.parse(initialLoopStepIds); + + if (Array.isArray(parsed) && parsed.every(isString)) { + return parsed; + } + } catch { + return []; + } + + return []; + } + + return initialLoopStepIds ?? []; +}; 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 72287142f6..c97b53138a 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 @@ -39,6 +39,7 @@ export class RunWorkflowJob { async handle({ workflowRunId, lastExecutedStepId, + stepIdsToRetry, workspaceId, }: RunWorkflowJobData): Promise { this.logger.log( @@ -48,7 +49,13 @@ export class RunWorkflowJob { await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { try { - if (lastExecutedStepId) { + if (isDefined(stepIdsToRetry)) { + await this.retryWorkflowExecution({ + workspaceId, + workflowRunId, + stepIdsToRetry, + }); + } else if (lastExecutedStepId) { await this.resumeWorkflowExecution({ workspaceId, workflowRunId, @@ -131,6 +138,32 @@ export class RunWorkflowJob { }); } + private async retryWorkflowExecution({ + workflowRunId, + stepIdsToRetry, + workspaceId, + }: { + workflowRunId: string; + stepIdsToRetry: string[]; + workspaceId: string; + }): Promise { + const workflowRun = + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }); + + if (workflowRun.status !== WorkflowRunStatus.RUNNING) { + return; + } + + await this.workflowExecutorWorkspaceService.executeFromSteps({ + stepIds: stepIdsToRetry, + workflowRunId, + workspaceId, + }); + } + private async resumeWorkflowExecution({ workflowRunId, lastExecutedStepId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/types/run-workflow-job-data.type.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/types/run-workflow-job-data.type.ts index 38884f55e5..00df174bb3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/types/run-workflow-job-data.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/types/run-workflow-job-data.type.ts @@ -2,4 +2,5 @@ export type RunWorkflowJobData = { workspaceId: string; workflowRunId: string; lastExecutedStepId?: string; + stepIdsToRetry?: string[]; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-iterator-step-infos.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-iterator-step-infos.util.spec.ts new file mode 100644 index 0000000000..fc3970827d --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-iterator-step-infos.util.spec.ts @@ -0,0 +1,75 @@ +import { StepStatus, type WorkflowRunStepInfo } from 'twenty-shared/workflow'; + +import { + createMockCodeStep, + createMockIteratorStep, +} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util'; +import { + type WorkflowAction, + type WorkflowIteratorAction, +} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { buildRetryIteratorStepInfos } from 'src/modules/workflow/workflow-runner/utils/build-retry-iterator-step-infos.util'; + +describe('buildRetryIteratorStepInfos', () => { + const iteratorStep = createMockIteratorStep( + 'iterator', + ['post'], + ['body'], + ) as WorkflowIteratorAction; + + const steps: WorkflowAction[] = [ + iteratorStep, + createMockCodeStep('body', ['iterator']), + createMockCodeStep('post'), + ]; + + it('restores an iterator that failed mid-loop to RUNNING with its cursor preserved', () => { + const iteratorStepInfo: WorkflowRunStepInfo = { + status: StepStatus.FAILED, + result: { + currentItemIndex: 2, + currentItem: 'item-2', + hasProcessedAllItems: false, + }, + error: 'ended before completion', + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryIteratorStepInfos({ + iteratorStep, + iteratorStepInfo, + steps, + }); + + expect(stepIdsToRetry).toEqual([]); + expect(stepInfosToUpdate).toEqual({ + iterator: { + status: StepStatus.RUNNING, + result: { + currentItemIndex: 2, + currentItem: 'item-2', + hasProcessedAllItems: false, + }, + error: undefined, + }, + }); + }); + + it('restarts the whole loop when the iterator itself failed', () => { + const iteratorStepInfo: WorkflowRunStepInfo = { + status: StepStatus.FAILED, + error: 'invalid items input', + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryIteratorStepInfos({ + iteratorStep, + iteratorStepInfo, + steps, + }); + + expect(stepIdsToRetry).toEqual(['iterator']); + expect(stepInfosToUpdate).toEqual({ + body: { status: StepStatus.NOT_STARTED }, + iterator: { status: StepStatus.NOT_STARTED }, + }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-step-infos.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-step-infos.util.spec.ts new file mode 100644 index 0000000000..d52fcd881c --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/build-retry-step-infos.util.spec.ts @@ -0,0 +1,118 @@ +import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow'; + +import { + createMockCodeStep, + createMockIteratorStep, +} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util'; +import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { buildRetryStepInfos } from 'src/modules/workflow/workflow-runner/utils/build-retry-step-infos.util'; + +describe('buildRetryStepInfos', () => { + it('resets a plain failed step to NOT_STARTED and marks it for retry', () => { + const steps: WorkflowAction[] = [ + createMockCodeStep('step-1', ['step-2']), + createMockCodeStep('step-2'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + 'step-1': { status: StepStatus.SUCCESS }, + 'step-2': { status: StepStatus.FAILED, error: 'boom' }, + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryStepInfos({ + steps, + stepInfos, + }); + + expect(stepIdsToRetry).toEqual(['step-2']); + expect(stepInfosToUpdate).toEqual({ + 'step-2': { status: StepStatus.NOT_STARTED }, + }); + }); + + it('leaves non-failed steps untouched', () => { + const steps: WorkflowAction[] = [ + createMockCodeStep('success'), + createMockCodeStep('failed-safely'), + createMockCodeStep('skipped'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + success: { status: StepStatus.SUCCESS }, + 'failed-safely': { status: StepStatus.FAILED_SAFELY }, + skipped: { status: StepStatus.SKIPPED }, + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryStepInfos({ + steps, + stepInfos, + }); + + expect(stepIdsToRetry).toEqual([]); + expect(stepInfosToUpdate).toEqual({}); + }); + + it('restores an iterator that failed mid-loop to RUNNING and resets the failed loop body step', () => { + const steps: WorkflowAction[] = [ + createMockIteratorStep('iterator', ['post'], ['body']), + createMockCodeStep('body', ['iterator']), + createMockCodeStep('post'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + iterator: { + status: StepStatus.FAILED, + result: { + currentItemIndex: 2, + currentItem: 'item-2', + hasProcessedAllItems: false, + }, + error: 'ended before completion', + }, + body: { status: StepStatus.FAILED, error: 'boom' }, + post: { status: StepStatus.NOT_STARTED }, + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryStepInfos({ + steps, + stepInfos, + }); + + expect(stepIdsToRetry).toEqual(['body']); + expect(stepInfosToUpdate.iterator).toEqual({ + status: StepStatus.RUNNING, + result: { + currentItemIndex: 2, + currentItem: 'item-2', + hasProcessedAllItems: false, + }, + error: undefined, + }); + expect(stepInfosToUpdate.body).toEqual({ status: StepStatus.NOT_STARTED }); + }); + + it('resets the whole loop when the iterator itself failed', () => { + const steps: WorkflowAction[] = [ + createMockIteratorStep('iterator', ['post'], ['body']), + createMockCodeStep('body', ['iterator']), + createMockCodeStep('post'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + iterator: { status: StepStatus.FAILED, error: 'invalid items input' }, + body: { status: StepStatus.NOT_STARTED }, + post: { status: StepStatus.NOT_STARTED }, + }; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryStepInfos({ + steps, + stepInfos, + }); + + expect(stepIdsToRetry).toEqual(['iterator']); + expect(stepInfosToUpdate.iterator).toEqual({ + status: StepStatus.NOT_STARTED, + }); + expect(stepInfosToUpdate.body).toEqual({ status: StepStatus.NOT_STARTED }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/get-runnable-step-ids.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/get-runnable-step-ids.util.spec.ts new file mode 100644 index 0000000000..183bd70bc9 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/__tests__/get-runnable-step-ids.util.spec.ts @@ -0,0 +1,81 @@ +import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow'; + +import { + createMockCodeStep, + createMockIteratorStep, +} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util'; +import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { getRunnableStepIds } from 'src/modules/workflow/workflow-runner/utils/get-runnable-step-ids.util'; + +describe('getRunnableStepIds', () => { + it('returns NOT_STARTED steps whose parents are satisfied', () => { + const steps: WorkflowAction[] = [ + createMockCodeStep('step-1', ['step-2']), + createMockCodeStep('step-2', ['step-3']), + createMockCodeStep('step-3'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + 'step-1': { status: StepStatus.SUCCESS }, + 'step-2': { status: StepStatus.NOT_STARTED }, + 'step-3': { status: StepStatus.NOT_STARTED }, + }; + + expect(getRunnableStepIds({ steps, stepInfos })).toEqual(['step-2']); + }); + + it('includes entry steps with no parents', () => { + const steps: WorkflowAction[] = [createMockCodeStep('entry')]; + + const stepInfos: WorkflowRunStepInfos = { + entry: { status: StepStatus.NOT_STARTED }, + }; + + expect(getRunnableStepIds({ steps, stepInfos })).toEqual(['entry']); + }); + + it('excludes steps that have already started', () => { + const steps: WorkflowAction[] = [createMockCodeStep('done')]; + + const stepInfos: WorkflowRunStepInfos = { + done: { status: StepStatus.SUCCESS }, + }; + + expect(getRunnableStepIds({ steps, stepInfos })).toEqual([]); + }); + + it('excludes loop-interior steps and keeps the iterator itself', () => { + const steps: WorkflowAction[] = [ + createMockIteratorStep('iterator', ['post'], ['body']), + createMockCodeStep('body', ['iterator']), + createMockCodeStep('post'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + iterator: { status: StepStatus.NOT_STARTED }, + body: { status: StepStatus.NOT_STARTED }, + post: { status: StepStatus.NOT_STARTED }, + }; + + expect(getRunnableStepIds({ steps, stepInfos })).toEqual(['iterator']); + }); + + it('includes a parallel branch that never started while a sibling failed and was reset', () => { + const steps: WorkflowAction[] = [ + createMockCodeStep('root', ['branch-a', 'branch-b']), + createMockCodeStep('branch-a'), + createMockCodeStep('branch-b'), + ]; + + const stepInfos: WorkflowRunStepInfos = { + root: { status: StepStatus.SUCCESS }, + 'branch-a': { status: StepStatus.NOT_STARTED }, + 'branch-b': { status: StepStatus.NOT_STARTED }, + }; + + expect(getRunnableStepIds({ steps, stepInfos })).toEqual([ + 'branch-a', + 'branch-b', + ]); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-iterator-step-infos.util.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-iterator-step-infos.util.ts new file mode 100644 index 0000000000..6046902245 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-iterator-step-infos.util.ts @@ -0,0 +1,64 @@ +import { isDefined } from 'twenty-shared/utils'; +import { StepStatus, type WorkflowRunStepInfo } from 'twenty-shared/workflow'; + +import { type WorkflowIteratorResult } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-result.type'; +import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util'; +import { getIteratorInitialLoopStepIds } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-iterator-initial-loop-step-ids.util'; +import { + type WorkflowAction, + type WorkflowIteratorAction, +} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +type BuildRetryIteratorStepInfosResult = { + stepInfosToUpdate: Record; + stepIdsToRetry: string[]; +}; + +export const buildRetryIteratorStepInfos = ({ + iteratorStep, + iteratorStepInfo, + steps, +}: { + iteratorStep: WorkflowIteratorAction; + iteratorStepInfo: WorkflowRunStepInfo; + steps: WorkflowAction[]; +}): BuildRetryIteratorStepInfosResult => { + const iteratorResult = iteratorStepInfo.result as + | WorkflowIteratorResult + | undefined; + + const failedMidLoop = + isDefined(iteratorResult) && iteratorResult.hasProcessedAllItems === false; + + if (failedMidLoop) { + return { + stepInfosToUpdate: { + [iteratorStep.id]: { + ...iteratorStepInfo, + status: StepStatus.RUNNING, + error: undefined, + }, + }, + stepIdsToRetry: [], + }; + } + + const loopStepIds = getAllStepIdsInLoop({ + iteratorStepId: iteratorStep.id, + initialLoopStepIds: getIteratorInitialLoopStepIds(iteratorStep), + steps, + }); + + const stepInfosToUpdate: Record = {}; + + for (const loopStepId of loopStepIds) { + stepInfosToUpdate[loopStepId] = { status: StepStatus.NOT_STARTED }; + } + + stepInfosToUpdate[iteratorStep.id] = { status: StepStatus.NOT_STARTED }; + + return { + stepInfosToUpdate, + stepIdsToRetry: [iteratorStep.id], + }; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-step-infos.util.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-step-infos.util.ts new file mode 100644 index 0000000000..9dc40777af --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/build-retry-step-infos.util.ts @@ -0,0 +1,51 @@ +import { + StepStatus, + type WorkflowRunStepInfo, + type WorkflowRunStepInfos, +} from 'twenty-shared/workflow'; + +import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard'; +import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { buildRetryIteratorStepInfos } from 'src/modules/workflow/workflow-runner/utils/build-retry-iterator-step-infos.util'; + +type BuildRetryStepInfosResult = { + stepInfosToUpdate: Record; + stepIdsToRetry: string[]; +}; + +export const buildRetryStepInfos = ({ + steps, + stepInfos, +}: { + steps: WorkflowAction[]; + stepInfos: WorkflowRunStepInfos; +}): BuildRetryStepInfosResult => { + const stepInfosToUpdate: Record = {}; + const stepIdsToRetry: string[] = []; + + for (const step of steps) { + const stepInfo = stepInfos[step.id]; + + if (stepInfo?.status !== StepStatus.FAILED) { + continue; + } + + if (isWorkflowIteratorAction(step)) { + const iteratorRetry = buildRetryIteratorStepInfos({ + iteratorStep: step, + iteratorStepInfo: stepInfo, + steps, + }); + + Object.assign(stepInfosToUpdate, iteratorRetry.stepInfosToUpdate); + stepIdsToRetry.push(...iteratorRetry.stepIdsToRetry); + + continue; + } + + stepInfosToUpdate[step.id] = { status: StepStatus.NOT_STARTED }; + stepIdsToRetry.push(step.id); + } + + return { stepInfosToUpdate, stepIdsToRetry }; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/utils/get-runnable-step-ids.util.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/get-runnable-step-ids.util.ts new file mode 100644 index 0000000000..c6e9b4e9b6 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/utils/get-runnable-step-ids.util.ts @@ -0,0 +1,49 @@ +import { type WorkflowRunStepInfos } from 'twenty-shared/workflow'; + +import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util'; +import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard'; +import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util'; +import { getIteratorInitialLoopStepIds } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-iterator-initial-loop-step-ids.util'; +import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +export const getRunnableStepIds = ({ + steps, + stepInfos, +}: { + steps: WorkflowAction[]; + stepInfos: WorkflowRunStepInfos; +}): string[] => { + const loopInteriorStepIds = new Set(); + + for (const step of steps) { + if (!isWorkflowIteratorAction(step)) { + continue; + } + + const loopStepIds = getAllStepIdsInLoop({ + iteratorStepId: step.id, + initialLoopStepIds: getIteratorInitialLoopStepIds(step), + steps, + }); + + for (const loopStepId of loopStepIds) { + loopInteriorStepIds.add(loopStepId); + } + } + + return steps + .filter((step) => { + if (loopInteriorStepIds.has(step.id)) { + return false; + } + + return shouldExecuteStep({ + step, + steps, + stepInfos, + workflowRunStatus: WorkflowRunStatus.RUNNING, + }); + }) + .map((step) => step.id); +}; 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 270de7307e..962322c2ed 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 @@ -27,6 +27,8 @@ import { } from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception'; import { RunWorkflowJob } from 'src/modules/workflow/workflow-runner/jobs/run-workflow.job'; import { type RunWorkflowJobData } from 'src/modules/workflow/workflow-runner/types/run-workflow-job-data.type'; +import { buildRetryStepInfos } from 'src/modules/workflow/workflow-runner/utils/build-retry-step-infos.util'; +import { getRunnableStepIds } from 'src/modules/workflow/workflow-runner/utils/get-runnable-step-ids.util'; import { WorkflowRunEnqueueJob, type WorkflowRunEnqueueJobData, @@ -209,50 +211,66 @@ export class WorkflowRunnerWorkspaceService { ]; if (!stoppableStatuses.includes(workflowRun.status)) { - throw new WorkflowRunException( - 'Workflow run cannot be stopped', - WorkflowRunExceptionCode.INVALID_OPERATION, - { - userFriendlyMessage: msg`Workflow run cannot be stopped in its current status`, - }, - ); + return { + id: workflowRun.id, + status: workflowRun.status, + }; } + const wasNotStarted = workflowRun.status === WorkflowRunStatus.NOT_STARTED; + let newStatus: WorkflowRunStatus; - const stepInfos = workflowRun.state.stepInfos; - const steps = workflowRun.state.flow.steps; - - if (workflowHasRunningSteps({ stepInfos, steps })) { - const stoppedIteratorStepInfos = setAllIteratorsStepInfosAsStopped({ - stepInfos, - steps, - }); - - const mergedStepInfos = { - ...stepInfos, - ...stoppedIteratorStepInfos, - }; - - await this.workflowRunWorkspaceService.updateWorkflowRun({ - workflowRunId, - workspaceId, - partialUpdate: { - status: WorkflowRunStatus.STOPPING, - state: { - ...workflowRun.state, - stepInfos: mergedStepInfos, - }, - }, - }); - newStatus = WorkflowRunStatus.STOPPING; - } else { + if (!isDefined(workflowRun.state)) { await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, workspaceId, status: WorkflowRunStatus.STOPPED, }); newStatus = WorkflowRunStatus.STOPPED; + } else { + const stepInfos = workflowRun.state.stepInfos; + const steps = workflowRun.state.flow.steps; + + if (workflowHasRunningSteps({ stepInfos, steps })) { + const stoppedIteratorStepInfos = setAllIteratorsStepInfosAsStopped({ + stepInfos, + steps, + }); + + const mergedStepInfos = { + ...stepInfos, + ...stoppedIteratorStepInfos, + }; + + await this.workflowRunWorkspaceService.updateWorkflowRun({ + workflowRunId, + workspaceId, + partialUpdate: { + status: WorkflowRunStatus.STOPPING, + state: { + ...workflowRun.state, + stepInfos: mergedStepInfos, + }, + }, + }); + newStatus = WorkflowRunStatus.STOPPING; + } else { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workflowRunId, + workspaceId, + status: WorkflowRunStatus.STOPPED, + }); + newStatus = WorkflowRunStatus.STOPPED; + } + } + + // Release the cached not-started slot only after the stop has been + // persisted, so a persistence failure can't desync the throttle counter. + if (wasNotStarted) { + await this.workflowThrottlingWorkspaceService.decreaseWorkflowRunNotStartedCount( + workspaceId, + ); } return { @@ -261,6 +279,100 @@ export class WorkflowRunnerWorkspaceService { }; } + async retryWorkflowRun(workspaceId: string, workflowRunId: string) { + const workflowRun = + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }); + + if (workflowRun.status !== WorkflowRunStatus.FAILED) { + return { + id: workflowRun.id, + status: workflowRun.status, + }; + } + + if (!isDefined(workflowRun.state)) { + throw new WorkflowRunException( + 'Cannot retry a workflow run without state', + WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID, + ); + } + + const steps = workflowRun.state.flow.steps; + + const { stepInfosToUpdate, stepIdsToRetry } = buildRetryStepInfos({ + steps, + stepInfos: workflowRun.state.stepInfos, + }); + + const mergedStepInfos = { + ...workflowRun.state.stepInfos, + ...stepInfosToUpdate, + }; + + const runnableStepIds = getRunnableStepIds({ + steps, + stepInfos: mergedStepInfos, + }); + + const stepIdsToRun = Array.from( + new Set([...stepIdsToRetry, ...runnableStepIds]), + ); + + if (stepIdsToRun.length === 0) { + return { + id: workflowRun.id, + status: workflowRun.status, + }; + } + + await this.workflowRunWorkspaceService.updateWorkflowRun({ + workflowRunId, + workspaceId, + partialUpdate: { + status: WorkflowRunStatus.RUNNING, + endedAt: null, + state: { + ...workflowRun.state, + stepInfos: mergedStepInfos, + workflowRunError: undefined, + }, + }, + }); + + try { + await this.messageQueueService.add( + RunWorkflowJob.name, + { + workspaceId, + workflowRunId, + stepIdsToRetry: stepIdsToRun, + }, + ); + } catch (error) { + // The job couldn't be enqueued: revert to the previous failed state so + // the run isn't left stuck as RUNNING without a worker job. + await this.workflowRunWorkspaceService.updateWorkflowRun({ + workflowRunId, + workspaceId, + partialUpdate: { + status: WorkflowRunStatus.FAILED, + endedAt: workflowRun.endedAt, + state: workflowRun.state, + }, + }); + + throw error; + } + + return { + id: workflowRun.id, + status: WorkflowRunStatus.RUNNING, + }; + } + private async checkHardThrottleLimit(workspaceId: string): Promise { try { await this.workflowThrottlingWorkspaceService.throttleOrThrowIfHardLimitReached( diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 6203079f1d..8b5550cf16 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -189,6 +189,13 @@ export class WorkflowTriggerWorkspaceService { ); } + async retryWorkflowRun(workflowRunId: string, workspaceId: string) { + return this.workflowRunnerWorkspaceService.retryWorkflowRun( + workspaceId, + workflowRunId, + ); + } + private async performActivationSteps( workflow: WorkflowWorkspaceEntity, workflowVersion: WorkflowVersionWorkspaceEntity,