Files
twenty/packages/twenty-shared/src/logic-function/build-tool-input-json-schema.ts
T
Raphaël Bosi 3675f264f1 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. -->
2026-06-19 09:10:01 +02:00

82 lines
2.2 KiB
TypeScript

import { isNonEmptyString, isObject } from '@sniptt/guards';
import { type InputJsonSchema } from '@/logic-function/input-json-schema.type';
import { isRecordObjectSchema } from '@/logic-function/is-record-object-schema';
import { isDefined } from '@/utils/validation/isDefined';
type ResolveObjectLabel = (
objectUniversalIdentifier: string,
) => string | undefined;
const buildRecordIdDescription = (
objectUniversalIdentifier: string | undefined,
resolveObjectLabel?: ResolveObjectLabel,
): string => {
const objectLabel = isNonEmptyString(objectUniversalIdentifier)
? resolveObjectLabel?.(objectUniversalIdentifier)
: undefined;
return `Id of the ${isNonEmptyString(objectLabel) ? objectLabel : 'linked'} record`;
};
export const buildToolInputJsonSchema = (
jsonSchema: InputJsonSchema,
resolveObjectLabel?: ResolveObjectLabel,
): InputJsonSchema => {
if (isRecordObjectSchema(jsonSchema)) {
return {
type: 'string',
description: buildRecordIdDescription(
jsonSchema.objectUniversalIdentifier,
resolveObjectLabel,
),
};
}
if (jsonSchema.type === 'records') {
return {
type: 'array',
items: {
type: 'string',
description: buildRecordIdDescription(
jsonSchema.objectUniversalIdentifier,
resolveObjectLabel,
),
},
};
}
const {
objectUniversalIdentifier: _objectUniversalIdentifier,
multiline: _multiline,
label: _label,
items,
properties,
additionalProperties,
...standardKeywords
} = jsonSchema;
const toolSchema: InputJsonSchema = { ...standardKeywords };
if (isDefined(items)) {
toolSchema.items = buildToolInputJsonSchema(items, resolveObjectLabel);
}
if (isDefined(properties)) {
toolSchema.properties = Object.fromEntries(
Object.entries(properties).map(([key, value]) => [
key,
buildToolInputJsonSchema(value, resolveObjectLabel),
]),
);
}
if (isDefined(additionalProperties)) {
toolSchema.additionalProperties = isObject(additionalProperties)
? buildToolInputJsonSchema(additionalProperties, resolveObjectLabel)
: additionalProperties;
}
return toolSchema;
};