## 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. -->
2.6 KiB
Logic function inputs
When a logic function opts into the workflow action or AI tool surface but does
not declare an explicit inputSchema, the SDK infers one from the handler's
parameter type during the manifest build. The workflow builder uses that schema
to render an input form, and record-typed inputs render as record pickers.
How inference works
Inference reads the handler's single params object type and maps each property:
string/number/booleanmap to the matching scalar input.- String literal unions (
'a' | 'b') map to a select input. T[]/Array<T>map to array inputs.TwentyRecord<'objectUniversalIdentifier'>maps to a record input (see below).
Inference runs only when the trigger settings omit inputSchema. Providing an
explicit inputSchema disables inference for that surface entirely — this is the
escape hatch when a handler type cannot be expressed inline.
Record-typed inputs
To bind an input to a workspace object, type it with TwentyRecord, passing the
object's universal identifier as a string literal:
import { defineLogicFunction, type TwentyRecord } from 'twenty-sdk/define';
const handler = async (params: {
companyId: TwentyRecord<'20202020-b374-4779-a561-80086cb2e17f'>;
postCardIds: TwentyRecord<'54b589ca-eeed-4950-a176-358418b85c05'>[];
}) => {
return {
companyId: params.companyId,
postCardCount: params.postCardIds.length,
};
};
The universal identifier is the source of truth and is read directly from the literal — there is no name matching, so an unrelated type can never be mistaken for a record.
- Standard objects: get the identifier from
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS(exported fromtwenty-sdk/define), e.g.STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier. - App objects: use the
universalIdentifieryou set on the object'sdefineObject(...).
Only a string-literal argument resolves. TwentyRecord with no argument, or with
a non-literal argument, is treated as an unknown input.
What the handler receives
TwentyRecord<TUid> is a branded string: companyId is a record id, and
postCardIds is an array of record ids. This matches what the runtime delivers —
the workflow action passes the selected record ids (or the value a bound
{{variable}} resolves to) straight to the handler. Handlers must therefore
accept ids. The People Data Labs functions model this:
export type RecordInput = string | { id?: string | null };
and normalize the input with an extractRecordIds helper before use. If a
handler needs full records, it fetches them by id with the Core API client.