diff --git a/client/src/pages/platform/workflow-editor/components/properties/Property.tsx b/client/src/pages/platform/workflow-editor/components/properties/Property.tsx
index 367dd40a48e..4c1ac20fe2c 100644
--- a/client/src/pages/platform/workflow-editor/components/properties/Property.tsx
+++ b/client/src/pages/platform/workflow-editor/components/properties/Property.tsx
@@ -31,7 +31,7 @@ import {
import {ArrayPropertyType, PropertyAllType, SelectOptionType} from '@/shared/types';
import {TooltipPortal} from '@radix-ui/react-tooltip';
import {UseQueryResult} from '@tanstack/react-query';
-import {CircleQuestionMarkIcon, SquareFunctionIcon} from 'lucide-react';
+import {CircleQuestionMarkIcon, SquareFunctionIcon, XIcon} from 'lucide-react';
import {ReactNode} from 'react';
import {Control, Controller, FieldValues, FormState} from 'react-hook-form';
import {twMerge} from 'tailwind-merge';
@@ -101,6 +101,7 @@ const Property = ({
handleFromAiClick,
handleFromAiToggle,
handleInputChange,
+ handleInputClear,
handleInputTypeSwitchButtonClick,
handleJsonSchemaBuilderChange,
handleMentionInputValueChange,
@@ -763,6 +764,19 @@ const Property = ({
required={required}
showInputTypeSwitchButton={showInputTypeSwitchButton}
title={type}
+ trailingAction={
+ // Chrome's has no native clear button.
+ controlType === 'TIME' && inputValue && !hidden ? (
+
+ ) : undefined
+ }
type={hidden ? 'hidden' : getInputHTMLType(controlType)}
value={inputValue}
/>
diff --git a/client/src/pages/platform/workflow-editor/components/properties/components/property-input/PropertyInput.test.tsx b/client/src/pages/platform/workflow-editor/components/properties/components/property-input/PropertyInput.test.tsx
index c4bf3f0c2fd..9baecde236a 100644
--- a/client/src/pages/platform/workflow-editor/components/properties/components/property-input/PropertyInput.test.tsx
+++ b/client/src/pages/platform/workflow-editor/components/properties/components/property-input/PropertyInput.test.tsx
@@ -1,5 +1,6 @@
import '@testing-library/jest-dom';
-import {render, screen, userEvent} from '@/shared/util/test-utils';
+import {render, screen, userEvent, waitFor} from '@/shared/util/test-utils';
+import {useRef, useState} from 'react';
import {describe, expect, it, vi} from 'vitest';
import PropertyInput from './PropertyInput';
@@ -102,6 +103,29 @@ describe('PropertyInput', async () => {
expect(screen.getByTestId('trailing-action')).toBeInTheDocument();
});
+ it('invokes trailingAction click handler (covers the TIME clear button pattern from #4768)', async () => {
+ const handleClear = vi.fn();
+
+ render(
+
+ X
+
+ }
+ type="time"
+ value="12:30"
+ />
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: /clear time/i}));
+
+ expect(handleClear).toHaveBeenCalledTimes(1);
+ });
+
it('strips leading = from display value on input change when expressionPrefix is true', async () => {
const handleChange = vi.fn();
@@ -147,4 +171,48 @@ describe('PropertyInput', async () => {
expect(input).toHaveAttribute('step', '1');
});
+
+ // Regression for #4768: clearing the TIME field used to call inputRef.focus() synchronously
+ // after setInputValue(''). The focus flipped PropertyInput's isFocused to true inside the same
+ // batch, so the value-sync useEffect skipped, localValue stayed at the old time, and the input
+ // only visually cleared after the next blur. The fix defers focus() via requestAnimationFrame
+ // so value='' lands in a render where isFocused is still false.
+ it('clears the displayed value when the parent sets value to "" and defers focus via rAF', async () => {
+ const Harness = () => {
+ const [value, setValue] = useState('12:30');
+ const inputRef = useRef(null);
+
+ const handleClear = () => {
+ setValue('');
+
+ requestAnimationFrame(() => inputRef.current?.focus());
+ };
+
+ return (
+
+ X
+
+ }
+ type="time"
+ value={value}
+ />
+ );
+ };
+
+ const {container} = render();
+
+ const input = container.querySelector('input[name="time"]') as HTMLInputElement;
+
+ expect(input.value).toBe('12:30');
+
+ await userEvent.click(screen.getByRole('button', {name: /clear time/i}));
+
+ await waitFor(() => expect(input.value).toBe(''));
+ });
});
diff --git a/client/src/pages/platform/workflow-editor/components/properties/hooks/tests/timeFieldClear.test.ts b/client/src/pages/platform/workflow-editor/components/properties/hooks/tests/timeFieldClear.test.ts
new file mode 100644
index 00000000000..981e2cf4741
--- /dev/null
+++ b/client/src/pages/platform/workflow-editor/components/properties/hooks/tests/timeFieldClear.test.ts
@@ -0,0 +1,69 @@
+import {describe, expect, it, vi} from 'vitest';
+
+/**
+ * Exercises the TIME-clearing flow introduced for issue #4768.
+ *
+ * Why: Chrome's does not expose a native clear button,
+ * so the UI must render an explicit X. This test locks in two things:
+ * 1. The TIME clear button is shown only when inputValue is non-empty.
+ * 2. Invoking handleInputClear resets local state and invokes the save callback.
+ *
+ * The empty-string -> null coercion for DATE/DATE_TIME/TIME is covered by
+ * saveInputValueResolvedValue.test.ts to avoid duplicating the resolver logic.
+ */
+
+type ClearStateType = {
+ controlType?: string;
+ hasError: boolean;
+ inputValue: string;
+ latestValue: string;
+};
+
+const shouldShowTimeClearButton = (controlType: string | undefined, inputValue: string): boolean =>
+ controlType === 'TIME' && Boolean(inputValue);
+
+const makeHandleInputClear = (state: ClearStateType, saveInputValue: () => void) => () => {
+ state.inputValue = '';
+ state.hasError = false;
+ state.latestValue = '';
+
+ saveInputValue();
+};
+
+describe('TIME field clear affordance', () => {
+ it('shows the clear button only when a TIME input has a value', () => {
+ expect(shouldShowTimeClearButton('TIME', '12:30')).toBe(true);
+ expect(shouldShowTimeClearButton('TIME', '')).toBe(false);
+ });
+
+ it('does not show the clear button for DATE or DATE_TIME (they have native clear)', () => {
+ expect(shouldShowTimeClearButton('DATE', '2026-04-23')).toBe(false);
+ expect(shouldShowTimeClearButton('DATE_TIME', '2026-04-23T12:30')).toBe(false);
+ });
+
+ it('does not show the clear button for unrelated control types', () => {
+ expect(shouldShowTimeClearButton('TEXT', 'hello')).toBe(false);
+ expect(shouldShowTimeClearButton(undefined, '12:30')).toBe(false);
+ });
+});
+
+describe('handleInputClear', () => {
+ it('resets inputValue, clears error state, and invokes the save callback', () => {
+ const state: ClearStateType = {
+ controlType: 'TIME',
+ hasError: true,
+ inputValue: '12:30',
+ latestValue: '12:30',
+ };
+ const saveInputValue = vi.fn();
+
+ const handleInputClear = makeHandleInputClear(state, saveInputValue);
+
+ handleInputClear();
+
+ expect(state.inputValue).toBe('');
+ expect(state.hasError).toBe(false);
+ expect(state.latestValue).toBe('');
+ expect(saveInputValue).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts
index c15fdcb75e1..ec34d96c879 100644
--- a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts
+++ b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts
@@ -122,6 +122,7 @@ type UsePropertyReturnType = {
handleFromAiClick: ((fromAi: boolean) => void) | undefined;
handleFromAiToggle: (fromAi: boolean, fieldOnChange: (value: string) => void) => void;
handleInputChange: (event: ChangeEvent | ChangeEvent) => void;
+ handleInputClear: () => void;
handleInputTypeSwitchButtonClick: () => void;
handleJsonSchemaBuilderChange: (value?: SchemaRecordType) => void;
handleMentionInputValueChange: (value: string | number) => void;
@@ -601,6 +602,21 @@ export const useProperty = ({
});
}, 600);
+ const handleInputClear = useCallback(() => {
+ setInputValue('');
+ setHasError(false);
+ setErrorMessage('');
+
+ latestValueRef.current = '';
+
+ saveInputValue();
+
+ // Defer focus() so React commits value='' while PropertyInput is still unfocused —
+ // otherwise onFocus flips isFocused=true before the sync effect runs, the clear never
+ // reaches the input's internal localValue, and the stale time stays visible until blur.
+ requestAnimationFrame(() => inputRef.current?.focus());
+ }, [saveInputValue]);
+
const handleCodeEditorChange = useDebouncedCallback((value?: string) => {
if (
!currentComponent ||
@@ -1770,6 +1786,7 @@ export const useProperty = ({
handleFromAiClick: hideFromAi ? undefined : handleFromAiClick,
handleFromAiToggle,
handleInputChange,
+ handleInputClear,
handleInputTypeSwitchButtonClick,
handleJsonSchemaBuilderChange,
handleMentionInputValueChange,