From 1adb59f7f8e20c824859a90e6cc4f1ab16745152 Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Tue, 12 May 2026 12:10:10 +0530
Subject: [PATCH] Support optional labels on logic-function input schema fields
(#20471)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds an optional label field to logic-function input schema properties
(InputSchemaProperty and InputJsonSchema). When set, the workflow
builder renders the label in place of the raw property key for both leaf
inputs and nested sections; when unset, it falls back to the key.
jsonSchemaToInputSchema propagates the label so app authors can declare
it in their JSON schema. Payload paths, the variable picker, and saved
workflow inputs continue to use the property key — labels are
display-only.
---
.../WorkflowEditActionCodeFields.tsx | 24 +++++-----
.../json-schema-to-input-schema.test.ts | 44 +++++++++++++++++++
.../logic-function/input-json-schema.type.ts | 1 +
.../json-schema-to-input-schema.ts | 5 +++
.../src/workflow/types/InputSchema.ts | 1 +
5 files changed, 65 insertions(+), 10 deletions(-)
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx
index deb44af178..ed90400e8c 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx
@@ -10,7 +10,7 @@ import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-step
import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
-import { isNonEmptyArray, isObject } from '@sniptt/guards';
+import { isNonEmptyArray, isNonEmptyString, 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';
@@ -51,10 +51,18 @@ export const WorkflowEditActionCodeFields = ({
const currentPath = [...path, inputKey];
const pathKey = currentPath.join('.');
+ const schemaProperty = getInputSchemaPropertyAtPath(
+ inputSchema,
+ currentPath,
+ );
+ const displayLabel = isNonEmptyString(schemaProperty?.label)
+ ? schemaProperty.label
+ : inputKey;
+
if (inputValue !== null && isObject(inputValue)) {
return (
- {inputKey}
+ {displayLabel}
{
expect(result[0].properties?.title).toEqual({ type: 'string' });
expect(result[0].properties?.other).toEqual({ type: 'string' });
});
+
+ it('preserves label on top-level and nested properties', () => {
+ const result = jsonSchemaToInputSchema({
+ type: 'object',
+ label: 'Slack Message',
+ properties: {
+ slackChannelId: { type: 'string', label: 'Channel' },
+ messageText: { type: 'string', label: 'Message text' },
+ meta: {
+ type: 'object',
+ properties: {
+ count: { type: 'integer', label: 'Count' },
+ },
+ },
+ },
+ });
+
+ expect(result[0].label).toBe('Slack Message');
+ expect(result[0].properties?.slackChannelId).toEqual({
+ type: 'string',
+ label: 'Channel',
+ });
+ expect(result[0].properties?.messageText).toEqual({
+ type: 'string',
+ label: 'Message text',
+ });
+ expect(result[0].properties?.meta?.properties?.count).toEqual({
+ type: 'number',
+ label: 'Count',
+ });
+ });
+
+ it('does not set label when empty or omitted', () => {
+ const result = jsonSchemaToInputSchema({
+ type: 'object',
+ properties: {
+ empty: { type: 'string', label: '' },
+ missing: { type: 'string' },
+ },
+ });
+
+ expect(result[0].properties?.empty).toEqual({ type: 'string' });
+ expect(result[0].properties?.missing).toEqual({ type: 'string' });
+ });
});
diff --git a/packages/twenty-shared/src/logic-function/input-json-schema.type.ts b/packages/twenty-shared/src/logic-function/input-json-schema.type.ts
index ec0f817986..782764eede 100644
--- a/packages/twenty-shared/src/logic-function/input-json-schema.type.ts
+++ b/packages/twenty-shared/src/logic-function/input-json-schema.type.ts
@@ -16,4 +16,5 @@ export type InputJsonSchema = {
minimum?: number;
maximum?: number;
multiline?: boolean;
+ label?: string;
};
diff --git a/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts b/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts
index dd2bb5cda1..2c0a17633a 100644
--- a/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts
+++ b/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts
@@ -3,6 +3,7 @@ import {
type InputSchema,
type InputSchemaProperty,
} from '@/workflow/types/InputSchema';
+import { isNonEmptyString } from '@sniptt/guards';
const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
const property: InputSchemaProperty = { type: 'unknown' };
@@ -50,6 +51,10 @@ const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
property.multiline = true;
}
+ if (isNonEmptyString(jsonSchema.label)) {
+ property.label = jsonSchema.label;
+ }
+
return property;
};
diff --git a/packages/twenty-shared/src/workflow/types/InputSchema.ts b/packages/twenty-shared/src/workflow/types/InputSchema.ts
index 83cffd9a5c..1032899323 100644
--- a/packages/twenty-shared/src/workflow/types/InputSchema.ts
+++ b/packages/twenty-shared/src/workflow/types/InputSchema.ts
@@ -9,6 +9,7 @@ export type InputSchemaProperty = {
items?: InputSchemaProperty;
properties?: Properties;
multiline?: boolean;
+ label?: string;
};
type Properties = {