Use workflow inputSchema to render boolean, number, and enum fields in code/logic function steps (#20439)

<img width="415" height="772" alt="Screenshot 2026-05-11 at 4 44 08 PM"
src="https://github.com/user-attachments/assets/32dbdd3c-e60b-4c43-90bc-18be05f22dcf"
/>
<img width="414" height="371" alt="Screenshot 2026-05-11 at 4 48 24 PM"
src="https://github.com/user-attachments/assets/83be062c-7ed3-4953-98bb-e4290865040b"
/>
This commit is contained in:
Abdul Rahman
2026-05-11 19:51:08 +05:30
committed by GitHub
parent 9813467cee
commit ed75fc8a25
14 changed files with 341 additions and 11 deletions

View File

@@ -8,10 +8,8 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
@@ -19,15 +17,19 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
import { WorkflowCodeEditor } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowCodeEditor';
import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields';
import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowLogicFunctionTabListComponentId';
import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { LogicFunctionTestInputInitEffect } from '@/logic-functions/components/LogicFunctionTestInputInitEffect';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
import { type Monaco } from '@monaco-editor/react';
@@ -35,20 +37,18 @@ import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import {
getOutputSchemaFromValue,
jsonSchemaToInputSchema,
type InputJsonSchema,
} from 'twenty-shared/logic-function';
import { isDefined } from 'twenty-shared/utils';
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
import { IconCode, IconPlayerPlay } from 'twenty-ui/display';
import { CodeEditor } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDebouncedCallback } from 'use-debounce';
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { LogicFunctionTestInputInitEffect } from '@/logic-functions/components/LogicFunctionTestInputInitEffect';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
const StyledCodeEditorContainer = styled.div`
display: flex;
@@ -341,6 +341,7 @@ export const WorkflowEditActionCode = ({
<div data-globally-prevent-click-outside="true">
<WorkflowEditActionCodeFields
functionInput={functionInput}
inputSchema={formValues.workflowActionTriggerSettings?.inputSchema}
VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange}
readonly={actionOptions.readonly}
@@ -381,6 +382,9 @@ export const WorkflowEditActionCode = ({
<>
<WorkflowEditActionCodeFields
functionInput={functionInput}
inputSchema={
formValues.workflowActionTriggerSettings?.inputSchema
}
VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange}
readonly={actionOptions.readonly}
@@ -406,6 +410,9 @@ export const WorkflowEditActionCode = ({
<>
<WorkflowEditActionCodeFields
functionInput={logicFunctionTestData.input}
inputSchema={
formValues.workflowActionTriggerSettings?.inputSchema
}
onInputChange={handleTestInputChange}
readonly={actionOptions.readonly}
/>

View File

@@ -1,11 +1,18 @@
import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput';
import { FormNestedFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormNestedFieldInputContainer';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
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 { InputLabel } from '@/ui/input/components/InputLabel';
import { type FunctionInput } from 'twenty-shared/workflow';
import { getInputSchemaPropertyAtPath } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath';
import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions';
import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isObject } from '@sniptt/guards';
import { isNonEmptyArray, isObject } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type FunctionInput, type InputSchema } from 'twenty-shared/workflow';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
@@ -26,6 +33,7 @@ type WorkflowEditActionCodeFieldsProps = {
onInputChange?: (value: any, path: string[]) => void | Promise<void>;
VariablePicker?: VariablePickerComponent;
fullWidth?: boolean;
inputSchema?: InputSchema;
};
export const WorkflowEditActionCodeFields = ({
@@ -35,6 +43,7 @@ export const WorkflowEditActionCodeFields = ({
onInputChange,
VariablePicker,
fullWidth,
inputSchema,
}: WorkflowEditActionCodeFieldsProps) => {
return (
<StyledContainer fullWidth={fullWidth}>
@@ -54,12 +63,84 @@ export const WorkflowEditActionCodeFields = ({
onInputChange={onInputChange}
VariablePicker={VariablePicker}
fullWidth={fullWidth}
inputSchema={inputSchema}
/>
</FormNestedFieldInputContainer>
</div>
);
}
const schemaProperty = getInputSchemaPropertyAtPath(
inputSchema,
currentPath,
);
const leafKind = getWorkflowCodeFieldsLeafKind(schemaProperty);
if (leafKind === 'boolean') {
return (
<FormBooleanFieldInput
key={pathKey}
label={inputKey}
defaultValue={
!isDefined(inputValue)
? undefined
: typeof inputValue === 'boolean' ||
typeof inputValue === 'string'
? inputValue
: undefined
}
readonly={readonly}
onChange={(value) => onInputChange?.(value, currentPath)}
VariablePicker={VariablePicker}
/>
);
}
if (leafKind === 'number') {
return (
<FormNumberFieldInput
key={pathKey}
label={inputKey}
defaultValue={
!isDefined(inputValue)
? undefined
: typeof inputValue === 'number' ||
typeof inputValue === 'string'
? inputValue
: undefined
}
readonly={readonly}
onChange={(value) => onInputChange?.(value, currentPath)}
VariablePicker={VariablePicker}
/>
);
}
if (leafKind === 'enum' && isDefined(schemaProperty)) {
const enumOptions =
getWorkflowCodeFieldsEnumSelectOptions(schemaProperty);
if (isNonEmptyArray(enumOptions)) {
return (
<FormSelectFieldInput
key={pathKey}
label={inputKey}
defaultValue={
!isDefined(inputValue)
? undefined
: typeof inputValue === 'string'
? inputValue
: String(inputValue)
}
readonly={readonly}
onChange={(value) => onInputChange?.(value, currentPath)}
VariablePicker={VariablePicker}
options={enumOptions}
/>
);
}
}
return (
<FormTextFieldInput
key={pathKey}
@@ -69,6 +150,7 @@ export const WorkflowEditActionCodeFields = ({
readonly={readonly}
onChange={(value) => onInputChange?.(value, currentPath)}
VariablePicker={VariablePicker}
multiline={schemaProperty?.multiline === true}
/>
);
})}

View File

@@ -1,6 +1,7 @@
import { useGetAvailablePackages } from '@/logic-functions/hooks/useGetAvailablePackages';
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields';
@@ -29,6 +30,10 @@ export const WorkflowReadonlyActionCode = ({
id: logicFunctionId,
});
const { logicFunction } = useGetOneLogicFunction({
id: logicFunctionId,
});
const { sourceHandlerCode, loading } = useGetLogicFunctionSourceCode({
logicFunctionId,
});
@@ -55,6 +60,9 @@ export const WorkflowReadonlyActionCode = ({
<WorkflowStepBody>
<WorkflowEditActionCodeFields
functionInput={action.settings.input.logicFunctionInput}
inputSchema={
logicFunction?.workflowActionTriggerSettings?.inputSchema
}
readonly
/>
<StyledCodeEditorContainer>

View File

@@ -0,0 +1,56 @@
import { type InputSchema } from 'twenty-shared/workflow';
import { getInputSchemaPropertyAtPath } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath';
describe('getInputSchemaPropertyAtPath', () => {
const inputSchema = [
{
type: 'object' as const,
properties: {
slackChannelId: { type: 'string' as const },
messageFormat: {
type: 'string' as const,
enum: ['plain', 'markdown'],
},
count: { type: 'number' as const },
nested: {
type: 'object' as const,
properties: {
inner: { type: 'string' as const },
},
},
},
},
] as InputSchema;
it('should return property at top-level path', () => {
expect(
getInputSchemaPropertyAtPath(inputSchema, ['messageFormat']),
).toEqual({
type: 'string',
enum: ['plain', 'markdown'],
});
});
it('should return property at nested path', () => {
expect(
getInputSchemaPropertyAtPath(inputSchema, ['nested', 'inner']),
).toEqual({
type: 'string',
});
});
it('should return undefined when path is invalid', () => {
expect(
getInputSchemaPropertyAtPath(inputSchema, ['missing']),
).toBeUndefined();
expect(
getInputSchemaPropertyAtPath(inputSchema, ['slackChannelId', 'tooDeep']),
).toBeUndefined();
});
it('should return undefined when inputSchema is missing or empty', () => {
expect(getInputSchemaPropertyAtPath(undefined, ['a'])).toBeUndefined();
expect(getInputSchemaPropertyAtPath([], ['a'])).toBeUndefined();
});
});

View File

@@ -0,0 +1,22 @@
import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions';
describe('getWorkflowCodeFieldsEnumSelectOptions', () => {
it('should build select options from schema enum', () => {
expect(
getWorkflowCodeFieldsEnumSelectOptions({
type: 'string',
enum: ['plain', 'markdown'],
}),
).toEqual([
{ value: 'plain', label: 'plain' },
{ value: 'markdown', label: 'markdown' },
]);
});
it('should return empty array when property has no enum', () => {
expect(getWorkflowCodeFieldsEnumSelectOptions({ type: 'string' })).toEqual(
[],
);
expect(getWorkflowCodeFieldsEnumSelectOptions(undefined)).toEqual([]);
});
});

View File

@@ -0,0 +1,36 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind';
describe('getWorkflowCodeFieldsLeafKind', () => {
it('should map schema types to leaf editor kind', () => {
expect(getWorkflowCodeFieldsLeafKind({ type: 'boolean' })).toBe('boolean');
expect(
getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.BOOLEAN }),
).toBe('boolean');
expect(getWorkflowCodeFieldsLeafKind({ type: 'number' })).toBe('number');
expect(
getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.NUMBER }),
).toBe('number');
expect(
getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.NUMERIC }),
).toBe('number');
expect(getWorkflowCodeFieldsLeafKind({ type: 'string' })).toBe('text');
expect(
getWorkflowCodeFieldsLeafKind({
type: 'string',
enum: ['plain', 'markdown'],
}),
).toBe('enum');
expect(
getWorkflowCodeFieldsLeafKind({
type: FieldMetadataType.TEXT,
enum: ['a', 'b'],
}),
).toBe('enum');
expect(getWorkflowCodeFieldsLeafKind({ type: 'string', enum: [] })).toBe(
'text',
);
expect(getWorkflowCodeFieldsLeafKind(undefined)).toBe('text');
});
});

View File

@@ -0,0 +1,30 @@
import {
type InputSchema,
type InputSchemaProperty,
} from 'twenty-shared/workflow';
import { isDefined } from 'twenty-shared/utils';
export const getInputSchemaPropertyAtPath = (
inputSchema: InputSchema | undefined,
path: string[],
): InputSchemaProperty | undefined => {
if (!isDefined(inputSchema) || inputSchema.length === 0) {
return undefined;
}
let current: InputSchemaProperty | undefined = inputSchema[0];
for (const segment of path) {
if (!isDefined(current)) {
return undefined;
}
if (current.type === 'object' && isDefined(current.properties?.[segment])) {
current = current.properties[segment];
} else {
return undefined;
}
}
return current;
};

View File

@@ -0,0 +1,17 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type InputSchemaProperty } from 'twenty-shared/workflow';
import { type SelectOption } from 'twenty-ui/input';
export const getWorkflowCodeFieldsEnumSelectOptions = (
property: InputSchemaProperty | undefined,
): SelectOption[] => {
if (!isDefined(property) || !isNonEmptyArray(property.enum)) {
return [];
}
return property.enum.map((value) => ({
value,
label: value,
}));
};

View File

@@ -0,0 +1,33 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type InputSchemaProperty } from 'twenty-shared/workflow';
type WorkflowCodeFieldsLeafKind = 'boolean' | 'enum' | 'number' | 'text';
export const getWorkflowCodeFieldsLeafKind = (
property: InputSchemaProperty | undefined,
): WorkflowCodeFieldsLeafKind => {
if (!isDefined(property)) {
return 'text';
}
if (
(property.type === 'string' || property.type === FieldMetadataType.TEXT) &&
isNonEmptyArray(property.enum)
) {
return 'enum';
}
switch (property.type) {
case 'boolean':
case FieldMetadataType.BOOLEAN:
return 'boolean';
case 'number':
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
return 'number';
default:
return 'text';
}
};

View File

@@ -213,6 +213,9 @@ export const WorkflowEditActionLogicFunction = ({
<>
<WorkflowEditActionCodeFields
functionInput={testInput}
inputSchema={
logicFunction?.workflowActionTriggerSettings?.inputSchema
}
onInputChange={handleTestInputChange}
readonly={actionOptions.readonly}
/>
@@ -237,6 +240,9 @@ export const WorkflowEditActionLogicFunction = ({
{hasInputFields ? (
<WorkflowEditActionCodeFields
functionInput={functionInput}
inputSchema={
logicFunction?.workflowActionTriggerSettings?.inputSchema
}
readonly={actionOptions.readonly}
onInputChange={handleInputChange}
VariablePicker={WorkflowVariablePicker}

View File

@@ -81,4 +81,31 @@ describe('jsonSchemaToInputSchema', () => {
enum: ['a', 'b'],
});
});
it('preserves multiline on string properties when true', () => {
const result = jsonSchemaToInputSchema({
type: 'object',
properties: {
body: { type: 'string', multiline: true },
},
});
expect(result[0].properties?.body).toEqual({
type: 'string',
multiline: true,
});
});
it('does not set multiline when false or omitted', () => {
const result = jsonSchemaToInputSchema({
type: 'object',
properties: {
title: { type: 'string', multiline: false },
other: { type: 'string' },
},
});
expect(result[0].properties?.title).toEqual({ type: 'string' });
expect(result[0].properties?.other).toEqual({ type: 'string' });
});
});

View File

@@ -15,4 +15,5 @@ export type InputJsonSchema = {
additionalProperties?: boolean | InputJsonSchema;
minimum?: number;
maximum?: number;
multiline?: boolean;
};

View File

@@ -46,6 +46,10 @@ const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
);
}
if (jsonSchema.multiline === true) {
property.multiline = true;
}
return property;
};

View File

@@ -1,5 +1,5 @@
import { type LeafType, type NodeType } from '@/workflow';
import { type FieldMetadataType } from '@/types';
import { type LeafType, type NodeType } from '@/workflow';
export type InputSchemaPropertyType = LeafType | NodeType | FieldMetadataType;
@@ -8,6 +8,7 @@ export type InputSchemaProperty = {
enum?: string[];
items?: InputSchemaProperty;
properties?: Properties;
multiline?: boolean;
};
type Properties = {