feat(workflow): variable pickers for Search Records limit, offset and date filters (#23696)

## Summary

<img width="491" height="390" alt="Capture d’écran 2026-08-03 à 11 30
40"
src="https://github.com/user-attachments/assets/6db59b8a-3e8e-41b8-80b3-c736e56b7f7f"
/>

Adds workflow variable pickers to the **Search Records** action for
fields that previously only accepted static values:

- **Limit** and **Offset** number inputs now expose the
`WorkflowVariablePicker`, so they can be bound to a variable from a
previous step. The stored value can be a standalone variable string; the
backend coerces the resolved value back to a number.
- **Date filters** using the `Is before` (`IS_BEFORE`) and `Is after or
equal` (`IS_AFTER`) operands now expose the variable picker in the
advanced filter side panel (previously disabled for all date filters).

The backend already resolves these inputs via `resolveInput`; the only
backend change is a small numeric coercion of the resolved limit/offset.

## Changes

- `WorkflowEditActionFindRecords.tsx` — pass `WorkflowVariablePicker` to
the Limit/Offset inputs; make `onChange` and form state variable-aware
(`number | string`).
- `AdvancedFilterSidePanelValueFormInput.tsx` — enable the date
`VariablePicker` only for `IS_BEFORE` / `IS_AFTER`.
- `useGetRecordFilterDisplayValue.ts` — return the raw variable for a
standalone `{{variable}}` value so date filters don't crash
`Temporal.*.from`.
- `find-records-action-settings-schema.ts` — allow a string (variable)
for `limit` / `offset`.
- `find-records.workflow-action.ts` — coerce resolved `limit` / `offset`
to numbers before querying.

## Testing

Built a workflow locally (Manual trigger → Code step returning `{ limit:
2, offset: 1, sinceDate }` → Search Records) with all three fields bound
to those variables. The run completed successfully; the Search Records
step returned exactly 2 records (limit applied) filtered by `createdAt
>= sinceDate`, confirming the backend resolves each variable.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23696?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas Trompette
2026-08-03 14:32:06 +02:00
committed by GitHub
parent 9e25121616
commit e81fdbcc7a
11 changed files with 128 additions and 26 deletions
@@ -33,6 +33,8 @@ import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-a
import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect';
import { WorkflowFindRecordsSorts } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts';
import { WorkflowObjectDropdownContent } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowObjectDropdownContent';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledLabel = styled.span`
@@ -65,8 +67,8 @@ type FindRecordsFormData = {
objectNameSingular: string;
filter?: FindRecordsActionFilter;
orderBy?: FindRecordsActionOrderBy;
limit?: number;
offset?: number;
limit?: number | string;
offset?: number | string;
};
export type FindRecordsActionFilter = {
@@ -96,12 +98,16 @@ export const WorkflowEditActionFindRecords = ({
const [formData, setFormData] = useState<FindRecordsFormData>(() => ({
objectNameSingular: action.settings.input.objectName,
limit:
isNumber(action.settings.input.limit) &&
action.settings.input.limit > QUERY_MAX_RECORDS
? QUERY_MAX_RECORDS
: (action.settings.input.limit ?? 1),
offset: Math.max(0, Math.floor(action.settings.input.offset ?? 0)),
limit: isNumber(action.settings.input.limit)
? Math.min(action.settings.input.limit, QUERY_MAX_RECORDS)
: isStandaloneVariableString(action.settings.input.limit)
? action.settings.input.limit
: 1,
offset: isNumber(action.settings.input.offset)
? Math.max(0, Math.floor(action.settings.input.offset))
: isStandaloneVariableString(action.settings.input.offset)
? action.settings.input.offset
: 0,
filter: action.settings.input.filter as FindRecordsActionFilter,
orderBy: action.settings.input.orderBy as FindRecordsActionOrderBy,
}));
@@ -155,7 +161,9 @@ export const WorkflowEditActionFindRecords = ({
input: {
objectName: updatedObjectName,
limit: updatedLimit ?? 1,
offset: Math.max(0, Math.floor(updatedOffset ?? 0)),
offset: isNumber(updatedOffset)
? Math.max(0, Math.floor(updatedOffset))
: (updatedOffset ?? 0),
filter: updatedFilter,
orderBy: updatedOrderBy as Record<string, any[]> | undefined,
},
@@ -312,12 +320,26 @@ export const WorkflowEditActionFindRecords = ({
readonly={isFormDisabled}
hint={t`This action can return up to ${maxRecordsFormatted} records.`}
error={limitError}
VariablePicker={WorkflowVariablePicker}
onChange={(limit) => {
if (isFormDisabled === true || !isNumber(limit)) {
if (isFormDisabled === true) {
return;
}
const normalizedLimit = Math.floor(limit);
if (isStandaloneVariableString(limit)) {
setLimitError(undefined);
const newFormData: FindRecordsFormData = {
...formData,
limit,
};
setFormData(newFormData);
saveAction(newFormData);
return;
}
const normalizedLimit = isNumber(limit) ? Math.floor(limit) : 1;
if (normalizedLimit <= 0) {
setLimitError(t`Limit must be greater than 0.`);
@@ -350,12 +372,26 @@ export const WorkflowEditActionFindRecords = ({
readonly={isFormDisabled}
hint={t`Number of records to skip. Combine with Limit to page through results.`}
error={offsetError}
VariablePicker={WorkflowVariablePicker}
onChange={(offset) => {
if (isFormDisabled === true || !isNumber(offset)) {
if (isFormDisabled === true) {
return;
}
const normalizedOffset = Math.floor(offset);
if (isStandaloneVariableString(offset)) {
setOffsetError(undefined);
const newFormData: FindRecordsFormData = {
...formData,
offset,
};
setFormData(newFormData);
saveAction(newFormData);
return;
}
const normalizedOffset = isNumber(offset) ? Math.floor(offset) : 0;
if (normalizedOffset < 0) {
setOffsetError(t`Offset cannot be negative.`);