diff --git a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx index 6edf3a9530..f0f83e0845 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx @@ -13,8 +13,8 @@ Each function file uses `defineLogicFunction()` to export a configuration with a ```ts src/logic-functions/createPostCard.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; -import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; +import type { RoutePayload } from 'twenty-sdk/logic-function'; +import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async (params: RoutePayload) => { const client = new CoreApiClient(); @@ -79,10 +79,10 @@ yarn twenty logs When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the [AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). -Import the `RoutePayload` type from `twenty-sdk`: +Import the `RoutePayload` type from `twenty-sdk/logic-function`: ```ts -import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; +import type { RoutePayload } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { const { headers, queryStringParameters, pathParameters, body } = event; @@ -141,6 +141,115 @@ const handler = async (event: RoutePayload) => { Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). +#### Database event trigger payload + +When a database event trigger invokes your logic function, it receives one `DatabaseEventPayload` per changed record. The payload combines metadata about the source workspace and object with the record-level event. + +```ts +import type { + DatabaseEventPayload, + ObjectRecordCreateEvent, + ObjectRecordDestroyEvent, + ObjectRecordUpdateEvent, +} from 'twenty-sdk/logic-function'; + +type Person = { + id: string; + emails?: { primaryEmail?: string }; +}; +``` + +The payload includes: + +| Property | Description | +|----------|-------------| +| `name` | Event name, such as `person.updated`. | +| `workspaceId` | Workspace where the event happened. | +| `objectMetadata` | Metadata for the object that changed. | +| `recordId` | Id of the changed record. | +| `userId`, `userWorkspaceId`, `workspaceMemberId` | Actor fields when the event was caused by a workspace user. | +| `properties` | Record data for the event, with `before`, `after`, `diff`, and `updatedFields` depending on the operation. | + +| Event | Record data | +|-------|-------------| +| `person.created` | `event.properties.after` | +| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` | +| `person.destroyed` | `event.properties.before` | + +For soft deletes, `.deleted` follows the update-style shape because the record's `deletedAt` field changes. +For permanent deletes, use `.destroyed`. + + +`databaseEventTriggerSettings.updatedFields` filters which update events trigger the function. +`event.properties.updatedFields` tells you which fields actually changed on the current event. + + +Created event example: + +```ts +type PersonCreatedEvent = DatabaseEventPayload< + ObjectRecordCreateEvent +>; + +const handler = async (event: PersonCreatedEvent) => { + const person = event.properties.after; + + return { + personId: event.recordId, + email: person.emails?.primaryEmail, + }; +}; +``` + +Updated event example: + +```ts +type PersonUpdatedEvent = DatabaseEventPayload< + ObjectRecordUpdateEvent +>; + +const handler = async (event: PersonUpdatedEvent) => { + const { before, after, diff, updatedFields } = event.properties; + + return { + personId: event.recordId, + updatedFields, + previousEmail: before.emails?.primaryEmail, + currentEmail: after.emails?.primaryEmail, + emailDiff: diff.emails, + }; +}; +``` + +Trigger only on email updates: + +```ts +export default defineLogicFunction({ + ..., + databaseEventTriggerSettings: { + eventName: 'person.updated', + updatedFields: ['emails'], + }, +}); +``` + +Destroyed event example: + +```ts +type PersonDestroyedEvent = DatabaseEventPayload< + ObjectRecordDestroyEvent +>; + +const handler = async (event: PersonDestroyedEvent) => { + const personBeforeDestroy = event.properties.before; + + return { + personId: event.recordId, + email: personBeforeDestroy.emails?.primaryEmail, + }; +}; +``` + #### Exposing a function as an AI tool or workflow action Logic functions can be exposed on two surfaces, each with its own trigger: diff --git a/packages/twenty-shared/src/database-events/database-event-payload.type.ts b/packages/twenty-shared/src/database-events/database-event-payload.type.ts index 46ac94f982..07f7ce204a 100644 --- a/packages/twenty-shared/src/database-events/database-event-payload.type.ts +++ b/packages/twenty-shared/src/database-events/database-event-payload.type.ts @@ -37,19 +37,11 @@ type SimplifiedFlatObjectMetadata = { viewUniversalIdentifiers: string[]; }; -type WorkspaceEventBatch = { +type DatabaseEventMetadata = { name: string; workspaceId: string; objectMetadata: SimplifiedFlatObjectMetadata; - userId: string; - userWorkspaceId: string; - workspaceMemberId: string; - recordId: string; - events: WorkspaceEvent[]; }; -export type DatabaseEventPayload = Omit< - WorkspaceEventBatch, - 'events' -> & - T; +export type DatabaseEventPayload = + DatabaseEventMetadata & T;