Compute output schema on frontend (#16530)

Fixes https://github.com/twentyhq/core-team-issues/issues/1382

Current issue : all step output schemas are computed and stored on
backend side. Which means that, when the database schema is updated -
like a field creation - steps needs to be deleted an recreated. Which is
invisible to users.

Solution : schema generation is moved on frontend side

1. Coming on the page the first time, the schema is populated for all
steps except a few ones that are handled differently (Code, Webhook,
http node, Agent)

2. A separated state allow to determine if a step needs a recomputation.

3. The user only needs a refresh to see the whole schema re-computed

Follow-up:
- check if remaining backend steps could be moved to runtime
computation. But Code will still require storage.
- Clean backend service that is not used anymore
This commit is contained in:
Thomas Trompette
2025-12-15 16:20:35 +01:00
committed by GitHub
parent 2e104c8e76
commit 95e0793f81
43 changed files with 3152 additions and 209 deletions
@@ -0,0 +1,156 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutputSchemaFamilyStateKey';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { shouldRecomputeOutputSchemaFamilyState } from '@/workflow/workflow-variables/states/shouldRecomputeOutputSchemaFamilyState';
import { stepsOutputSchemaFamilyState } from '@/workflow/workflow-variables/states/stepsOutputSchemaFamilyState';
import {
type OutputSchemaV2,
type StepOutputSchemaV2,
} from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
import {
computeStepOutputSchema,
shouldComputeOutputSchemaOnFrontend,
} from '@/workflow/workflow-variables/utils/generate/computeStepOutputSchema';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
export const useStepsOutputSchema = () => {
const populateStepsOutputSchema = useRecoilCallback(
({ set, snapshot }) =>
(workflowVersion: WorkflowVersion) => {
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue();
workflowVersion.steps?.forEach((step) => {
const stepKey = getStepOutputSchemaFamilyStateKey(
workflowVersion.id,
step.id,
);
const shouldRecompute = snapshot
.getLoadable(shouldRecomputeOutputSchemaFamilyState(stepKey))
.getValue();
const shouldComputeOnFrontend = shouldComputeOutputSchemaOnFrontend(
step.type,
);
if (!shouldRecompute) {
return;
}
const outputSchema = shouldComputeOnFrontend
? computeStepOutputSchema({
step,
objectMetadataItems,
})
: step.settings?.outputSchema;
const stepOutputSchema: StepOutputSchemaV2 = {
id: step.id,
name: step.name,
type: step.type,
icon: getActionIcon(step.type),
outputSchema: (outputSchema ?? {}) as OutputSchemaV2,
};
set(stepsOutputSchemaFamilyState(stepKey), stepOutputSchema);
set(shouldRecomputeOutputSchemaFamilyState(stepKey), false);
});
const trigger = workflowVersion.trigger;
if (isDefined(trigger)) {
const triggerKey = getStepOutputSchemaFamilyStateKey(
workflowVersion.id,
TRIGGER_STEP_ID,
);
const shouldRecompute = snapshot
.getLoadable(shouldRecomputeOutputSchemaFamilyState(triggerKey))
.getValue();
const shouldComputeOnFrontend = shouldComputeOutputSchemaOnFrontend(
trigger.type,
);
if (!shouldRecompute) {
return;
}
const triggerIconKey = getTriggerIcon(trigger);
const outputSchema = shouldComputeOnFrontend
? computeStepOutputSchema({
step: trigger,
objectMetadataItems,
})
: trigger.settings?.outputSchema;
const triggerOutputSchema: StepOutputSchemaV2 = {
id: TRIGGER_STEP_ID,
name: isDefined(trigger.name)
? trigger.name
: getTriggerDefaultLabel(trigger),
type: trigger.type,
icon: triggerIconKey,
outputSchema: (outputSchema ?? {}) as OutputSchemaV2,
};
set(stepsOutputSchemaFamilyState(triggerKey), triggerOutputSchema);
set(shouldRecomputeOutputSchemaFamilyState(triggerKey), false);
}
},
[],
);
const markStepForRecomputation = useRecoilCallback(
({ set }) =>
({
stepId,
workflowVersionId,
}: {
stepId: string;
workflowVersionId: string;
}) => {
const stepKey = getStepOutputSchemaFamilyStateKey(
workflowVersionId,
stepId,
);
set(shouldRecomputeOutputSchemaFamilyState(stepKey), true);
},
[],
);
const deleteStepsOutputSchema = useRecoilCallback(
({ set }) =>
({
stepIds,
workflowVersionId,
}: {
stepIds: string[];
workflowVersionId: string;
}) => {
stepIds.forEach((stepId) => {
const stepKey = getStepOutputSchemaFamilyStateKey(
workflowVersionId,
stepId,
);
set(stepsOutputSchemaFamilyState(stepKey), null);
set(shouldRecomputeOutputSchemaFamilyState(stepKey), true);
});
},
[],
);
return {
populateStepsOutputSchema,
markStepForRecomputation,
deleteStepsOutputSchema,
};
};