feat(workflow): surface manual-trigger payload + metadata in variable picker (#21692)

## Summary

Step 2 of the manual-trigger output schema restructuring (expand →
display → migrate → contract).

Builds on the now-merged #21676 (which expanded the runtime payload to
serve `payload` and `metadata` siblings at the trigger root). This PR
**surfaces** those in the variable picker as nested, expandable nodes:

- `trigger.payload.{record fields}` — the record(s) that triggered the
run
- `trigger.metadata.workspaceMemberId` — who triggered it

The flat root fields (`trigger.id`, etc.) remain available, so existing
saved variable references keep working until a later migration phase
moves them.

### Changes
- **twenty-shared**: metadata/payload label constants +
`build-manual-trigger-metadata-node` util + barrel exports.
- **twenty-front**: `computeStepOutputSchema` MANUAL branch now nests
`payload` (RecordNode for SINGLE_RECORD, array Node for BULK_RECORDS,
omitted for GLOBAL) and `metadata`; `ManualTriggerOutputSchema` type
updated to `{ payload?; metadata }`.
- **twenty-server**: `computeTriggerOutputSchemaFromAvailability`
mirrors the same nested shape for server-side validation.

The key is `metadata` (not `_metadata`) — custom fields can't start with
`_`, so collision risk was deemed acceptable.

## Test plan
- [x] `npx nx build twenty-shared`
- [x] `computeStepOutputSchema` unit tests pass (55)
- [x] Manual: create a manual-trigger workflow (GLOBAL / single-record /
bulk), confirm the picker shows `payload` and `metadata` as expandable
folders and that selecting a field yields `{{trigger.payload.<field>}}`
/ `{{trigger.metadata.workspaceMemberId}}`

> Note: server typecheck has pre-existing unrelated failures on main
(Stripe billing mocks, gmail mocks); none touch workflow files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21692?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:
Thomas Trompette
2026-06-17 17:54:35 +02:00
committed by GitHub
parent 607d9ee6e5
commit 105f9565a5
19 changed files with 629 additions and 50 deletions
@@ -4,12 +4,16 @@ import { isString } from '@sniptt/guards';
import { isDefined, isValidVariable } from 'twenty-shared/utils';
import {
BaseOutputSchemaV2,
buildManualTriggerMetadataNode,
BulkRecordsAvailability,
extractRawVariableNamePart,
GlobalAvailability,
navigateOutputSchemaProperty,
SingleRecordAvailability,
TRIGGER_STEP_ID,
WORKFLOW_TRIGGER_METADATA_KEY,
WORKFLOW_TRIGGER_PAYLOAD_KEY,
WORKFLOW_TRIGGER_PAYLOAD_LABEL,
WorkflowActionType,
} from 'twenty-shared/workflow';
@@ -291,14 +295,28 @@ export class WorkflowSchemaWorkspaceService {
workspaceId: string;
}): Promise<OutputSchema> {
if (availability.type === 'GLOBAL') {
return {};
return {
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
};
}
if (availability.type === 'SINGLE_RECORD') {
return this.computeRecordOutputSchema({
const recordOutputSchema = await this.computeRecordOutputSchema({
objectType: availability.objectNameSingular,
workspaceId,
});
const payload: Node = {
isLeaf: false,
type: 'object',
label: WORKFLOW_TRIGGER_PAYLOAD_LABEL,
value: recordOutputSchema,
};
return {
[WORKFLOW_TRIGGER_PAYLOAD_KEY]: payload,
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
};
}
if (availability.type === 'BULK_RECORDS') {
@@ -308,15 +326,25 @@ export class WorkflowSchemaWorkspaceService {
workspaceId,
);
return {
[objectMetadataInfo.flatObjectMetadata.namePlural]: {
label: objectMetadataInfo.flatObjectMetadata.labelPlural,
isLeaf: true,
type: 'array',
value:
'Array of ' + objectMetadataInfo.flatObjectMetadata.labelPlural,
const payload: Node = {
isLeaf: false,
type: 'object',
label: WORKFLOW_TRIGGER_PAYLOAD_LABEL,
value: {
[objectMetadataInfo.flatObjectMetadata.namePlural]: {
label: objectMetadataInfo.flatObjectMetadata.labelPlural,
isLeaf: true,
type: 'array',
value:
'Array of ' + objectMetadataInfo.flatObjectMetadata.labelPlural,
},
},
};
return {
[WORKFLOW_TRIGGER_PAYLOAD_KEY]: payload,
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
};
}
return {};