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
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
import {
buildToolInputJsonSchema,
DEFAULT_TOOL_INPUT_SCHEMA,
} from 'twenty-shared/logic-function';
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
@@ -46,14 +49,18 @@ export class LogicFunctionToolProvider implements ToolProvider {
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
const includeSchemas = options?.includeSchemas ?? true;
const { flatLogicFunctionMaps } =
const { flatLogicFunctionMaps, flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: context.workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
flatMapsKeys: ['flatLogicFunctionMaps', 'flatObjectMetadataMaps'],
},
);
const resolveObjectLabel = (objectUniversalIdentifier: string) =>
flatObjectMetadataMaps.byUniversalIdentifier[objectUniversalIdentifier]
?.labelSingular;
const logicFunctionsWithSchema = Object.values(
flatLogicFunctionMaps.byUniversalIdentifier,
).filter(
@@ -83,9 +90,12 @@ export class LogicFunctionToolProvider implements ToolProvider {
if (includeSchemas) {
descriptors.push({
...base,
inputSchema:
(logicFunction.toolTriggerSettings?.inputSchema as object) ??
DEFAULT_TOOL_INPUT_SCHEMA,
inputSchema: isDefined(logicFunction.toolTriggerSettings?.inputSchema)
? (buildToolInputJsonSchema(
logicFunction.toolTriggerSettings.inputSchema,
resolveObjectLabel,
) as object)
: DEFAULT_TOOL_INPUT_SCHEMA,
});
} else {
descriptors.push(base);