diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 3cf53dc876..aad4f42d90 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -980,6 +980,7 @@ export enum FeatureFlagKey {
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
+ IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts
index 39d7feae44..ab86491a2b 100644
--- a/packages/twenty-front/src/generated/graphql.ts
+++ b/packages/twenty-front/src/generated/graphql.ts
@@ -944,6 +944,7 @@ export enum FeatureFlagKey {
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
+ IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
index b355721b73..3cc6cf0eb9 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
@@ -16,6 +16,7 @@ import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/s
import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions';
import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions';
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
+import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -91,6 +92,9 @@ export const CommandMenuWorkflowSelectActionContent = ({
const theme = useTheme();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
+ const isIteratorEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
+ );
const { t } = useLingui();
@@ -116,6 +120,14 @@ export const CommandMenuWorkflowSelectActionContent = ({
{t`Human Input`}
{WorkflowActionMenuItems(HUMAN_INPUT_ACTIONS, theme, handleCreateStep)}
+ {isIteratorEnabled && (
+ <>
+
+ {t`Others`}
+
+ {WorkflowActionMenuItems(OTHER_ACTIONS, theme, handleCreateStep)}
+ >
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
index 53973041df..cdd250367e 100644
--- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts
+++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
@@ -9,6 +9,7 @@ import {
type workflowFindRecordsActionSchema,
type workflowFormActionSchema,
type workflowHttpRequestActionSchema,
+ type workflowIteratorActionSchema,
type workflowManualTriggerSchema,
type workflowRunSchema,
type workflowRunStateSchema,
@@ -42,7 +43,9 @@ export type WorkflowFormAction = z.infer;
export type WorkflowHttpRequestAction = z.infer<
typeof workflowHttpRequestActionSchema
>;
-
+export type WorkflowIteratorAction = z.infer<
+ typeof workflowIteratorActionSchema
+>;
export type WorkflowAiAgentAction = z.infer;
export type WorkflowAction =
@@ -55,7 +58,8 @@ export type WorkflowAction =
| WorkflowFilterAction
| WorkflowFormAction
| WorkflowHttpRequestAction
- | WorkflowAiAgentAction;
+ | WorkflowAiAgentAction
+ | WorkflowIteratorAction;
export type WorkflowActionType = WorkflowAction['type'];
export type WorkflowStep = WorkflowAction;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx
index 1e564ded2b..c6396fb686 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx
@@ -14,6 +14,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
+import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
@@ -214,6 +215,17 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
+ case 'ITERATOR': {
+ 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 732ae3cb27..146c5a4949 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
@@ -13,6 +13,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
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';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
+import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
@@ -196,7 +197,15 @@ export const WorkflowStepDetail = ({
/>
);
}
-
+ case 'ITERATOR': {
+ return (
+
+ );
+ }
default:
return assertUnreachable(
stepDefinition.definition,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts
index 141c77440a..6791bd27dd 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts
@@ -2,35 +2,12 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';
export const OTHER_ACTIONS: Array<{
label: string;
- type: Exclude<
- WorkflowActionType,
- 'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS'
- >;
+ type: Extract;
icon: string;
}> = [
{
- label: 'Send Email',
- type: 'SEND_EMAIL',
- icon: 'IconSend',
- },
- {
- label: 'Code',
- type: 'CODE',
- icon: 'IconCode',
- },
- {
- label: 'Form',
- type: 'FORM',
- icon: 'IconForms',
- },
- {
- label: 'HTTP Request',
- type: 'HTTP_REQUEST',
- icon: 'IconWorld',
- },
- {
- label: 'AI Agent',
- type: 'AI_AGENT',
- icon: 'IconBrain',
+ label: 'Iterator',
+ type: 'ITERATOR',
+ icon: 'IconRepeat',
},
];
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx
new file mode 100644
index 0000000000..e03a7e81ad
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx
@@ -0,0 +1,121 @@
+import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
+import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
+import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
+import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
+import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
+import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
+import { useLingui } from '@lingui/react/macro';
+import { useState } from 'react';
+import { useDebouncedCallback } from 'use-debounce';
+
+type WorkflowEditActionIteratorProps = {
+ action: WorkflowIteratorAction;
+ actionOptions:
+ | {
+ readonly: true;
+ }
+ | {
+ readonly?: false;
+ onActionUpdate: (action: WorkflowIteratorAction) => void;
+ };
+};
+
+export const WorkflowEditActionIterator = ({
+ action,
+ actionOptions,
+}: WorkflowEditActionIteratorProps) => {
+ const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
+ useWorkflowActionHeader({
+ action,
+ defaultTitle: 'Iterator',
+ });
+
+ const { t } = useLingui();
+
+ const [formData, setFormData] = useState({
+ items: action.settings.input.items || [],
+ initialLoopStepIds: action.settings.input.initialLoopStepIds || [],
+ });
+
+ const saveAction = useDebouncedCallback(
+ (updatedFormData: typeof formData) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ actionOptions.onActionUpdate?.({
+ ...action,
+ settings: {
+ ...action.settings,
+ input: {
+ items: updatedFormData.items,
+ initialLoopStepIds: updatedFormData.initialLoopStepIds,
+ },
+ },
+ });
+ },
+ 1000,
+ );
+
+ const handleFieldChange = (field: string, value: any) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ const updatedFormData = { ...formData, [field]: value };
+ setFormData(updatedFormData);
+ saveAction(updatedFormData);
+ };
+
+ const handleTitleChange = (newName: string) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ actionOptions.onActionUpdate?.({
+ ...action,
+ name: newName,
+ });
+ };
+
+ return (
+ <>
+
+
+ handleFieldChange('items', value)}
+ readonly={actionOptions.readonly}
+ VariablePicker={WorkflowVariablePicker}
+ />
+ {/* TODO: remove once the UI is implemented */}
+
+ handleFieldChange('initialLoopStepIds', value)
+ }
+ readonly={actionOptions.readonly}
+ />
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
index 306d09aabf..e065250579 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
@@ -20,7 +20,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
case 'FILTER': {
return msg`Filter`;
}
-
+ case 'ITERATOR': {
+ return msg`Iterator`;
+ }
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
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 1da2091338..040fb29b15 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
@@ -21,6 +21,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'FORM':
return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon;
+ case 'ITERATOR':
+ return 'IconRepeat';
default:
return 'IconDefault';
}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
index 9adfb9eff8..52e87d047f 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
@@ -21,6 +21,7 @@ export const getActionIconColorOrThrow = ({
return theme.font.color.tertiary;
case 'FORM':
return theme.color.orange;
+ case 'ITERATOR':
case 'FILTER':
return theme.font.color.tertiary;
case 'AI_AGENT':
diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
index 6ee344972c..932eed7933 100644
--- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
+++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
@@ -15,4 +15,5 @@ export enum FeatureFlagKey {
IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED',
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
+ IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
}
diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
index 6c6ebfa3e1..047c6e4ebf 100644
--- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
@@ -138,6 +138,7 @@ describe('WorkspaceEntityManager', () => {
IS_API_KEY_ROLES_ENABLED: false,
IS_PAGE_LAYOUT_ENABLED: false,
IS_MESSAGE_FOLDER_CONTROL_ENABLED: false,
+ IS_WORKFLOW_ITERATOR_ENABLED: false,
},
eventEmitterService: {
emitMutationEvent: jest.fn(),
@@ -163,6 +164,7 @@ describe('WorkspaceEntityManager', () => {
IS_WORKSPACE_MIGRATION_V2_ENABLED: false,
IS_API_KEY_ROLES_ENABLED: false,
IS_PAGE_LAYOUT_ENABLED: false,
+ IS_WORKFLOW_ITERATOR_ENABLED: false,
},
permissionsPerRoleId: {},
} as WorkspaceDataSource;
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
index 4d816645d5..caa96f75a7 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
@@ -80,6 +80,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
+ {
+ key: FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
+ workspaceId: workspaceId,
+ value: false,
+ },
])
.execute();
};
diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts
index bb9e08d34f..7befcc6f52 100644
--- a/packages/twenty-shared/src/workflow/index.ts
+++ b/packages/twenty-shared/src/workflow/index.ts
@@ -23,6 +23,7 @@ export {
workflowHttpRequestActionSettingsSchema,
workflowAiAgentActionSettingsSchema,
workflowFilterActionSettingsSchema,
+ workflowIteratorActionSettingsSchema,
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
@@ -33,6 +34,7 @@ export {
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
+ workflowIteratorActionSchema,
workflowActionSchema,
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
diff --git a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts
index c56dffffb8..a78ddbde99 100644
--- a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts
+++ b/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts
@@ -219,6 +219,15 @@ export const workflowFilterActionSettingsSchema =
}),
});
+export const workflowIteratorActionSettingsSchema =
+ baseWorkflowActionSettingsSchema.extend({
+ input: z.object({
+ items: z.union([z.array(z.any()), z.string()]).optional(),
+ // TODO: should never be a string once fix the UI
+ initialLoopStepIds: z.union([z.array(z.string()), z.string()]).optional(),
+ }),
+ });
+
// Action schemas
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
@@ -270,6 +279,11 @@ export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
settings: workflowFilterActionSettingsSchema,
});
+export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
+ type: z.literal('ITERATOR'),
+ settings: workflowIteratorActionSettingsSchema,
+});
+
// Combined action schema
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
@@ -282,6 +296,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
+ workflowIteratorActionSchema,
]);
// Trigger schemas