diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
index 691032a70..3daa533d2 100644
--- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts
+++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
@@ -55,6 +55,13 @@ export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
};
};
+export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
+ input: {
+ objectName: string;
+ limit?: number;
+ };
+};
+
type BaseWorkflowAction = {
id: string;
name: string;
@@ -86,12 +93,18 @@ export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
settings: WorkflowDeleteRecordActionSettings;
};
+export type WorkflowFindRecordsAction = BaseWorkflowAction & {
+ type: 'FIND_RECORDS';
+ settings: WorkflowFindRecordsActionSettings;
+};
+
export type WorkflowAction =
| WorkflowCodeAction
| WorkflowSendEmailAction
| WorkflowCreateRecordAction
| WorkflowUpdateRecordAction
- | WorkflowDeleteRecordAction;
+ | WorkflowDeleteRecordAction
+ | WorkflowFindRecordsAction;
export type WorkflowActionType = WorkflowAction['type'];
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 985b05638..373570359 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
@@ -7,11 +7,12 @@ import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord';
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
+import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords';
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormSendEmail';
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
+import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
-import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
@@ -138,6 +139,16 @@ export const WorkflowStepDetail = ({
/>
);
}
+
+ case 'FIND_RECORDS': {
+ return (
+
+ );
+ }
}
return null;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords.tsx
new file mode 100644
index 000000000..fd1e879f0
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords.tsx
@@ -0,0 +1,147 @@
+import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
+import { Select, SelectOption } from '@/ui/input/components/Select';
+import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
+import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
+import { useTheme } from '@emotion/react';
+import { useEffect, useState } from 'react';
+
+import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
+import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
+import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
+import { isDefined } from 'twenty-shared';
+import { HorizontalSeparator, useIcons } from 'twenty-ui';
+import { useDebouncedCallback } from 'use-debounce';
+
+type WorkflowEditActionFormFindRecordsProps = {
+ action: WorkflowFindRecordsAction;
+ actionOptions:
+ | {
+ readonly: true;
+ }
+ | {
+ readonly?: false;
+ onActionUpdate: (action: WorkflowFindRecordsAction) => void;
+ };
+};
+
+type FindRecordsFormData = {
+ objectName: string;
+ limit?: number;
+};
+
+export const WorkflowEditActionFormFindRecords = ({
+ action,
+ actionOptions,
+}: WorkflowEditActionFormFindRecordsProps) => {
+ const theme = useTheme();
+ const { getIcon } = useIcons();
+
+ const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
+
+ const availableMetadata: Array> =
+ activeObjectMetadataItems.map((item) => ({
+ Icon: getIcon(item.icon),
+ label: item.labelPlural,
+ value: item.nameSingular,
+ }));
+
+ const [formData, setFormData] = useState({
+ objectName: action.settings.input.objectName,
+ limit: action.settings.input.limit,
+ });
+ const isFormDisabled = actionOptions.readonly;
+
+ const selectedObjectMetadataItemNameSingular = formData.objectName;
+
+ const selectedObjectMetadataItem = activeObjectMetadataItems.find(
+ (item) => item.nameSingular === selectedObjectMetadataItemNameSingular,
+ );
+ if (!isDefined(selectedObjectMetadataItem)) {
+ throw new Error('Should have found the metadata item');
+ }
+
+ const saveAction = useDebouncedCallback(
+ async (formData: FindRecordsFormData) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ const { objectName: updatedObjectName, limit: updatedLimit } = formData;
+
+ actionOptions.onActionUpdate({
+ ...action,
+ settings: {
+ ...action.settings,
+ input: {
+ objectName: updatedObjectName,
+ limit: updatedLimit ?? 1,
+ },
+ },
+ });
+ },
+ 1_000,
+ );
+
+ useEffect(() => {
+ return () => {
+ saveAction.flush();
+ };
+ }, [saveAction]);
+
+ const headerTitle = isDefined(action.name) ? action.name : `Search Records`;
+ const headerIcon = getActionIcon(action.type);
+
+ return (
+ <>
+ {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ actionOptions.onActionUpdate({
+ ...action,
+ name: newName,
+ });
+ }}
+ Icon={getIcon(headerIcon)}
+ iconColor={theme.font.color.tertiary}
+ initialTitle={headerTitle}
+ headerType="Action"
+ disabled={isFormDisabled}
+ />
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormFindRecords.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormFindRecords.stories.tsx
new file mode 100644
index 000000000..527d65f80
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormFindRecords.stories.tsx
@@ -0,0 +1,92 @@
+import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
+import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords';
+import { Meta, StoryObj } from '@storybook/react';
+import { expect, fn, userEvent, within } from '@storybook/test';
+import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
+import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
+import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
+import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
+
+const DEFAULT_ACTION = {
+ id: getWorkflowNodeIdMock(),
+ name: 'Search Records',
+ type: 'FIND_RECORDS',
+ valid: false,
+ settings: {
+ input: {
+ objectName: 'person',
+ limit: 1,
+ },
+ outputSchema: {},
+ errorHandlingOptions: {
+ retryOnFailure: {
+ value: false,
+ },
+ continueOnFailure: {
+ value: false,
+ },
+ },
+ },
+} satisfies WorkflowFindRecordsAction;
+
+const meta: Meta = {
+ title: 'Modules/Workflow/WorkflowEditActionFormFindRecords',
+ component: WorkflowEditActionFormFindRecords,
+ parameters: {
+ msw: graphqlMocks,
+ },
+ args: {
+ action: DEFAULT_ACTION,
+ },
+ decorators: [
+ WorkflowStepActionDrawerDecorator,
+ WorkflowStepDecorator,
+ ComponentDecorator,
+ ObjectMetadataItemsDecorator,
+ SnackBarDecorator,
+ RouterDecorator,
+ WorkspaceDecorator,
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ actionOptions: {
+ onActionUpdate: fn(),
+ },
+ },
+};
+
+export const DisabledWithEmptyValues: Story = {
+ args: {
+ actionOptions: {
+ readonly: true,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const titleInput = await canvas.findByDisplayValue('Search Records');
+
+ expect(titleInput).toBeDisabled();
+
+ const objectSelectCurrentValue = await canvas.findByText('People');
+
+ await userEvent.click(objectSelectCurrentValue);
+
+ {
+ const searchInputInSelectDropdown =
+ canvas.queryByPlaceholderText('Search');
+
+ expect(searchInputInSelectDropdown).not.toBeInTheDocument();
+ }
+ },
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/RecordActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/RecordActions.ts
index 6935b23c9..7fcf94a72 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/RecordActions.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/RecordActions.ts
@@ -20,4 +20,9 @@ export const RECORD_ACTIONS: Array<{
type: 'DELETE_RECORD',
icon: 'IconTrash',
},
+ {
+ label: 'Search Records',
+ type: 'FIND_RECORDS',
+ icon: 'IconSearch',
+ },
];
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts
index c042becf5..a571fcbe7 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts
@@ -6,6 +6,7 @@ export const getActionIcon = (actionType: string) => {
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
+ case 'FIND_RECORDS':
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
default:
return OTHER_ACTIONS.find((item) => item.type === actionType)?.icon;
diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts
index a685e7844..e572083f3 100644
--- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
+import { isDefined } from 'twenty-shared';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
-import { isDefined } from 'twenty-shared';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
@@ -186,6 +186,26 @@ export class WorkflowVersionStepWorkspaceService {
},
};
}
+ case WorkflowActionType.FIND_RECORDS: {
+ const activeObjectMetadataItem =
+ await this.objectMetadataRepository.findOne({
+ where: { workspaceId, isActive: true, isSystem: false },
+ });
+
+ return {
+ id: newStepId,
+ name: 'Search Records',
+ type: WorkflowActionType.FIND_RECORDS,
+ valid: false,
+ settings: {
+ ...BASE_STEP_DEFINITION,
+ input: {
+ objectName: activeObjectMetadataItem?.nameSingular || '',
+ limit: 1,
+ },
+ },
+ };
+ }
default:
throw new WorkflowVersionStepException(
`WorkflowActionType '${type}' unknown`,