Infer record pickers for record-typed logic function workflow inputs (#21494)

## Context

Logic functions can declare workflow inputs typed as records or arrays
of records (e.g. the People Data Labs enrichment functions), but the
workflow builder rendered those as a plain text input with a variable
picker, which is not usable.

## What this does

- Adds an `objectUniversalIdentifier` link on input schema properties,
so a record-typed input is tied to a workspace object.
- The SDK build infers it from a
`TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler
signature, reading the object's universal identifier straight from the
source; explicit input schemas can still set the field directly.
- The workflow builder renders these inputs as a single record picker or
a record multi-select with the variable picker on the right. Selected
records are stored as record ids; `TwentyRecord<UID>` is a branded
`string`, so the handler signature reflects that it receives ids (a
bound variable resolves to whatever the referenced step produced).
- The multi-select collapses overflowing chips into a `+N` badge
(reusing `ExpandableList`) and its variable picker offers both record
objects and fields.
- Updates the People Data Labs enrichment inputs as the reference
implementation.

<img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x"
src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?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:
Raphaël Bosi
2026-06-19 09:10:01 +02:00
committed by GitHub
parent 9eb90e72f9
commit 3675f264f1
47 changed files with 1861 additions and 168 deletions
@@ -94,6 +94,7 @@ export { workflowTriggerSchema } from './schemas/workflow-trigger-schema';
export type { EmailRecipients } from './types/EmailRecipients';
export type { FunctionInput } from './types/FunctionInput';
export type {
RecordSchemaType,
InputSchemaPropertyType,
InputSchemaProperty,
InputSchema,
@@ -1,7 +1,13 @@
import { type FieldMetadataType } from '@/types';
import { type LeafType, type NodeType } from '@/workflow';
export type InputSchemaPropertyType = LeafType | NodeType | FieldMetadataType;
export type RecordSchemaType = 'record' | 'records';
export type InputSchemaPropertyType =
| LeafType
| NodeType
| RecordSchemaType
| FieldMetadataType;
export type InputSchemaProperty = {
type: InputSchemaPropertyType;
@@ -10,6 +16,7 @@ export type InputSchemaProperty = {
properties?: Properties;
multiline?: boolean;
label?: string;
objectUniversalIdentifier?: string;
};
type Properties = {
@@ -56,4 +56,56 @@ describe('getDefaultFunctionInputFromInputSchema', () => {
{ briefs: [] },
]);
});
it('should init record-typed inputs with null and record arrays with empty arrays', () => {
const inputSchema = [
{
type: 'object',
properties: {
company: {
type: 'object',
objectUniversalIdentifier: 'company-universal-identifier',
},
people: {
type: 'array',
items: {
type: 'object',
objectUniversalIdentifier: 'person-universal-identifier',
},
},
plainObject: { type: 'object' },
},
},
] as InputSchema;
expect(getFunctionInputFromInputSchema(inputSchema)).toEqual([
{
company: null,
people: [],
plainObject: {},
},
]);
});
it('should init record/records types with null and empty array', () => {
const inputSchema = [
{
type: 'object',
properties: {
company: {
type: 'record',
objectUniversalIdentifier: 'company-universal-identifier',
},
people: {
type: 'records',
objectUniversalIdentifier: 'person-universal-identifier',
},
},
},
] as InputSchema;
expect(getFunctionInputFromInputSchema(inputSchema)).toEqual([
{ company: null, people: [] },
]);
});
});
@@ -1,11 +1,20 @@
import { type InputSchema, type FunctionInput } from '@/workflow';
import { type InputJsonSchema } from '@/logic-function';
import { isRecordObjectSchema } from '@/logic-function/is-record-object-schema';
import { isDefined } from '@/utils';
export const getFunctionInputFromInputSchema = (
inputSchema: InputSchema | InputJsonSchema[],
): FunctionInput => {
return inputSchema.map((param) => {
if (isRecordObjectSchema(param)) {
return null;
}
if (param.type === 'records') {
return [];
}
if (
isDefined(param.type) &&
['string', 'number', 'boolean'].includes(param.type)