From 4e5783eaf4766580ce7d5e08ef7f99c8cc05e137 Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:45:55 +0530 Subject: [PATCH] feat: workflow delay action (Pause - Wait/Sleep/Delay) (#14915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - This PR focuses on issue https://github.com/orgs/twentyhq/projects/1/views/33?pane=issue&itemId=93150683&issue=twentyhq%7Ccore-team-issues%7C20 - added Workflow delay as a Flow action - for V1 added Type 1: Resume at a specific date or time ## Visual Appearance Screenshot 2025-10-09 at 5 46
18 PM Screenshot 2025-10-09 at 5 46
35 PM --------- Co-authored-by: Thomas Trompette --- .../src/modules/workflow/types/Workflow.ts | 3 + .../WorkflowDiagramStepNodeIcon.tsx | 7 +- .../components/WorkflowRunStepNodeDetail.tsx | 12 + .../components/WorkflowStepDetail.tsx | 10 + .../workflow-actions/constants/FlowActions.ts | 7 +- .../components/WorkflowEditActionDelay.tsx | 225 ++++++++++++++++++ .../getActionIconColorOrThrow.test.ts | 4 +- .../utils/getActionHeaderTypeOrThrow.ts | 1 + .../workflow-actions/utils/getActionIcon.ts | 7 +- .../utils/getActionIconColorOrThrow.ts | 5 +- .../message-queue/drivers/bullmq.driver.ts | 4 +- .../interfaces/job-options.interface.ts | 1 + .../message-queue-priority.constant.ts | 19 ++ .../message-queue/message-queue.constants.ts | 1 + ...rsion-step-operations.workspace-service.ts | 21 ++ .../factories/workflow-action.factory.ts | 4 + .../resume-delayed-workflow-job-name.ts | 1 + .../delay/delay-action.module.ts | 13 + .../delay/delay.workflow-action.ts | 116 +++++++++ .../guards/is-workflow-delay-action.guard.ts | 11 + .../delay/jobs/resume-delayed-workflow.job.ts | 108 +++++++++ .../resume-delayed-workflow-job-data.type.ts | 5 + .../types/workflow-delay-action-input.type.ts | 18 ++ .../workflow-delay-action-settings.type.ts | 6 + .../types/workflow-action-settings.type.ts | 2 + .../types/workflow-action.type.ts | 10 +- .../workflow-executor.module.ts | 2 + packages/twenty-shared/src/workflow/index.ts | 2 + .../schemas/workflow-action-schema.ts | 2 + .../schemas/workflow-delay-action-schema.ts | 8 + .../workflow-delay-action-settings-schema.ts | 18 ++ .../display/icon/components/TablerIcons.ts | 1 + packages/twenty-ui/src/display/index.ts | 1 + 33 files changed, 643 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx create mode 100644 packages/twenty-server/src/engine/core-modules/message-queue/message-queue-priority.constant.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/contants/resume-delayed-workflow-job-name.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/delay-action.module.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/delay.workflow-action.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/guards/is-workflow-delay-action.guard.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/jobs/resume-delayed-workflow.job.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/types/resume-delayed-workflow-job-data.type.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/types/workflow-delay-action-input.type.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/types/workflow-delay-action-settings.type.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-delay-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-delay-action-settings-schema.ts diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 1d615254e31..4f82c724c60 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -23,6 +23,7 @@ import { type workflowTriggerSchema, type workflowUpdateRecordActionSchema, type workflowWebhookTriggerSchema, + type workflowDelayActionSchema, } from 'twenty-shared/workflow'; import { type z } from 'zod'; @@ -42,6 +43,7 @@ export type WorkflowDeleteRecordAction = z.infer< export type WorkflowFindRecordsAction = z.infer< typeof workflowFindRecordsActionSchema >; +export type WorkflowDelayAction = z.infer; export type WorkflowFilterAction = z.infer; export type WorkflowFormAction = z.infer; export type WorkflowHttpRequestAction = z.infer< @@ -65,6 +67,7 @@ export type WorkflowAction = | WorkflowHttpRequestAction | WorkflowAiAgentAction | WorkflowIteratorAction + | WorkflowDelayAction | WorkflowEmptyAction; export type WorkflowActionType = WorkflowAction['type']; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx index 5d7936d242a..ed95dac5948 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx @@ -47,9 +47,12 @@ export const WorkflowDiagramStepNodeIcon = ({ case 'AI_AGENT': { return ; } - case 'EMPTY': { + case 'EMPTY': return null; - } + case 'DELAY': + case 'FILTER': + case 'ITERATOR': + return ; default: { return ( ); } + case 'DELAY': { + return ( + + ); + } } } } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx index e4110387ca5..0d9a5433f18 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx @@ -10,6 +10,7 @@ import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workfl import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty'; import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail'; import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord'; +import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay'; import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder'; @@ -210,6 +211,15 @@ export const WorkflowStepDetail = ({ case 'EMPTY': { return ; } + case 'DELAY': { + return ( + + ); + } default: return assertUnreachable( stepDefinition.definition, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts index b13f8f591b4..1c57657c8bd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts @@ -2,7 +2,7 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow'; export const FLOW_ACTIONS: Array<{ label: string; - type: Extract; + type: Extract; icon: string; }> = [ { @@ -15,4 +15,9 @@ export const FLOW_ACTIONS: Array<{ type: 'FILTER', icon: 'IconFilter', }, + { + label: 'Delay', + type: 'DELAY', + icon: 'IconPlayerPause', + }, ]; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx new file mode 100644 index 00000000000..08a119b4ed8 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx @@ -0,0 +1,225 @@ +import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput'; +import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; +import { Select } from '@/ui/input/components/Select'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { type WorkflowDelayAction } from '@/workflow/types/Workflow'; +import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter'; +import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; +import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; +import { t } from '@lingui/core/macro'; +import { + HorizontalSeparator, + IconCalendar, + IconHourglassHigh, +} from 'twenty-ui/display'; +import { type SelectOption } from 'twenty-ui/input'; +type WorkflowEditActionDelayProps = { + action: WorkflowDelayAction; + actionOptions: + | { + readonly: true; + } + | { + readonly?: false; + onActionUpdate: (action: WorkflowDelayAction) => void; + }; +}; + +export const WorkflowEditActionDelay = ({ + action, + actionOptions, +}: WorkflowEditActionDelayProps) => { + const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } = + useWorkflowActionHeader({ + action, + defaultTitle: 'Delay', + }); + + const delayOptions: Array> = [ + { + label: t`At a specific date or time`, + value: 'SCHEDULED_DATE', + Icon: IconCalendar, + }, + { + label: t`After a set amount of time`, + value: 'DURATION', + Icon: IconHourglassHigh, + }, + ]; + + const handleDelayTypeChange = ( + newDelayType: 'SCHEDULED_DATE' | 'DURATION', + ) => { + if ( + actionOptions.readonly === true || + newDelayType === action.settings.input.delayType + ) { + return; + } + + if (newDelayType === 'SCHEDULED_DATE') { + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'SCHEDULED_DATE', + }, + }, + }); + } else { + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'DURATION', + duration: undefined, + }, + }, + }); + } + }; + + const handleDateTimeChange = (value: string | null) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'SCHEDULED_DATE', + scheduledDateTime: value ?? '', + }, + }, + }); + }; + + const handleDurationChange = ( + field: 'days' | 'hours' | 'minutes' | 'seconds', + value: number | string | null, + ) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'DURATION', + duration: { + days: + field === 'days' + ? (value ?? undefined) + : action.settings.input.duration?.days, + hours: + field === 'hours' + ? (value ?? undefined) + : action.settings.input.duration?.hours, + minutes: + field === 'minutes' + ? (value ?? undefined) + : action.settings.input.duration?.minutes, + seconds: + field === 'seconds' + ? (value ?? undefined) + : action.settings.input.duration?.seconds, + }, + }, + }, + }); + }; + + const HeaderIcon = getIcon(headerIcon ?? 'IconPlayerPause'); + + return ( + <> + { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + name: newTitle, + }); + }} + /> + + +