From 1a38152451c6b55d6e8755612b062f2b4cc36d4e Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:33:49 +0100 Subject: [PATCH] Support referencing updatedFields array in databaseEvent triggers (#17240) Closes https://github.com/twentyhq/core-team-issues/issues/1838 DatabaseEventTriggers can now define a field-level granularity on which updates they should react to. After a discussion with the team, we have decided to rely on field names and not UID, just as we do for objects. The rationale is that we want to keep a pleasant devX and consider it's not on twenty to ensure continuity of api usage around an object if their name is updated: for instance if a user has based their webhook on the name of an object, if they decide to update it, they have to take care of updating their webhook. --- .../developers/extend/capabilities/apps.mdx | 7 +- .../components/SettingsDatabaseEventsForm.tsx | 41 +- .../SettingsServerlessFunctionTriggersTab.tsx | 18 +- .../application/application-sync.service.ts | 2 + .../entities/database-event-trigger.entity.ts | 1 + .../call-database-event-trigger-jobs.job.ts | 8 +- ...form-event-batch-to-event-payloads.spec.ts | 357 ++++++++++++++++++ ...transform-event-batch-to-event-payloads.ts | 48 ++- .../serverlessFunctionManifestType.ts | 1 + 9 files changed, 451 insertions(+), 32 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/__tests__/transform-event-batch-to-event-payloads.spec.ts diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index e3154c03d7..cddfbcf2c2 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -491,7 +491,8 @@ export default defineFunction({ { universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156', type: 'databaseEvent', - eventName: 'person.created', + eventName: 'person.updated', + updatedFields: ['name'], }, ], }); @@ -501,8 +502,8 @@ Common trigger types: - **route**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: > e.g. `path: '/post-card/create',` -> call on `/s/post-card/create` - **cron**: Runs your function on a schedule using a CRON expression. -- **databaseEvent**: Runs on workspace object lifecycle events -> e.g. `person.created` +- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated` Notes: - The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions. diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDatabaseEventsForm.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDatabaseEventsForm.tsx index 22eb2b277f..986ad16ca1 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsDatabaseEventsForm.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsDatabaseEventsForm.tsx @@ -1,7 +1,9 @@ -import { t } from '@lingui/core/macro'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { Select } from '@/ui/input/components/Select'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -import { IconButton, type SelectOption } from 'twenty-ui/input'; import { IconBox, IconNorthStar, @@ -9,12 +11,10 @@ import { IconTrash, useIcons, } from 'twenty-ui/display'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import styled from '@emotion/styled'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { IconButton, type SelectOption } from 'twenty-ui/input'; -const OBJECT_DROPDOWN_WIDTH = 340; -const ACTION_DROPDOWN_WIDTH = 140; +const OBJECT_DROPDOWN_WIDTH = 240; +const ACTION_DROPDOWN_WIDTH = 240; const OBJECT_MOBILE_WIDTH = 150; const ACTION_MOBILE_WIDTH = 140; @@ -40,7 +40,7 @@ export const SettingsDatabaseEventsForm = ({ removeOperation, disabled = false, }: { - events: { object: string | null; action: string }[]; + events: { object: string | null; action: string; updatedFields?: string[] }[]; updateOperation?: ( index: number, field: 'object' | 'action', @@ -64,12 +64,23 @@ export const SettingsDatabaseEventsForm = ({ })), ]; - const actionOptions: SelectOption[] = [ - { label: t`All`, value: '*', Icon: IconNorthStar }, - { label: t`Created`, value: 'created', Icon: IconPlus }, - { label: t`Updated`, value: 'updated', Icon: IconBox }, - { label: t`Deleted`, value: 'deleted', Icon: IconTrash }, - ]; + const getActionOptions = ( + updatedFields?: string[], + ): SelectOption[] => { + const hasSpecificFields = + isDefined(updatedFields) && updatedFields.length > 0; + + return [ + { label: t`All`, value: '*', Icon: IconNorthStar }, + { label: t`Created`, value: 'created', Icon: IconPlus }, + { + label: hasSpecificFields ? t`Updated (on specific fields)` : t`Updated`, + value: 'updated', + Icon: IconBox, + }, + { label: t`Deleted`, value: 'deleted', Icon: IconTrash }, + ]; + }; return ( <> @@ -89,7 +100,7 @@ export const SettingsDatabaseEventsForm = ({