From 742f76e3181e765dedeaea5e8012c80079b3cc18 Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Wed, 29 Jul 2026 20:14:51 +0530
Subject: [PATCH] [BREAKING-CHANGE] Add NOT_RECORDED call recording status
(#23478)
Adds NOT_RECORDED to the CallRecording status select, for meetings where
nothing was captured (bot never admitted, meeting not started, nobody
joined). First part of twentyhq/core-team-issues#2706, split out so
existing workspaces are upgraded before the call-recorder app starts
writing the new status.
- NOT_RECORDED enum value + standard select option
- 2-26 workspace upgrade command adding the option to existing
workspaces (idempotent, same option id as the standard definition)
App-side classification from Recall sub codes comes in a follow-up PR.
---
.../twenty-front/src/generated/graphql.ts | 1 +
.../CalendarEventCallRecorderAvatar.tsx | 14 +-
.../2-26-upgrade-version-command.module.ts | 5 +-
...-recorded-call-recording-status.command.ts | 168 ++++++++++++++++++
...e-call-recording-standard-metadata.spec.ts | 19 ++
...rding-standard-flat-field-metadata.util.ts | 7 +
.../enums/call-recording-status.enum.ts | 1 +
7 files changed, 212 insertions(+), 3 deletions(-)
create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785334800000-add-not-recorded-call-recording-status.command.ts
diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts
index ed12eda83f..b07d44b765 100644
--- a/packages/twenty-front/src/generated/graphql.ts
+++ b/packages/twenty-front/src/generated/graphql.ts
@@ -28,6 +28,7 @@ export enum CallRecordingStatus {
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
JOINING = 'JOINING',
+ NOT_RECORDED = 'NOT_RECORDED',
PROCESSING = 'PROCESSING',
RECORDING = 'RECORDING',
SCHEDULED = 'SCHEDULED'
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx
index 5390b108dd..9fe115d71b 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useId } from 'react';
import { createPortal } from 'react-dom';
+import { assertUnreachable } from 'twenty-shared/utils';
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces';
import { CallRecordingStatus } from '~/generated/graphql';
@@ -12,6 +13,11 @@ const IN_PROGRESS_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [
CallRecordingStatus.PROCESSING,
];
+const UNAVAILABLE_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [
+ CallRecordingStatus.FAILED,
+ CallRecordingStatus.NOT_RECORDED,
+];
+
const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
switch (status) {
case CallRecordingStatus.SCHEDULED:
@@ -26,6 +32,10 @@ const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
return t`Completed`;
case CallRecordingStatus.FAILED:
return t`Failed`;
+ case CallRecordingStatus.NOT_RECORDED:
+ return t`Not recorded`;
+ default:
+ return assertUnreachable(status);
}
};
@@ -44,12 +54,12 @@ export const CalendarEventCallRecorderAvatar = ({
status,
}: CalendarEventCallRecorderAvatarProps) => {
const instanceId = useId();
- const hasFailed = status === CallRecordingStatus.FAILED;
+ const hasNoRecording = UNAVAILABLE_CALL_RECORDING_STATUSES.includes(status);
const tooltipAnchorId = `call-recorder-${instanceId.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
return (
<>
-
+
{
+ const isDryRun = options.dryRun ?? false;
+
+ const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatFieldMetadataMaps',
+ 'flatObjectMetadataMaps',
+ ]);
+
+ const callRecordingObject =
+ flatObjectMetadataMaps.byUniversalIdentifier[
+ CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER
+ ];
+
+ if (!isDefined(callRecordingObject)) {
+ this.logger.log(
+ `CallRecording object metadata does not exist for workspace ${workspaceId}, skipping`,
+ );
+
+ return;
+ }
+
+ const statusField =
+ flatFieldMetadataMaps.byUniversalIdentifier[
+ CALL_RECORDING_STATUS_FIELD_UNIVERSAL_IDENTIFIER
+ ];
+
+ if (!isDefined(statusField)) {
+ this.logger.log(
+ `CallRecording status field metadata does not exist for workspace ${workspaceId}, skipping`,
+ );
+
+ return;
+ }
+
+ if (statusField.type !== FieldMetadataType.SELECT) {
+ throw new Error(
+ `CallRecording status metadata is not a SELECT field for workspace ${workspaceId}`,
+ );
+ }
+
+ const selectStatusField =
+ statusField as FlatFieldMetadata;
+ const currentOptions = selectStatusField.options ?? [];
+ const hasNotRecordedStatus = currentOptions.some(
+ (option) => option.value === CallRecordingStatus.NOT_RECORDED,
+ );
+
+ if (hasNotRecordedStatus) {
+ this.logger.log(
+ `CallRecording status metadata already has NOT_RECORDED for workspace ${workspaceId}, skipping`,
+ );
+
+ return;
+ }
+
+ this.logger.log(
+ `${isDryRun ? '[DRY RUN] ' : ''}Adding NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}`,
+ );
+
+ if (isDryRun) {
+ return;
+ }
+
+ const { twentyStandardFlatApplication } =
+ await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
+ { workspaceId },
+ );
+
+ const nextOptionPosition =
+ currentOptions.reduce(
+ (highestPosition, option) => Math.max(highestPosition, option.position),
+ -1,
+ ) + 1;
+
+ const updatedStatusField: FlatFieldMetadata = {
+ ...selectStatusField,
+ options: [
+ ...currentOptions,
+ { ...NOT_RECORDED_STATUS_OPTION, position: nextOptionPosition },
+ ],
+ updatedAt: new Date().toISOString(),
+ };
+
+ const validateAndBuildResult =
+ await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
+ {
+ isSystemBuild: true,
+ applicationUniversalIdentifier:
+ twentyStandardFlatApplication.universalIdentifier,
+ workspaceId,
+ allFlatEntityOperationByMetadataName: {
+ fieldMetadata: {
+ flatEntityToCreate: [],
+ flatEntityToDelete: [],
+ flatEntityToUpdate: [updatedStatusField],
+ },
+ },
+ },
+ );
+
+ if (validateAndBuildResult.status === 'fail') {
+ throw new Error(
+ `Failed to add NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}: ${JSON.stringify(
+ validateAndBuildResult,
+ null,
+ 2,
+ )}`,
+ );
+ }
+
+ this.logger.log(
+ `Added NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}`,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts
index 29f25245ef..1df454cc97 100644
--- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts
@@ -2,8 +2,10 @@ import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
+import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
+import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
@@ -36,6 +38,23 @@ describe('CallRecording standard metadata build', () => {
expect(callRecording?.isSystem).toBe(true);
});
+ it('offers the full recording lifecycle as status options', () => {
+ const statusField = allFlatEntityMaps.flatFieldMetadataMaps
+ .byUniversalIdentifier[
+ STANDARD_OBJECTS.callRecording.fields.status.universalIdentifier
+ ] as FlatFieldMetadata | undefined;
+
+ expect(statusField?.options?.map((option) => option.value)).toEqual([
+ 'SCHEDULED',
+ 'JOINING',
+ 'RECORDING',
+ 'PROCESSING',
+ 'COMPLETED',
+ 'FAILED',
+ 'NOT_RECORDED',
+ ]);
+ });
+
it('links callRecording to a calendarEvent through a direct relation', () => {
const calendarEventField =
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts
index a6728f6065..7a4f5f6554 100644
--- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts
@@ -178,6 +178,13 @@ export const buildCallRecordingStandardFlatFieldMetadatas = ({
position: 5,
color: 'gray',
},
+ {
+ id: 'cbd14df8-9cc2-4399-92f5-31fc41f3768b',
+ value: CallRecordingStatus.NOT_RECORDED,
+ label: i18nLabel(msg`Not recorded`),
+ position: 6,
+ color: 'yellow',
+ },
],
},
standardObjectMetadataRelatedEntityIds,
diff --git a/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts b/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts
index cc9cff6af5..c03ff8a141 100644
--- a/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts
+++ b/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts
@@ -5,4 +5,5 @@ export enum CallRecordingStatus {
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
+ NOT_RECORDED = 'NOT_RECORDED',
}