diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect.tsx
index 34f3176b48..80c6657808 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect.tsx
@@ -5,22 +5,14 @@ import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFiltersComponentFamilyState';
+import { type FilterSettingsWithPotentiallyDeprecatedOperand } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { useEffect, useMemo } from 'react';
-import {
- type StepFilterGroup,
- type StepFilterWithPotentiallyDeprecatedOperand,
-} from 'twenty-shared/types';
import {
convertViewFilterOperandToCoreOperand,
isDefined,
} from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
-type FilterSettingsWithPotentiallyDeprecatedOperand = {
- stepFilterGroups?: StepFilterGroup[];
- stepFilters?: StepFilterWithPotentiallyDeprecatedOperand[];
-};
-
export const WorkflowEditActionFilterBodyEffect = ({
stepId,
defaultValue,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder.tsx
new file mode 100644
index 0000000000..d9f925d66e
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder.tsx
@@ -0,0 +1,127 @@
+import { InputLabel } from '@/ui/input/components/InputLabel';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect';
+import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
+import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddRootStepFilterButton';
+import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
+import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn';
+import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
+import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
+import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
+import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
+import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/filters/states/rootLevelStepFilterGroupComponentSelector';
+import {
+ type FilterSettings,
+ type FilterSettingsWithPotentiallyDeprecatedOperand,
+} from '@/workflow/workflow-steps/filters/types/FilterSettings';
+import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/filters/utils/isStepFilterGroupChildAStepFilterGroup';
+import { styled } from '@linaria/react';
+import { t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ align-items: start;
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledChildContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[6]};
+ width: 100%;
+`;
+
+const StyledConditions = styled.div`
+ display: flex;
+ flex-direction: column;
+ row-gap: ${themeCssVariables.spacing[0]};
+`;
+
+type WorkflowStepFilterBuilderProps = {
+ instanceId: string;
+ defaultValue?: FilterSettingsWithPotentiallyDeprecatedOperand;
+ readonly?: boolean;
+ onFilterSettingsUpdate: (filterSettings: FilterSettings) => void;
+};
+
+const WorkflowStepFilterBuilderConditions = ({
+ readonly,
+}: {
+ readonly?: boolean;
+}) => {
+ const rootStepFilterGroup = useAtomComponentSelectorValue(
+ rootLevelStepFilterGroupComponentSelector,
+ );
+
+ const { childStepFiltersAndChildStepFilterGroups } =
+ useChildStepFiltersAndChildStepFilterGroups({
+ stepFilterGroupId: rootStepFilterGroup?.id ?? '',
+ });
+
+ return (
+
+ {t`Conditions`}
+ {isDefined(rootStepFilterGroup) ? (
+
+
+ {childStepFiltersAndChildStepFilterGroups.map(
+ (stepFilterGroupChild, stepFilterGroupChildIndex) =>
+ isStepFilterGroupChildAStepFilterGroup(stepFilterGroupChild) ? (
+
+ ) : (
+
+ ),
+ )}
+
+ {!readonly && (
+
+ )}
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const WorkflowStepFilterBuilder = ({
+ instanceId,
+ defaultValue,
+ readonly,
+ onFilterSettingsUpdate,
+}: WorkflowStepFilterBuilderProps) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx
index d98c04ba77..9baac32abd 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx
@@ -14,7 +14,10 @@ import { useLingui } from '@lingui/react/macro';
import { useContext, useState } from 'react';
import { type StepFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
-import { extractRawVariableNamePart } from 'twenty-shared/workflow';
+import {
+ extractRawVariableNamePart,
+ TRIGGER_STEP_ID,
+} from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/icon';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -30,7 +33,9 @@ const NON_SELECTABLE_FIELD_TYPES = [
export const WorkflowStepFilterFieldSelect = ({
stepFilter,
}: WorkflowStepFilterFieldSelectProps) => {
- const { readonly } = useContext(WorkflowStepFilterContext);
+ const { readonly, stepId: currentStepId } = useContext(
+ WorkflowStepFilterContext,
+ );
const { t } = useLingui();
const { closeDropdown } = useCloseDropdown();
const { getIcon } = useIcons();
@@ -88,7 +93,9 @@ export const WorkflowStepFilterFieldSelect = ({
const isSelectedFieldNotFound = !isDefined(variableLabel);
const label = isSelectedFieldNotFound
- ? t`Select a field from a previous step`
+ ? currentStepId === TRIGGER_STEP_ID
+ ? t`Select a field`
+ : t`Select a field from a previous step`
: variableLabel;
const fullRecordIconProps = stepFilter.isFullRecord
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/__stories__/WorkflowStepFilterBuilder.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/__stories__/WorkflowStepFilterBuilder.stories.tsx
new file mode 100644
index 0000000000..c39076bd24
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/__stories__/WorkflowStepFilterBuilder.stories.tsx
@@ -0,0 +1,50 @@
+import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
+import { WorkflowStepFilterBuilder } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder';
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { fn } from 'storybook/test';
+import { ComponentDecorator } from 'twenty-ui/testing';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+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 meta: Meta = {
+ title: 'Modules/Workflow/Filters/WorkflowStepFilterBuilder',
+ component: WorkflowStepFilterBuilder,
+ parameters: {
+ msw: graphqlMocks,
+ },
+ args: {
+ instanceId: getWorkflowNodeIdMock(),
+ defaultValue: {
+ stepFilterGroups: [],
+ stepFilters: [],
+ },
+ readonly: false,
+ onFilterSettingsUpdate: fn(),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ WorkflowStepActionDrawerDecorator,
+ WorkflowStepDecorator,
+ ComponentDecorator,
+ WorkspaceDecorator,
+ ],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const ReadOnly: Story = {
+ args: {
+ readonly: true,
+ },
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/types/FilterSettings.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/types/FilterSettings.ts
index 24807953e6..c98ee63a79 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/types/FilterSettings.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/types/FilterSettings.ts
@@ -1,6 +1,15 @@
-import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
+import {
+ type StepFilter,
+ type StepFilterGroup,
+ type StepFilterWithPotentiallyDeprecatedOperand,
+} from 'twenty-shared/types';
export type FilterSettings = {
stepFilterGroups?: StepFilterGroup[];
stepFilters?: StepFilter[];
};
+
+export type FilterSettingsWithPotentiallyDeprecatedOperand = {
+ stepFilterGroups?: StepFilterGroup[];
+ stepFilters?: StepFilterWithPotentiallyDeprecatedOperand[];
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx
index eb49f9d759..71f282af26 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx
@@ -1,9 +1,9 @@
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
+import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
-import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect';
-import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
-import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
-import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
+import { WorkflowStepFilterBuilder } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder';
+import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
type WorkflowEditActionFilterProps = {
action: WorkflowFilterAction;
@@ -21,31 +21,33 @@ export const WorkflowEditActionFilter = ({
action,
actionOptions,
}: WorkflowEditActionFilterProps) => {
+ const handleFilterSettingsUpdate = (filterSettings: FilterSettings) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ actionOptions.onActionUpdate({
+ ...action,
+ settings: {
+ ...action.settings,
+ input: {
+ stepFilterGroups: filterSettings.stepFilterGroups ?? [],
+ stepFilters: filterSettings.stepFilters ?? [],
+ },
+ },
+ });
+ };
+
return (
<>
-
-
-
-
-
-
+
+
+
{!actionOptions.readonly && }
>
);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx
deleted file mode 100644
index 389cdaea7a..0000000000
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { InputLabel } from '@/ui/input/components/InputLabel';
-import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
-import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
-import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
-import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
-import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddRootStepFilterButton';
-import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
-import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn';
-import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
-import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
-import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/filters/states/rootLevelStepFilterGroupComponentSelector';
-import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
-import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/filters/utils/isStepFilterGroupChildAStepFilterGroup';
-import { styled } from '@linaria/react';
-import { t } from '@lingui/core/macro';
-import { isDefined } from 'twenty-shared/utils';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-
-const StyledContainer = styled.div`
- align-items: start;
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[2]};
-`;
-
-const StyledChildContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[6]};
- width: 100%;
-`;
-
-type WorkflowEditActionFilterBodyProps = {
- action: WorkflowFilterAction;
- actionOptions:
- | {
- readonly: true;
- }
- | {
- readonly?: false;
- onActionUpdate: (action: WorkflowFilterAction) => void;
- };
-};
-
-export const WorkflowEditActionFilterBody = ({
- action,
- actionOptions,
-}: WorkflowEditActionFilterBodyProps) => {
- const rootStepFilterGroup = useAtomComponentSelectorValue(
- rootLevelStepFilterGroupComponentSelector,
- );
-
- const { childStepFiltersAndChildStepFilterGroups } =
- useChildStepFiltersAndChildStepFilterGroups({
- stepFilterGroupId: rootStepFilterGroup?.id ?? '',
- });
-
- const onFilterSettingsUpdate = (newFilterSettings: FilterSettings) => {
- if (actionOptions.readonly === true) {
- return;
- }
-
- actionOptions.onActionUpdate({
- ...action,
- settings: {
- ...action.settings,
- input: {
- stepFilterGroups: newFilterSettings.stepFilterGroups ?? [],
- stepFilters: newFilterSettings.stepFilters ?? [],
- },
- },
- });
- };
-
- return (
-
-
- {t`Conditions`}
- {isDefined(rootStepFilterGroup) ? (
-
-
- {childStepFiltersAndChildStepFilterGroups.map(
- (stepFilterGroupChild, stepFilterGroupChildIndex) =>
- isStepFilterGroupChildAStepFilterGroup(
- stepFilterGroupChild,
- ) ? (
-
- ) : (
-
- ),
- )}
-
- {!actionOptions.readonly && (
-
- )}
-
- ) : (
-
- )}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowEditActionFilterBody.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowEditActionFilterBody.stories.tsx
deleted file mode 100644
index 5f6d0de5e0..0000000000
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowEditActionFilterBody.stories.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
-import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
-import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
-import { type Meta, type StoryObj } from '@storybook/react-vite';
-import { fn } from 'storybook/test';
-import { ComponentDecorator } from 'twenty-ui/testing';
-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: WorkflowFilterAction = {
- id: getWorkflowNodeIdMock(),
- name: 'Filter Records',
- type: 'FILTER',
- valid: false,
- settings: {
- input: {
- stepFilterGroups: [],
- stepFilters: [],
- },
- outputSchema: {},
- errorHandlingOptions: {
- retryOnFailure: {
- value: false,
- },
- continueOnFailure: {
- value: false,
- },
- },
- },
-};
-
-const meta: Meta = {
- title: 'Modules/Workflow/Actions/Filter/WorkflowEditActionFilterBody',
- component: WorkflowEditActionFilterBody,
- parameters: {
- msw: graphqlMocks,
- },
- args: {
- action: DEFAULT_ACTION,
- actionOptions: {
- readonly: false,
- onActionUpdate: fn(),
- },
- },
- decorators: [
- WorkflowStepActionDrawerDecorator,
- WorkflowStepDecorator,
- ComponentDecorator,
- WorkspaceDecorator,
- WorkflowStepFilterDecorator,
- ],
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
-
-export const ReadOnly: Story = {
- args: {
- action: DEFAULT_ACTION,
- actionOptions: {
- readonly: true,
- },
- },
-};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx
index 7a140ffca2..e5c92ed199 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx
@@ -16,6 +16,8 @@ import { type WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
+import { WorkflowStepFilterBuilder } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder';
+import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { useCallback, useMemo, useState } from 'react';
@@ -155,6 +157,23 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
});
};
+ const handleFilterSettingsUpdate = (filterSettings: FilterSettings) => {
+ if (triggerOptions.readonly === true) {
+ return;
+ }
+
+ triggerOptions.onTriggerUpdate({
+ ...trigger,
+ settings: {
+ ...trigger.settings,
+ filter: {
+ stepFilterGroups: filterSettings.stepFilterGroups ?? [],
+ stepFilters: filterSettings.stepFilters ?? [],
+ },
+ },
+ });
+ };
+
const handleSystemObjectsClick = () => {
setIsSystemObjectsOpen(true);
setSearchInputValue('');
@@ -269,6 +288,19 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
actionType="DATABASE_EVENT"
/>
)}
+ {isDefined(selectedObjectMetadataItem) && (
+
+ )}
{!triggerOptions.readonly && (
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/__tests__/build-person-sync-source-filter.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/__tests__/build-person-sync-source-filter.util.spec.ts
new file mode 100644
index 0000000000..0acebab95f
--- /dev/null
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/__tests__/build-person-sync-source-filter.util.spec.ts
@@ -0,0 +1,58 @@
+import { buildPersonSyncSourceFilter } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/build-person-sync-source-filter.util';
+import { evaluateStepFilters } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util';
+
+describe('buildPersonSyncSourceFilter', () => {
+ const filter = buildPersonSyncSourceFilter({
+ createdByFieldMetadataId: 'created-by-field-id',
+ });
+
+ const evaluateForSource = (source?: string) =>
+ evaluateStepFilters({
+ stepFilters: filter.stepFilters,
+ stepFilterGroups: filter.stepFilterGroups,
+ context: {
+ trigger: {
+ properties: {
+ after: {
+ createdBy: source === undefined ? {} : { source },
+ },
+ },
+ },
+ },
+ });
+
+ it('suppresses people auto-created by the email sync', () => {
+ expect(evaluateForSource('EMAIL')).toBe(false);
+ });
+
+ it('suppresses people auto-created by the calendar sync', () => {
+ expect(evaluateForSource('CALENDAR')).toBe(false);
+ });
+
+ it.each(['MANUAL', 'API', 'IMPORT', 'WORKFLOW', 'SYSTEM', 'WEBHOOK'])(
+ 'runs the workflow for people created via %s',
+ (source) => {
+ expect(evaluateForSource(source)).toBe(true);
+ },
+ );
+
+ it('runs the workflow when the createdBy source is missing (fails open)', () => {
+ expect(evaluateForSource(undefined)).toBe(true);
+ });
+
+ it('builds two ANDed source filters that reference the given field', () => {
+ expect(filter.stepFilterGroups).toHaveLength(1);
+ expect(filter.stepFilterGroups[0].logicalOperator).toBe('AND');
+
+ expect(filter.stepFilters).toHaveLength(2);
+ expect(
+ filter.stepFilters.every(
+ (stepFilter) =>
+ stepFilter.fieldMetadataId === 'created-by-field-id' &&
+ stepFilter.operand === 'IS_NOT' &&
+ stepFilter.compositeFieldSubFieldName === 'source' &&
+ stepFilter.stepFilterGroupId === filter.stepFilterGroups[0].id,
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/build-person-sync-source-filter.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/build-person-sync-source-filter.util.ts
new file mode 100644
index 0000000000..5ad3b45009
--- /dev/null
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/build-person-sync-source-filter.util.ts
@@ -0,0 +1,54 @@
+import {
+ FieldActorSource,
+ type StepFilter,
+ type StepFilterGroup,
+ StepLogicalOperator,
+ ViewFilterOperand,
+} from 'twenty-shared/types';
+
+const PERSON_SYNC_SOURCE_FILTER_GROUP_ID =
+ '2d9c1f3a-6b4e-4c8a-9f12-7a3b5c6d8e90';
+
+const PERSON_SYNC_SOURCE_EMAIL_FILTER_ID =
+ '3e8b2a4c-7c5f-4d9b-8a23-6b4c5d7e9f01';
+
+const PERSON_SYNC_SOURCE_CALENDAR_FILTER_ID =
+ '4f9c3b5d-8d6a-4e0c-9b34-7c5d6e8f0a12';
+
+export const buildPersonSyncSourceFilter = ({
+ createdByFieldMetadataId,
+}: {
+ createdByFieldMetadataId: string;
+}): { stepFilterGroups: StepFilterGroup[]; stepFilters: StepFilter[] } => {
+ const baseFilter = {
+ type: 'ACTOR',
+ operand: ViewFilterOperand.IS_NOT,
+ stepOutputKey: '{{trigger.properties.after.createdBy.source}}',
+ stepFilterGroupId: PERSON_SYNC_SOURCE_FILTER_GROUP_ID,
+ compositeFieldSubFieldName: 'source',
+ fieldMetadataId: createdByFieldMetadataId,
+ };
+
+ return {
+ stepFilterGroups: [
+ {
+ id: PERSON_SYNC_SOURCE_FILTER_GROUP_ID,
+ logicalOperator: StepLogicalOperator.AND,
+ },
+ ],
+ stepFilters: [
+ {
+ ...baseFilter,
+ id: PERSON_SYNC_SOURCE_EMAIL_FILTER_ID,
+ value: JSON.stringify([FieldActorSource.EMAIL]),
+ positionInStepFilterGroup: 0,
+ },
+ {
+ ...baseFilter,
+ id: PERSON_SYNC_SOURCE_CALENDAR_FILTER_ID,
+ value: JSON.stringify([FieldActorSource.CALENDAR]),
+ positionInStepFilterGroup: 1,
+ },
+ ],
+ };
+};
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts
index 5eb4f49e5e..842ea60152 100644
--- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts
@@ -8,6 +8,7 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
+import { buildPersonSyncSourceFilter } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/build-person-sync-source-filter.util';
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
@@ -75,6 +76,22 @@ export const prefillWorkflows = async (
throw new Error('Company domainName field metadata not found');
}
+ const personCreatedByFieldMetadata = Object.values(
+ flatFieldMetadataMaps.byUniversalIdentifier,
+ ).find(
+ (fieldMetadata) =>
+ fieldMetadata?.objectMetadataId === personObjectMetadataId &&
+ fieldMetadata?.name === 'createdBy',
+ );
+
+ if (!isDefined(personCreatedByFieldMetadata)) {
+ throw new Error('Person createdBy field metadata not found');
+ }
+
+ const personSyncSourceFilter = buildPersonSyncSourceFilter({
+ createdByFieldMetadataId: personCreatedByFieldMetadata.id,
+ });
+
await entityManager
.createQueryBuilder()
.insert()
@@ -356,6 +373,7 @@ export const prefillWorkflows = async (
},
DatabaseEventAction.UPSERTED,
),
+ filter: personSyncSourceFilter,
},
nextStepIds: ['c30d7cbe-00e0-4966-bc1a-99b0a11a2cca'],
}),
@@ -755,6 +773,7 @@ export const prefillWorkflows = async (
settings: {
eventName: 'person.upserted',
fields: ['emails'],
+ filter: personSyncSourceFilter,
},
},
])
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action.ts
index 2f404cb3aa..dac4a58e90 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action.ts
@@ -1,7 +1,5 @@
import { Injectable } from '@nestjs/common';
-import { resolveInput } from 'twenty-shared/utils';
-
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import {
@@ -12,7 +10,7 @@ import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
-import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
+import { evaluateStepFilters } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util';
@Injectable()
export class FilterWorkflowAction implements WorkflowAction {
@@ -41,15 +39,10 @@ export class FilterWorkflowAction implements WorkflowAction {
};
}
- const resolvedFilters = stepFilters.map((filter) => ({
- ...filter,
- rightOperand: resolveInput(filter.value, context),
- leftOperand: resolveInput(filter.stepOutputKey, context),
- }));
-
- const matchesFilter = evaluateFilterConditions({
- filterGroups: stepFilterGroups,
- filters: resolvedFilters,
+ const matchesFilter = evaluateStepFilters({
+ stepFilters,
+ stepFilterGroups,
+ context,
});
return {
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-step-filters.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-step-filters.util.spec.ts
new file mode 100644
index 0000000000..3ecdb0eb3c
--- /dev/null
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-step-filters.util.spec.ts
@@ -0,0 +1,143 @@
+import {
+ type StepFilter,
+ type StepFilterGroup,
+ StepLogicalOperator,
+ ViewFilterOperand,
+} from 'twenty-shared/types';
+
+import { evaluateStepFilters } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util';
+
+describe('evaluateStepFilters', () => {
+ const context = {
+ trigger: {
+ properties: {
+ after: {
+ createdBy: { source: 'EMAIL' },
+ name: 'Acme',
+ },
+ },
+ },
+ };
+
+ const group: StepFilterGroup = {
+ id: 'group-1',
+ logicalOperator: StepLogicalOperator.AND,
+ };
+
+ const sourceFilter = (operand: ViewFilterOperand): StepFilter => ({
+ id: 'filter-1',
+ type: 'ACTOR',
+ operand,
+ value: JSON.stringify(['EMAIL']),
+ stepOutputKey: '{{trigger.properties.after.createdBy.source}}',
+ stepFilterGroupId: group.id,
+ compositeFieldSubFieldName: 'source',
+ });
+
+ it('returns true when there are no filters', () => {
+ expect(
+ evaluateStepFilters({
+ stepFilters: [],
+ stepFilterGroups: [],
+ context,
+ }),
+ ).toBe(true);
+ });
+
+ it('resolves operands from the context and matches the record', () => {
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [group],
+ stepFilters: [sourceFilter(ViewFilterOperand.IS)],
+ context,
+ }),
+ ).toBe(true);
+ });
+
+ it('returns false when the record source is excluded (IS_NOT)', () => {
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [group],
+ stepFilters: [sourceFilter(ViewFilterOperand.IS_NOT)],
+ context,
+ }),
+ ).toBe(false);
+ });
+
+ it('returns true when a different source is excluded (IS_NOT)', () => {
+ const calendarFilter: StepFilter = {
+ ...sourceFilter(ViewFilterOperand.IS_NOT),
+ value: JSON.stringify(['CALENDAR']),
+ };
+
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [group],
+ stepFilters: [calendarFilter],
+ context,
+ }),
+ ).toBe(true);
+ });
+
+ it('evaluates IS_NOT_EMPTY against a present field when no value is set', () => {
+ const filter: StepFilter = {
+ id: 'filter-present',
+ type: 'TEXT',
+ operand: ViewFilterOperand.IS_NOT_EMPTY,
+ value: '',
+ stepOutputKey: '{{trigger.properties.after.name}}',
+ stepFilterGroupId: group.id,
+ };
+
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [group],
+ stepFilters: [filter],
+ context,
+ }),
+ ).toBe(true);
+ });
+
+ it('resolves a missing field path to empty (IS_EMPTY is true)', () => {
+ const filter: StepFilter = {
+ id: 'filter-missing',
+ type: 'TEXT',
+ operand: ViewFilterOperand.IS_EMPTY,
+ value: '',
+ stepOutputKey: '{{trigger.properties.after.missingField}}',
+ stepFilterGroupId: group.id,
+ };
+
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [group],
+ stepFilters: [filter],
+ context,
+ }),
+ ).toBe(true);
+ });
+
+ it('applies implicit AND across flat filters without groups', () => {
+ const nameContains: StepFilter = {
+ id: 'name-contains',
+ type: 'TEXT',
+ operand: ViewFilterOperand.CONTAINS,
+ value: 'Acme',
+ stepOutputKey: '{{trigger.properties.after.name}}',
+ stepFilterGroupId: 'unused',
+ };
+ const sourceIsCalendar: StepFilter = {
+ ...sourceFilter(ViewFilterOperand.IS),
+ value: JSON.stringify(['CALENDAR']),
+ stepFilterGroupId: 'unused',
+ };
+
+ expect(
+ evaluateStepFilters({
+ stepFilterGroups: [],
+ stepFilters: [nameContains, sourceIsCalendar],
+ context,
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util.ts
new file mode 100644
index 0000000000..a1cd2f711e
--- /dev/null
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util.ts
@@ -0,0 +1,25 @@
+import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
+import { resolveInput } from 'twenty-shared/utils';
+
+import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
+
+export const evaluateStepFilters = ({
+ stepFilters,
+ stepFilterGroups,
+ context,
+}: {
+ stepFilters: StepFilter[];
+ stepFilterGroups: StepFilterGroup[];
+ context: Record;
+}): boolean => {
+ const resolvedFilters = stepFilters.map((filter) => ({
+ ...filter,
+ rightOperand: resolveInput(filter.value, context),
+ leftOperand: resolveInput(filter.stepOutputKey, context),
+ }));
+
+ return evaluateFilterConditions({
+ filterGroups: stepFilterGroups,
+ filters: resolvedFilters,
+ });
+};
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings.ts
index abcf1a2472..868820d632 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings.ts
@@ -1,5 +1,13 @@
+import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
+
+export type DatabaseEventTriggerFilterSettings = {
+ stepFilters: StepFilter[];
+ stepFilterGroups: StepFilterGroup[];
+};
+
export type BaseDatabaseEventTriggerSettings = {
eventName: string;
+ filter?: DatabaseEventTriggerFilterSettings;
};
export type DatabaseEventTriggerSettings =
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts
index 724fb2cab0..c2b3c05a45 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts
@@ -9,7 +9,8 @@ import {
type ObjectRecordUpsertEvent,
} from 'twenty-shared/database-events';
import { type ObjectRecord } from 'twenty-shared/types';
-import { isDefined } from 'twenty-shared/utils';
+import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
+import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { In, Raw } from 'typeorm';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
@@ -31,15 +32,22 @@ import {
type WorkflowAutomatedTriggerWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
+import { evaluateStepFilters } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-step-filters.util';
import {
+ type BaseDatabaseEventTriggerSettings,
type UpdateEventTriggerSettings,
- type UpsertEventTriggerSettings,
} from 'src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings';
import {
WorkflowTriggerJob,
type WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
+type TriggerEvaluationArgs = {
+ eventPayload: ObjectRecordEvent;
+ eventListener: WorkflowAutomatedTriggerWorkspaceEntity;
+ action: DatabaseEventAction;
+};
+
@Injectable()
export class WorkflowDatabaseEventTriggerListener {
private readonly logger = new Logger(
@@ -386,27 +394,26 @@ export class WorkflowDatabaseEventTriggerListener {
eventPayload,
eventListener,
action,
- }: {
- eventPayload: ObjectRecordEvent;
- eventListener: WorkflowAutomatedTriggerWorkspaceEntity;
- action: DatabaseEventAction;
- }) {
- if (action === DatabaseEventAction.UPDATED) {
+ }: TriggerEvaluationArgs) {
+ return (
+ this.eventMatchesWatchedFields({ eventPayload, eventListener, action }) &&
+ this.eventMatchesRecordFilter({ eventPayload, eventListener })
+ );
+ }
+
+ private eventMatchesWatchedFields({
+ eventPayload,
+ eventListener,
+ action,
+ }: TriggerEvaluationArgs) {
+ if (
+ action === DatabaseEventAction.UPDATED ||
+ action === DatabaseEventAction.UPSERTED
+ ) {
const settings = eventListener.settings as UpdateEventTriggerSettings;
- const updateEventPayload = eventPayload as ObjectRecordUpdateEvent;
- const updatedFields = updateEventPayload?.properties?.updatedFields ?? [];
-
- return (
- !settings.fields ||
- settings.fields.length === 0 ||
- settings.fields.some((field) => updatedFields.includes(field))
- );
- }
-
- if (action === DatabaseEventAction.UPSERTED) {
- const settings = eventListener.settings as UpsertEventTriggerSettings;
- const upsertEventPayload = eventPayload as ObjectRecordUpsertEvent;
- const updatedFields = upsertEventPayload?.properties?.updatedFields ?? [];
+ const updatedFields =
+ (eventPayload as ObjectRecordUpdateEvent)?.properties?.updatedFields ??
+ [];
return (
!settings.fields ||
@@ -417,4 +424,32 @@ export class WorkflowDatabaseEventTriggerListener {
return true;
}
+
+ private eventMatchesRecordFilter({
+ eventPayload,
+ eventListener,
+ }: Pick) {
+ const { filter } =
+ eventListener.settings as BaseDatabaseEventTriggerSettings;
+
+ if (!isDefined(filter) || !isNonEmptyArray(filter.stepFilters)) {
+ return true;
+ }
+
+ try {
+ return evaluateStepFilters({
+ stepFilters: filter.stepFilters,
+ stepFilterGroups: filter.stepFilterGroups,
+ context: { [TRIGGER_STEP_ID]: eventPayload },
+ });
+ } catch (error) {
+ this.logger.error(
+ `Failed to evaluate database-event trigger filter for workflow ${eventListener.workflowId}: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ );
+
+ return false;
+ }
+ }
}
diff --git a/packages/twenty-server/test/integration/graphql/suites/workflow/database-event-trigger-filter.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workflow/database-event-trigger-filter.integration-spec.ts
new file mode 100644
index 0000000000..0a2bd94af7
--- /dev/null
+++ b/packages/twenty-server/test/integration/graphql/suites/workflow/database-event-trigger-filter.integration-spec.ts
@@ -0,0 +1,205 @@
+import request from 'supertest';
+
+const client = request(`http://localhost:${APP_PORT}`);
+
+const STEP_FILTER_GROUP_ID = 'a1b2c3d4-1111-4a2b-8c3d-000000000001';
+const STEP_FILTER_ID = 'a1b2c3d4-2222-4a2b-8c3d-000000000002';
+const FILTER_VALUE = 'trigger-me-co';
+
+type AutomatedTriggerNode = {
+ type: string;
+ workflowId: string;
+ settings: {
+ eventName?: string;
+ filter?: {
+ stepFilters: Array>;
+ stepFilterGroups: Array>;
+ };
+ };
+};
+
+const graphql = (query: string, variables?: object) =>
+ client
+ .post('/graphql')
+ .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
+ .send({ query, variables });
+
+describe('Database event trigger filter (e2e)', () => {
+ let createdWorkflowId: string | null = null;
+ let createdWorkflowVersionId: string | null = null;
+
+ beforeAll(async () => {
+ const createWorkflowResponse = await graphql(`
+ mutation CreateWorkflow {
+ createWorkflow(data: { name: "DB Event Trigger Filter Test" }) {
+ id
+ }
+ }
+ `);
+
+ expect(createWorkflowResponse.body.errors).toBeUndefined();
+ createdWorkflowId = createWorkflowResponse.body.data.createWorkflow.id;
+
+ const getWorkflowResponse = await graphql(
+ `
+ query GetWorkflow($id: UUID!) {
+ workflow(filter: { id: { eq: $id } }) {
+ id
+ versions {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
+ }
+ `,
+ { id: createdWorkflowId },
+ );
+
+ expect(getWorkflowResponse.body.errors).toBeUndefined();
+ createdWorkflowVersionId =
+ getWorkflowResponse.body.data.workflow.versions.edges[0].node.id;
+
+ const databaseEventTrigger = {
+ name: 'Company is created',
+ type: 'DATABASE_EVENT',
+ settings: {
+ eventName: 'company.created',
+ outputSchema: {},
+ filter: {
+ stepFilterGroups: [
+ { id: STEP_FILTER_GROUP_ID, logicalOperator: 'AND' },
+ ],
+ stepFilters: [
+ {
+ id: STEP_FILTER_ID,
+ type: 'TEXT',
+ operand: 'CONTAINS',
+ value: FILTER_VALUE,
+ stepOutputKey: '{{trigger.properties.after.name}}',
+ stepFilterGroupId: STEP_FILTER_GROUP_ID,
+ },
+ ],
+ },
+ },
+ nextStepIds: [],
+ position: { x: 0, y: 0 },
+ };
+
+ const updateTriggerResponse = await graphql(
+ `
+ mutation UpdateWorkflowVersion(
+ $id: UUID!
+ $data: WorkflowVersionUpdateInput!
+ ) {
+ updateWorkflowVersion(id: $id, data: $data) {
+ id
+ }
+ }
+ `,
+ {
+ id: createdWorkflowVersionId,
+ data: { trigger: databaseEventTrigger },
+ },
+ );
+
+ expect(updateTriggerResponse.body.errors).toBeUndefined();
+
+ const createStepResponse = await graphql(
+ `
+ mutation CreateWorkflowVersionStep(
+ $input: CreateWorkflowVersionStepInput!
+ ) {
+ createWorkflowVersionStep(input: $input) {
+ stepsDiff
+ }
+ }
+ `,
+ {
+ input: {
+ workflowVersionId: createdWorkflowVersionId,
+ stepType: 'CODE',
+ parentStepId: 'trigger',
+ position: { x: 200, y: 0 },
+ },
+ },
+ );
+
+ expect(createStepResponse.body.errors).toBeUndefined();
+
+ const activateResponse = await graphql(
+ `
+ mutation ActivateWorkflowVersion($workflowVersionId: UUID!) {
+ activateWorkflowVersion(workflowVersionId: $workflowVersionId)
+ }
+ `,
+ { workflowVersionId: createdWorkflowVersionId },
+ );
+
+ expect(activateResponse.body.errors).toBeUndefined();
+ expect(activateResponse.body.data.activateWorkflowVersion).toBe(true);
+ });
+
+ afterAll(async () => {
+ if (createdWorkflowId) {
+ await graphql(
+ `
+ mutation DestroyWorkflow($id: ID!) {
+ destroyWorkflow(id: $id) {
+ id
+ }
+ }
+ `,
+ { id: createdWorkflowId },
+ );
+ }
+ });
+
+ it('syncs the trigger filter onto the workflowAutomatedTrigger row read by the listener', async () => {
+ const response = await graphql(
+ `
+ query WorkflowAutomatedTriggers($workflowId: UUID!) {
+ workflowAutomatedTriggers(
+ filter: { workflowId: { eq: $workflowId } }
+ ) {
+ edges {
+ node {
+ type
+ settings
+ workflowId
+ }
+ }
+ }
+ }
+ `,
+ { workflowId: createdWorkflowId },
+ );
+
+ expect(response.body.errors).toBeUndefined();
+
+ const automatedTriggers: AutomatedTriggerNode[] =
+ response.body.data.workflowAutomatedTriggers.edges.map(
+ (edge: { node: AutomatedTriggerNode }) => edge.node,
+ );
+
+ expect(automatedTriggers).toHaveLength(1);
+
+ const automatedTrigger = automatedTriggers[0];
+
+ expect(automatedTrigger.type).toBe('DATABASE_EVENT');
+ expect(automatedTrigger.settings.eventName).toBe('company.created');
+
+ const filter = automatedTrigger.settings.filter;
+
+ expect(filter).toBeDefined();
+ expect(filter?.stepFilterGroups).toHaveLength(1);
+ expect(filter?.stepFilters).toHaveLength(1);
+ expect(filter?.stepFilters[0]).toMatchObject({
+ operand: 'CONTAINS',
+ value: FILTER_VALUE,
+ stepOutputKey: '{{trigger.properties.after.name}}',
+ });
+ });
+});
diff --git a/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts
index be0dfa5b6a..e773fb9f3d 100644
--- a/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts
+++ b/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts
@@ -1,5 +1,7 @@
import { z } from 'zod';
import { baseTriggerSchema } from './base-trigger-schema';
+import { stepFilterGroupSchema } from './step-filter-group-schema';
+import { stepFilterSchema } from './step-filter-schema';
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema
.extend({
@@ -22,6 +24,15 @@ export const workflowDatabaseEventTriggerSchema = baseTriggerSchema
),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
+ filter: z
+ .object({
+ stepFilterGroups: z.array(stepFilterGroupSchema),
+ stepFilters: z.array(stepFilterSchema),
+ })
+ .optional()
+ .describe(
+ 'Optional condition evaluated against the triggering record. The workflow only runs when the record matches; non-matching events are skipped before a run is created.',
+ ),
}),
})
.describe(