Skip to content

Support morph relations in workflow record nodes#21403

Open
ijreilly wants to merge 3 commits into
mainfrom
ft--morph-in-workflows
Open

Support morph relations in workflow record nodes#21403
ijreilly wants to merge 3 commits into
mainfrom
ft--morph-in-workflows

Conversation

@ijreilly

@ijreilly ijreilly commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Support morph (polymorphic) relations in workflow record nodes

Morph relations (e.g. a polymorphic Owner on Pet targeting Person or Company) were not selectable in the workflow Create / Update / Upsert Record nodes. This PR adds full support for setting them.

What changed

Frontend

  • shouldDisplayFormField: allow MORPH_RELATION (many-to-one) so morph fields appear in record forms.
  • New FormMorphRelationToOneFieldInput: a polymorphic record picker across the morph's target objects, storing a self-describing value { targetObjectMetadataId, id }.
  • Wired the morph branch into FormFieldInput.

Backend

  • New formatWorkflowRecordMorphRelationFields util: resolves the form value (stored under the base field name, e.g. owner) into the correct per-target join column (ownerCompanyId), nulling siblings to keep exactly one target referenced.
  • Wired into the create / update / upsert workflow actions (update also expands fieldsToUpdate to the concrete join columns).

Permissions handling

  • The picker's search is scoped to only the morph targets the user can read (canReadObjectRecords), so it no longer breaks when a target object is inaccessible.
  • If an existing value points to an object the user can't read, the field shows the reused "Not shared" lock display instead of an empty field, while remaining editable when other targets are readable.

Notes

  • No data schema / migration changes — reuses the existing per-target morph columns and stores the selection in the existing workflow step JSON settings.
Screenshot 2026-06-10 at 14 57 40

Also handles the case where the selected record is not readable
image

@twenty-ci-bot-public

Copy link
Copy Markdown

👋 Thanks for contributing to Twenty!

Your PR has been set to draft while you work on it. Once you're done, mark it as Ready for review and our automated checks will run.

Looking forward to your contribution!

@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
@UseFilters(
SearchApiExceptionFilter,
PermissionsGraphqlApiExceptionFilter,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so that permission errors in search do not end up at 500 anymore

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx:250">
P2: Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.</violation>
</file>

<file name="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts">

<violation number="1" location="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts:117">
P1: Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.</violation>
</file>

<file name="packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx:150">
P2: Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

(targetJoinColumn) => targetJoinColumn.joinColumnName,
);

for (const { joinColumnName } of targetJoinColumns) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts, line 117:

<comment>Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.</comment>

<file context>
@@ -0,0 +1,139 @@
+      (targetJoinColumn) => targetJoinColumn.joinColumnName,
+    );
+
+    for (const { joinColumnName } of targetJoinColumns) {
+      formattedRecord[joinColumnName] = null;
+    }
</file context>

readonly={readonly}
/>
) : isFieldMorphRelationManyToOne(field) ? (
<FormMorphRelationToOneFieldInput

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx, line 250:

<comment>Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.</comment>

<file context>
@@ -241,6 +246,15 @@ export const FormFieldInput = ({
       readonly={readonly}
     />
+  ) : isFieldMorphRelationManyToOne(field) ? (
+    <FormMorphRelationToOneFieldInput
+      label={field.label}
+      morphRelations={field.metadata.morphRelations}
</file context>

return;
}

if (recordId === selectedMorphItem.recordId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx, line 150:

<comment>Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.</comment>

<file context>
@@ -0,0 +1,252 @@
+      return;
+    }
+
+    if (recordId === selectedMorphItem.recordId) {
+      onClear?.();
+    } else {
</file context>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end support for selecting and persisting polymorphic (morph) many-to-one relations in workflow Create/Update/Upsert Record nodes. It introduces a new frontend morph relation form input and a backend formatter that maps the user-facing morph field value to the correct concrete join column(s).

Changes:

  • Frontend: allow morph relation fields to appear in workflow record forms and add a polymorphic record picker input.
  • Backend: add a formatter that converts { targetObjectMetadataId, id } into the correct per-target join column while nulling sibling columns.
  • Backend: update workflow record CRUD actions to run the morph formatter before existing relation formatting; add a permissions exception filter to search resolver.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts Runs morph formatter prior to relation formatting for upsert inputs.
packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts Adds morph formatting and expands fieldsToUpdate to concrete morph join columns.
packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts Runs morph formatter prior to relation formatting for create inputs.
packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts New utility to map user-facing morph values to concrete join columns.
packages/twenty-server/src/engine/core-modules/search/search.resolver.ts Adds permissions exception filter to Search resolver filter chain.
packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts Allows MORPH_RELATION fields (many-to-one only) to be displayed in workflow record forms.
packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx New polymorphic record picker input for morph many-to-one fields.
packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx Wires morph relation fields to the new morph input component.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +115
const selectedObjectNameSingular =
selectedObjectMetadataItem?.nameSingular ?? readableObjectNameSingulars[0];

const { record: selectedRecord } = useFindOneRecord({
objectRecordId:
isDefined(recordId) && isValidUuid(recordId) ? recordId : '',
objectNameSingular: selectedObjectNameSingular ?? '',
withSoftDeleted: true,
skip:
!isDefined(recordId) ||
!isValidUuid(recordId) ||
!isDefined(selectedObjectNameSingular) ||
hasForbiddenSelectedRecord,
});
Comment on lines +113 to +135
joinColumnNamesByMorphFieldName[key] = targetJoinColumns.map(
(targetJoinColumn) => targetJoinColumn.joinColumnName,
);

for (const { joinColumnName } of targetJoinColumns) {
formattedRecord[joinColumnName] = null;
}

const morphValue = extractMorphValue(value);

if (!isDefined(morphValue)) {
continue;
}

const matchingTargetJoinColumn = targetJoinColumns.find(
(targetJoinColumn) =>
targetJoinColumn.targetObjectMetadataId ===
morphValue.targetObjectMetadataId,
);

if (isDefined(matchingTargetJoinColumn)) {
formattedRecord[matchingTargetJoinColumn.joinColumnName] = morphValue.id;
}
Comment on lines +249 to +253
) : isFieldMorphRelationManyToOne(field) ? (
<FormMorphRelationToOneFieldInput
label={field.label}
morphRelations={field.metadata.morphRelations}
defaultValue={defaultValue as FormMorphRelationToOneValue}
defaultValue={defaultValue as FormMorphRelationToOneValue}
onClear={onClear}
onChange={onChange}
readonly={readonly}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing VariablePicker, I guess we need variables for those fields?

testId?: string;
};

export const FormMorphRelationToOneFieldInput = ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why that component is so complicated compared to FormRelationToOneFieldInput. I think we should rely more on FormSingleRecordPicker and extend it if needed. Unless I am mistaken and this is not possible?

return null;
};

export const formatWorkflowRecordMorphRelationFields = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably only need one util, formatWorkflowRecordRelationFields. We call both utils together in actions. We should probably merge and test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants