feat(workflow): add offset to Find Records node for pagination (#21484)
<img width="471" height="362" alt="Capture d’écran 2026-06-12 à 15 38 45" src="https://github.com/user-attachments/assets/9656d3a6-6f56-4587-add6-55c0a0a32482" /> ## Summary The workflow Find Records (search) node previously exposed only `objectName`, `filter`, `sort`, and `limit` (capped at `QUERY_MAX_RECORDS` = 200), with no way to page beyond the first page of results. This adds an optional **Offset** to the node so a workflow can fetch an arbitrary page (`offset = pageIndex * limit`) while keeping the same filter and sort. The underlying `FindRecordsService` already accepts `offset` (it forwards it to the query runner's `skip`, and stabilizes ordering with an `id` tiebreaker), so this change just threads `offset` through the remaining layers: - `workflowFindRecordsActionSettingsSchema` (shared zod schema) — new optional `offset` - `FindRecordsInput` type — new optional `offset?: number` - `find-records.workflow-action.ts` — forwards `offset` to `FindRecordsService.execute` - `WorkflowEditActionFindRecords.tsx` — new "Offset" number input (non-negative, defaults to 0) with form state + persistence - Default `FIND_RECORDS` step settings — `offset: 0` ### Notes / non-goals - Offset-only, single page: the node returns one page. Looping over all pages inside one run is not included (the Iterator action loops a static array and cannot re-query). The node output already returns `totalCount`, so a workflow can compute total pages as `ceil(totalCount / limit)`. - Offset on very large/changing datasets can be slow or skip/duplicate rows; cursor/keyset pagination would be a future follow-up. ## Test plan - [x] Create a Find Records node, set Limit=50, Offset=0 → returns first page - [x] Set Offset=50 with the same filter/sort → returns the second page (no overlap) - [x] Negative offset shows a validation error and is not saved - [x] Existing Find Records nodes (no offset stored) still run, defaulting to offset 0 - [x] Typecheck/lint pass in CI <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21484?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:
+38
@@ -66,6 +66,7 @@ type FindRecordsFormData = {
|
||||
filter?: FindRecordsActionFilter;
|
||||
orderBy?: FindRecordsActionOrderBy;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type FindRecordsActionFilter = {
|
||||
@@ -100,11 +101,13 @@ export const WorkflowEditActionFindRecords = ({
|
||||
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)),
|
||||
filter: action.settings.input.filter as FindRecordsActionFilter,
|
||||
orderBy: action.settings.input.orderBy as FindRecordsActionOrderBy,
|
||||
}));
|
||||
|
||||
const [limitError, setLimitError] = useState<string | undefined>(undefined);
|
||||
const [offsetError, setOffsetError] = useState<string | undefined>(undefined);
|
||||
const isFormDisabled = actionOptions.readonly ?? false;
|
||||
const instanceId = `workflow-edit-action-record-find-records-${action.id}-${formData.objectNameSingular}`;
|
||||
|
||||
@@ -140,6 +143,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
const {
|
||||
objectNameSingular: updatedObjectName,
|
||||
limit: updatedLimit,
|
||||
offset: updatedOffset,
|
||||
filter: updatedFilter,
|
||||
orderBy: updatedOrderBy,
|
||||
} = formData;
|
||||
@@ -151,6 +155,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
input: {
|
||||
objectName: updatedObjectName,
|
||||
limit: updatedLimit ?? 1,
|
||||
offset: Math.max(0, Math.floor(updatedOffset ?? 0)),
|
||||
filter: updatedFilter,
|
||||
orderBy: updatedOrderBy as Record<string, any[]> | undefined,
|
||||
},
|
||||
@@ -174,6 +179,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
const newFormData: FindRecordsFormData = {
|
||||
objectNameSingular: value,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
@@ -335,6 +341,38 @@ export const WorkflowEditActionFindRecords = ({
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormNumberFieldInput
|
||||
label={t`Offset`}
|
||||
defaultValue={formData.offset}
|
||||
placeholder={t`Enter offset`}
|
||||
readonly={isFormDisabled}
|
||||
hint={t`Number of records to skip. Combine with Limit to page through results.`}
|
||||
error={offsetError}
|
||||
onChange={(offset) => {
|
||||
if (isFormDisabled === true || !isNumber(offset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedOffset = Math.floor(offset);
|
||||
|
||||
if (normalizedOffset < 0) {
|
||||
setOffsetError(t`Offset cannot be negative.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setOffsetError(undefined);
|
||||
|
||||
const newFormData: FindRecordsFormData = {
|
||||
...formData,
|
||||
offset: normalizedOffset,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export type FindRecordsInput = {
|
||||
gqlOperationOrderBy?: Partial<ObjectRecordOrderBy>;
|
||||
};
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type UpsertRecordInput = {
|
||||
|
||||
+1
@@ -434,6 +434,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
input: {
|
||||
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
@@ -95,6 +95,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
filter: gqlOperationFilter,
|
||||
orderBy: workflowActionInput.orderBy?.gqlOperationOrderBy,
|
||||
limit: workflowActionInput.limit,
|
||||
offset: workflowActionInput.offset,
|
||||
authContext: executionContext.authContext,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
shouldBuildEffectiveSelectFields: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ export const workflowFindRecordsActionSettingsSchema =
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().int().nonnegative().optional(),
|
||||
filter: z
|
||||
.object({
|
||||
recordFilterGroups: z.array(z.any()).optional(),
|
||||
|
||||
Reference in New Issue
Block a user