diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFieldLeaf.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFieldLeaf.tsx
index 7ba327756a..48c0f716a9 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFieldLeaf.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFieldLeaf.tsx
@@ -1,4 +1,5 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
+import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput';
import { FormMultiRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormMultiRecordPicker';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
@@ -6,6 +7,7 @@ import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
+import { type FieldArrayValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions';
import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind';
@@ -89,6 +91,22 @@ export const WorkflowEditActionCodeFieldLeaf = ({
}
}
+ if (leafKind === 'array') {
+ return (
+
+ );
+ }
+
if (leafKind === 'boolean') {
return (
{
).toBe('record-array');
});
+ it('should map arrays of primitives to the array kind', () => {
+ expect(
+ getWorkflowCodeFieldsLeafKind({
+ type: 'array',
+ items: { type: 'string' },
+ }),
+ ).toBe('array');
+ expect(
+ getWorkflowCodeFieldsLeafKind({
+ type: 'array',
+ items: { type: 'number' },
+ }),
+ ).toBe('array');
+ expect(
+ getWorkflowCodeFieldsLeafKind({
+ type: 'array',
+ items: { type: 'boolean' },
+ }),
+ ).toBe('array');
+ expect(
+ getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.ARRAY }),
+ ).toBe('array');
+ });
+
it('should map the legacy object/array+marker form to record kinds', () => {
expect(
getWorkflowCodeFieldsLeafKind({
@@ -66,11 +90,5 @@ describe('getWorkflowCodeFieldsLeafKind', () => {
}),
).toBe('record-array');
expect(getWorkflowCodeFieldsLeafKind({ type: 'object' })).toBe('text');
- expect(
- getWorkflowCodeFieldsLeafKind({
- type: 'array',
- items: { type: 'object' },
- }),
- ).toBe('text');
});
});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts
index 873d8aad8b..90d475276e 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts
@@ -8,6 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
import { type InputSchemaProperty } from 'twenty-shared/workflow';
type WorkflowCodeFieldsLeafKind =
+ | 'array'
| 'boolean'
| 'enum'
| 'number'
@@ -37,6 +38,10 @@ export const getWorkflowCodeFieldsLeafKind = (
return 'enum';
}
+ if (property.type === 'array' || property.type === FieldMetadataType.ARRAY) {
+ return 'array';
+ }
+
switch (property.type) {
case 'boolean':
case FieldMetadataType.BOOLEAN:
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
index e42521750a..e4e6e1552a 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
@@ -15,11 +15,15 @@ import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/g
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
import { getStepItemIcon } from '@/workflow/workflow-variables/utils/getStepItemIcon';
import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath';
+import {
+ getWorkflowVariableSpecialItems,
+ type WorkflowVariableSpecialItem,
+} from '@/workflow/workflow-variables/utils/getWorkflowVariableSpecialItems';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, useIcons } from 'twenty-ui/icon';
-import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { MenuItemSelect } from 'twenty-ui/navigation';
+import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
type WorkflowVariablesDropdownStepItemsProps = {
step: StepOutputSchemaV2;
@@ -80,6 +84,23 @@ export const WorkflowVariablesDropdownStepItems = ({
);
};
+ const specialItems = getWorkflowVariableSpecialItems({
+ step,
+ currentPath,
+ searchInputValue,
+ });
+
+ const handleSelectSpecialItem = (
+ specialItem: WorkflowVariableSpecialItem,
+ ) => {
+ onSelect(
+ getVariableTemplateFromPath({
+ stepId: step.id,
+ path: specialItem.path,
+ }),
+ );
+ };
+
const displayedSubStepObject = getDisplayedSubStepObject();
const displayedSubStepObjectMetadata = isDefined(displayedSubStepObject)
@@ -136,6 +157,18 @@ export const WorkflowVariablesDropdownStepItems = ({
/>
+ {specialItems.map((specialItem) => (
+ handleSelectSpecialItem(specialItem)}
+ text={specialItem.label}
+ hasSubMenu={false}
+ LeftIcon={getIcon(specialItem.iconName)}
+ contextualText={specialItem.contextualText}
+ />
+ ))}
{shouldDisplaySubStepObject && (
)}
- {filteredOptions.length > 0 && shouldDisplaySubStepObject && (
-
- )}
+ {filteredOptions.length > 0 &&
+ (shouldDisplaySubStepObject || specialItems.length > 0) && (
+
+ )}
{filteredOptions.map(([key, subStep]) => {
if (!isDefined(subStep)) {
return null;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getWorkflowVariableSpecialItems.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getWorkflowVariableSpecialItems.test.ts
new file mode 100644
index 0000000000..b49f937b40
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getWorkflowVariableSpecialItems.test.ts
@@ -0,0 +1,121 @@
+import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
+import { getWorkflowVariableSpecialItems } from '@/workflow/workflow-variables/utils/getWorkflowVariableSpecialItems';
+
+const ITERATOR_STEP_WITH_OBJECT_CURRENT_ITEM: StepOutputSchemaV2 = {
+ id: 'step-1',
+ name: 'Loop',
+ type: 'ITERATOR',
+ outputSchema: {
+ currentItem: {
+ isLeaf: false,
+ icon: 'IconUser',
+ label: 'Current Item',
+ value: {} as never,
+ },
+ currentItemIndex: 0,
+ hasProcessedAllItems: false,
+ },
+};
+
+const CODE_STEP_WITH_FLATTENED_ARRAY: StepOutputSchemaV2 = {
+ id: 'step-2',
+ name: 'Run code',
+ type: 'CODE',
+ outputSchema: {
+ '0': { isLeaf: true, type: 'string', label: '0', value: 'a' },
+ '1': { isLeaf: true, type: 'string', label: '1', value: 'b' },
+ },
+};
+
+const CODE_STEP_WITH_OBJECT_OUTPUT: StepOutputSchemaV2 = {
+ id: 'step-3',
+ name: 'Run code',
+ type: 'CODE',
+ outputSchema: {
+ message: { isLeaf: true, type: 'string', label: 'message', value: 'hi' },
+ },
+};
+
+describe('getWorkflowVariableSpecialItems', () => {
+ it('should offer the whole iterator item when viewing a non-leaf currentItem', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: ITERATOR_STEP_WITH_OBJECT_CURRENT_ITEM,
+ currentPath: ['currentItem'],
+ });
+
+ expect(specialItems).toEqual([
+ {
+ id: 'wholeIteratorItem',
+ label: 'Current Item',
+ contextualText: 'Use the whole item',
+ iconName: 'IconUser',
+ path: ['currentItem'],
+ },
+ ]);
+ });
+
+ it('should not offer the whole iterator item outside of the currentItem path', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: ITERATOR_STEP_WITH_OBJECT_CURRENT_ITEM,
+ currentPath: [],
+ });
+
+ expect(specialItems).toEqual([]);
+ });
+
+ it('should offer the whole list when a step output is a flattened array', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: CODE_STEP_WITH_FLATTENED_ARRAY,
+ currentPath: [],
+ });
+
+ expect(specialItems).toEqual([
+ {
+ id: 'wholeList',
+ label: 'Whole list',
+ contextualText: 'Use the whole list',
+ iconName: 'IconListDetails',
+ path: [],
+ },
+ ]);
+ });
+
+ it('should not offer the whole list when navigating inside the array', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: CODE_STEP_WITH_FLATTENED_ARRAY,
+ currentPath: ['0'],
+ });
+
+ expect(specialItems).toEqual([]);
+ });
+
+ it('should not offer any special item for a regular object output', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: CODE_STEP_WITH_OBJECT_OUTPUT,
+ currentPath: [],
+ });
+
+ expect(specialItems).toEqual([]);
+ });
+
+ it('should hide the whole list when it does not match the search', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: CODE_STEP_WITH_FLATTENED_ARRAY,
+ currentPath: [],
+ searchInputValue: 'zzz',
+ });
+
+ expect(specialItems).toEqual([]);
+ });
+
+ it('should keep the whole list when it matches the search', () => {
+ const specialItems = getWorkflowVariableSpecialItems({
+ step: CODE_STEP_WITH_FLATTENED_ARRAY,
+ currentPath: [],
+ searchInputValue: 'whole',
+ });
+
+ expect(specialItems).toHaveLength(1);
+ expect(specialItems[0].id).toBe('wholeList');
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getWorkflowVariableSpecialItems.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getWorkflowVariableSpecialItems.ts
new file mode 100644
index 0000000000..d97f29af84
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getWorkflowVariableSpecialItems.ts
@@ -0,0 +1,80 @@
+import { isBaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2';
+import { isIteratorOutputSchema } from '@/workflow/workflow-variables/types/guards/isIteratorOutputSchema';
+import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
+import { t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { isFlattenedArrayOutputSchema } from 'twenty-shared/workflow';
+
+export type WorkflowVariableSpecialItemId = 'wholeIteratorItem' | 'wholeList';
+
+export type WorkflowVariableSpecialItem = {
+ id: WorkflowVariableSpecialItemId;
+ label: string;
+ contextualText: string;
+ iconName: string;
+ path: string[];
+};
+
+const matchesSearch = (label: string, searchInputValue?: string): boolean =>
+ !isDefined(searchInputValue) ||
+ label.toLowerCase().includes(searchInputValue.toLowerCase());
+
+export const getWorkflowVariableSpecialItems = ({
+ step,
+ currentPath,
+ searchInputValue,
+}: {
+ step: StepOutputSchemaV2;
+ currentPath: string[];
+ searchInputValue?: string;
+}): WorkflowVariableSpecialItem[] => {
+ const specialItems: WorkflowVariableSpecialItem[] = [];
+
+ const iteratorCurrentItemNode = isIteratorOutputSchema(
+ step.type,
+ step.outputSchema,
+ )
+ ? step.outputSchema.currentItem
+ : undefined;
+
+ const isViewingIteratorCurrentItem =
+ isDefined(iteratorCurrentItemNode) &&
+ !iteratorCurrentItemNode.isLeaf &&
+ currentPath.length === 1 &&
+ currentPath[0] === 'currentItem';
+
+ if (
+ isViewingIteratorCurrentItem &&
+ matchesSearch(iteratorCurrentItemNode.label, searchInputValue)
+ ) {
+ specialItems.push({
+ id: 'wholeIteratorItem',
+ label: iteratorCurrentItemNode.label,
+ contextualText: t`Use the whole item`,
+ iconName: iteratorCurrentItemNode.icon ?? 'IconBraces',
+ path: currentPath,
+ });
+ }
+
+ const isStepOutputFlattenedArray =
+ isBaseOutputSchemaV2(step.outputSchema) &&
+ isFlattenedArrayOutputSchema(step.outputSchema);
+
+ const wholeListLabel = t`Whole list`;
+
+ if (
+ isStepOutputFlattenedArray &&
+ currentPath.length === 0 &&
+ matchesSearch(wholeListLabel, searchInputValue)
+ ) {
+ specialItems.push({
+ id: 'wholeList',
+ label: wholeListLabel,
+ contextualText: t`Use the whole list`,
+ iconName: 'IconListDetails',
+ path: [],
+ });
+ }
+
+ return specialItems;
+};
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts
index 46da8a1890..8748fe2004 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts
@@ -11,8 +11,10 @@ import {
buildManualTriggerMetadataNode,
BulkRecordsAvailability,
extractRawVariableNamePart,
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema,
GlobalAvailability,
isBaseOutputSchemaV2,
+ isFlattenedArrayOutputSchema,
navigateOutputSchemaProperty,
SingleRecordAvailability,
TRIGGER_STEP_ID,
@@ -653,8 +655,26 @@ export class WorkflowSchemaWorkspaceService {
case WorkflowActionType.HTTP_REQUEST:
case WorkflowActionType.LOGIC_FUNCTION: {
const propertyPath = extractPropertyPathFromVariable(items);
+ const outputSchema = this.getOutputSchemaWithExpectedFallback(
+ step.settings,
+ );
+
+ const variableTargetsWholeStepOutput = propertyPath.length === 0;
+
+ if (variableTargetsWholeStepOutput) {
+ if (!isFlattenedArrayOutputSchema(outputSchema)) {
+ return DEFAULT_ITERATOR_CURRENT_ITEM;
+ }
+
+ return (
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema({
+ schema: outputSchema,
+ }) ?? DEFAULT_ITERATOR_CURRENT_ITEM
+ );
+ }
+
const schemaNode = navigateOutputSchemaProperty({
- schema: this.getOutputSchemaWithExpectedFallback(step.settings),
+ schema: outputSchema,
propertyPath,
});
diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts
index ff515f1a80..ae24b301a4 100644
--- a/packages/twenty-shared/src/workflow/index.ts
+++ b/packages/twenty-shared/src/workflow/index.ts
@@ -188,6 +188,10 @@ export { buildManualTriggerMetadataNode } from './workflow-schema/utils/build-ma
export { collectOutputSchemaPaths } from './workflow-schema/utils/collect-output-schema-paths';
export type { OutputSchemaPathFailure } from './workflow-schema/utils/find-output-schema-path-failure';
export { findOutputSchemaPathFailure } from './workflow-schema/utils/find-output-schema-path-failure';
+export {
+ isFlattenedArrayOutputSchema,
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema,
+} from './workflow-schema/utils/flattened-array-output-schema';
export { navigateOutputSchemaProperty } from './workflow-schema/utils/navigate-output-schema-property';
export type { ResolvedVariable } from './workflow-schema/utils/resolve-variable-path-in-output-schema';
export {
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/index.ts b/packages/twenty-shared/src/workflow/workflow-schema/index.ts
index d1f04dee48..5673a3bf9b 100644
--- a/packages/twenty-shared/src/workflow/workflow-schema/index.ts
+++ b/packages/twenty-shared/src/workflow/workflow-schema/index.ts
@@ -7,6 +7,10 @@ export type {
NodeType,
} from './types/base-output-schema.type';
export { collectOutputSchemaPaths } from './utils/collect-output-schema-paths';
+export {
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema,
+ isFlattenedArrayOutputSchema,
+} from './utils/flattened-array-output-schema';
export {
findOutputSchemaPathFailure,
type OutputSchemaPathFailure,
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/flattened-array-output-schema.test.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/flattened-array-output-schema.test.ts
new file mode 100644
index 0000000000..cae7748534
--- /dev/null
+++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/flattened-array-output-schema.test.ts
@@ -0,0 +1,119 @@
+import { type BaseOutputSchemaV2 } from '../../types/base-output-schema.type';
+import {
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema,
+ isFlattenedArrayOutputSchema,
+} from '../flattened-array-output-schema';
+
+describe('isFlattenedArrayOutputSchema', () => {
+ it('should return true when keys are the sequential array indexes', () => {
+ const schema: BaseOutputSchemaV2 = {
+ '0': { isLeaf: false, type: 'object', label: '0', value: {} },
+ '1': { isLeaf: false, type: 'object', label: '1', value: {} },
+ };
+
+ expect(isFlattenedArrayOutputSchema(schema)).toBe(true);
+ });
+
+ it('should return false for a regular object schema', () => {
+ const schema: BaseOutputSchemaV2 = {
+ message: { isLeaf: true, type: 'string', label: 'message', value: 'hi' },
+ };
+
+ expect(isFlattenedArrayOutputSchema(schema)).toBe(false);
+ });
+
+ it('should return false for non-sequential numeric keys', () => {
+ const schema: BaseOutputSchemaV2 = {
+ '0': { isLeaf: true, type: 'string', label: '0', value: 'a' },
+ '2': { isLeaf: true, type: 'string', label: '2', value: 'b' },
+ };
+
+ expect(isFlattenedArrayOutputSchema(schema)).toBe(false);
+ });
+
+ it('should return false for an empty schema', () => {
+ expect(isFlattenedArrayOutputSchema({})).toBe(false);
+ });
+
+ it('should return false for undefined', () => {
+ expect(isFlattenedArrayOutputSchema(undefined)).toBe(false);
+ });
+});
+
+describe('getCurrentItemSchemaFromFlattenedArrayOutputSchema', () => {
+ it('should derive the current item from the first object element', () => {
+ const schema: BaseOutputSchemaV2 = {
+ '0': {
+ isLeaf: false,
+ type: 'object',
+ label: '0',
+ value: {
+ hello: { isLeaf: true, type: 'string', label: 'hello', value: '1' },
+ },
+ },
+ '1': {
+ isLeaf: false,
+ type: 'object',
+ label: '1',
+ value: {
+ hello: { isLeaf: true, type: 'string', label: 'hello', value: '2' },
+ },
+ },
+ };
+
+ expect(
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema({ schema }),
+ ).toEqual({
+ isLeaf: false,
+ type: 'object',
+ label: 'Current Item',
+ value: {
+ hello: { isLeaf: true, type: 'string', label: 'hello', value: '1' },
+ },
+ });
+ });
+
+ it('should derive the current item from the first primitive element', () => {
+ const schema: BaseOutputSchemaV2 = {
+ '0': { isLeaf: true, type: 'number', label: '0', value: 1 },
+ '1': { isLeaf: true, type: 'number', label: '1', value: 2 },
+ };
+
+ expect(
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema({ schema }),
+ ).toEqual({
+ isLeaf: true,
+ type: 'number',
+ label: 'Current Item',
+ value: 1,
+ });
+ });
+
+ it('should support a custom label', () => {
+ const schema: BaseOutputSchemaV2 = {
+ '0': { isLeaf: true, type: 'string', label: '0', value: 'a' },
+ };
+
+ expect(
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema({
+ schema,
+ label: 'Item',
+ }),
+ ).toEqual({
+ isLeaf: true,
+ type: 'string',
+ label: 'Item',
+ value: 'a',
+ });
+ });
+
+ it('should return undefined when there is no first element', () => {
+ const schema: BaseOutputSchemaV2 = {
+ message: { isLeaf: true, type: 'string', label: 'message', value: 'hi' },
+ };
+
+ expect(
+ getCurrentItemSchemaFromFlattenedArrayOutputSchema({ schema }),
+ ).toBeUndefined();
+ });
+});
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.code.test.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.code.test.ts
index 2a374efe5c..be872547bb 100644
--- a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.code.test.ts
+++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.code.test.ts
@@ -225,6 +225,55 @@ describe('searchVariableInOutputSchema - code output schema', () => {
});
});
+ describe('Flattened top-level array tests', () => {
+ const mockFlattenedArraySchema: BaseOutputSchemaV2 = {
+ '0': {
+ isLeaf: false,
+ type: 'object',
+ label: '0',
+ value: {
+ hello: { isLeaf: true, type: 'string', label: 'hello', value: '1' },
+ },
+ },
+ '1': {
+ isLeaf: false,
+ type: 'object',
+ label: '1',
+ value: {
+ hello: { isLeaf: true, type: 'string', label: 'hello', value: '2' },
+ },
+ },
+ };
+
+ it('should label the whole output ({{stepId}}) as an array', () => {
+ const result = searchVariableThroughCodeOutputSchema({
+ stepName: 'Code Action',
+ codeOutputSchema: mockFlattenedArraySchema,
+ rawVariableName: '{{step1}}',
+ });
+
+ expect(result).toEqual({
+ variableLabel: 'Code Action',
+ variablePathLabel: 'Code Action',
+ variableType: 'ARRAY',
+ });
+ });
+
+ it('should still resolve an indexed element of the array', () => {
+ const result = searchVariableThroughCodeOutputSchema({
+ stepName: 'Code Action',
+ codeOutputSchema: mockFlattenedArraySchema,
+ rawVariableName: '{{step1.0.hello}}',
+ });
+
+ expect(result).toEqual({
+ variableLabel: 'hello',
+ variablePathLabel: 'Code Action > 0 > hello',
+ variableType: 'string',
+ });
+ });
+ });
+
describe('Edge cases', () => {
const mockBaseSchema: BaseOutputSchemaV2 = {
simpleField: {
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.iterator.test.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.iterator.test.ts
index a752219875..5c3b600d16 100644
--- a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.iterator.test.ts
+++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/search-variable-in-output-schema.iterator.test.ts
@@ -108,6 +108,21 @@ describe('searchVariableInOutputSchema - iterator output schema', () => {
});
});
+ it('should handle selecting the whole currentItem correctly', () => {
+ const result = searchVariableThroughIteratorOutputSchema({
+ stepName: 'Iterate Companies',
+ iteratorOutputSchema: mockIteratorSchema,
+ rawVariableName: '{{step1.currentItem}}',
+ isFullRecord: false,
+ });
+
+ expect(result).toEqual({
+ variableLabel: 'Current Item',
+ variablePathLabel: 'Iterate Companies > Current Item',
+ variableType: 'unknown',
+ });
+ });
+
it('should return undefined for invalid field name', () => {
const result = searchVariableThroughIteratorOutputSchema({
stepName: 'Iterate Companies',
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/flattened-array-output-schema.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/flattened-array-output-schema.ts
new file mode 100644
index 0000000000..f6724f09af
--- /dev/null
+++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/flattened-array-output-schema.ts
@@ -0,0 +1,42 @@
+import { isDefined } from '@/utils';
+
+import {
+ type BaseOutputSchemaV2,
+ type Leaf,
+ type Node,
+} from '../types/base-output-schema.type';
+
+export const isFlattenedArrayOutputSchema = (
+ schema: BaseOutputSchemaV2 | undefined,
+): boolean => {
+ if (!isDefined(schema)) {
+ return false;
+ }
+
+ const keys = Object.keys(schema);
+
+ if (keys.length === 0) {
+ return false;
+ }
+
+ return keys.every((key, index) => key === String(index));
+};
+
+export const getCurrentItemSchemaFromFlattenedArrayOutputSchema = ({
+ schema,
+ label = 'Current Item',
+}: {
+ schema: BaseOutputSchemaV2;
+ label?: string;
+}): Leaf | Node | undefined => {
+ const firstItemNode = schema['0'];
+
+ if (!isDefined(firstItemNode)) {
+ return undefined;
+ }
+
+ return {
+ ...firstItemNode,
+ label,
+ };
+};
diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/search-variable-in-output-schema.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/search-variable-in-output-schema.ts
index fab2835d78..112c2cbf4e 100644
--- a/packages/twenty-shared/src/workflow/workflow-schema/utils/search-variable-in-output-schema.ts
+++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/search-variable-in-output-schema.ts
@@ -1,6 +1,6 @@
+import { FieldMetadataType } from '@/types/FieldMetadataType';
import { isDefined } from '@/utils';
import { isObject } from 'class-validator';
-import { FieldMetadataType } from '@/types/FieldMetadataType';
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '../../constants/CaptureAllVariableTagInnerRegex';
import { parseVariablePath } from '../../utils/variable-path.util';
@@ -16,6 +16,7 @@ import {
type RecordOutputSchemaV2,
type VariableSearchResult,
} from '../types/output-schema.type';
+import { isFlattenedArrayOutputSchema } from './flattened-array-output-schema';
const EMPTY_RESULT: VariableSearchResult = {
variableLabel: undefined,
@@ -490,6 +491,19 @@ const searchThroughCodeOutputSchema = ({
return EMPTY_RESULT;
}
+ const parts = parseVariablePath(stripBrackets(rawVariableName));
+
+ if (
+ parts.length === 1 &&
+ isFlattenedArrayOutputSchema(codeOutputSchema as BaseOutputSchemaV2)
+ ) {
+ return {
+ variableLabel: stepName,
+ variablePathLabel: stepName,
+ variableType: FieldMetadataType.ARRAY,
+ };
+ }
+
return searchThroughBaseOutputSchema({
stepName,
baseOutputSchema: codeOutputSchema as BaseOutputSchemaV2,