## 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. -->
Adds an optional label field to logic-function input schema properties
(InputSchemaProperty and InputJsonSchema). When set, the workflow
builder renders the label in place of the raw property key for both leaf
inputs and nested sections; when unset, it falls back to the key.
jsonSchemaToInputSchema propagates the label so app authors can declare
it in their JSON schema. Payload paths, the variable picker, and saved
workflow inputs continue to use the property key — labels are
display-only.
## Summary
Replaces the bolted-on `isTool` + `toolInputSchema` fields on
`LogicFunctionManifest` with two distinct, opt-in triggers that align
with the existing `cron` / `databaseEvent` / `httpRoute` trigger
pattern:
- **`toolTriggerSettings`** — exposes the function as an AI tool (chat /
MCP / function calling). Uses standard JSON Schema (the format LLMs
natively understand).
- **`workflowActionTriggerSettings`** — exposes the function as a step
in the visual workflow builder. Uses Twenty's rich `InputSchema` so the
builder can render proper `FieldMetadataType`-aware editors, variable
pickers, labels, and an optional `outputSchema`.
A function can opt into none, one, or both. Each surface gets the schema
format appropriate for it.
### Why
`isTool: true` previously exposed the function as both an AI tool AND a
workflow node, with the same JSON Schema feeding both — but the workflow
builder really wants Twenty's `InputSchema` (with `CURRENCY`,
`RELATION`, `EMAILS`, etc.) and the AI surface really wants standard
JSON Schema. Today the workflow builder hacks around this by treating
JSON Schema as `InputSchema`, which silently breaks for any
non-primitive field type. Splitting the triggers fixes that and lets
each surface evolve independently.
### Migration
- **Fast** instance command adds the two new nullable columns.
- **Slow** instance command backfills `toolTriggerSettings` +
`workflowActionTriggerSettings` from `isTool=true` rows (preserving
today's both-surfaces behaviour) then drops the legacy columns.
### Stacked
Stacked on top of #20181. Merge that first, then this.
## Test plan
- [ ] CI green (oxlint, typecheck, jest, vitest)
- [ ] Run `--include-slow` upgrade against a workspace with existing
`isTool=true` logic functions; verify both new columns populated and old
columns dropped
- [ ] Verify AI chat sees migrated tool functions (Linear create-issue,
Exa search) and can call them with the JSON Schema
- [ ] Add an AI-tool function from the Settings UI (toggles
`toolTriggerSettings`) and verify it shows up in chat
- [ ] Add a workflow-action function from the Settings UI (toggles
`workflowActionTriggerSettings`) and verify it appears in the workflow
node picker
- [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify
input fields render (no more JSON-Schema-as-InputSchema hack)
- [ ] Try defining a function with no triggers in the SDK and verify
`defineLogicFunction` rejects it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>