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:
@@ -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 = {
|
||||
|
||||
+52
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user