--- title: Funciones de lógica description: Defina funciones de TypeScript del lado del servidor con activadores HTTP, de cron y de eventos de base de datos. icon: bolt --- Las funciones lógicas son funciones de TypeScript del lado del servidor que se ejecutan en la plataforma Twenty. Pueden activarse mediante solicitudes HTTP, programaciones de cron o eventos de base de datos — y también pueden exponerse como herramientas para agentes de IA. Cada archivo de función usa `defineLogicFunction()` para exportar una configuración con un controlador y desencadenadores opcionales. ```ts src/logic-functions/createPostCard.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; import type { RoutePayload } from 'twenty-sdk/logic-function'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async (params: RoutePayload) => { const client = new CoreApiClient(); const body = (params.body ?? {}) as { name?: string }; const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; const result = await client.mutation({ createPostCard: { __args: { data: { name } }, id: true, name: true, }, }); return result; }; export default defineLogicFunction({ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'create-new-post-card', timeoutSeconds: 2, handler, httpRouteTriggerSettings: { path: '/post-card/create', httpMethod: 'POST', isAuthRequired: true, }, /*databaseEventTriggerSettings: { eventName: 'people.created', },*/ /*cronTriggerSettings: { pattern: '0 0 1 1 *', },*/ }); ``` Tipos de desencadenadores disponibles: * **httpRoute**: Expone tu función en una ruta y método HTTP **bajo el endpoint `/s/`**: > p. ej., `path: '/post-card/create'` se puede invocar en `https://your-twenty-server.com/s/post-card/create` Para invocar una función de lógica activada por una ruta desde un componente de frontend (headless), consulta [Llamar a una función de lógica](/l/es/developers/extend/apps/layout/front-components#calling-a-logic-function). * **cron**: Ejecuta tu función en un horario usando una expresión CRON. * **databaseEvent**: Se ejecuta en eventos del ciclo de vida de objetos del espacio de trabajo. Cuando la operación del evento es `updated`, se pueden especificar campos específicos que se deben escuchar en la matriz `updatedFields`. Si se deja sin definir o vacío, cualquier actualización activará la función. > p. ej. `person.updated`, `*.created`, `company.*` También puedes ejecutar manualmente una función usando la CLI: ```bash filename="Terminal" yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}' ``` ```bash filename="Terminal" yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf ``` Puedes ver los registros con: ```bash filename="Terminal" yarn twenty dev:function:logs ``` #### Carga útil del disparador de ruta Cuando un desencadenador de ruta invoca tu función de lógica, esta recibe un objeto `RoutePayload` que sigue el [formato AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). Importa el tipo `RoutePayload` desde `twenty-sdk/logic-function`: ```ts import type { RoutePayload } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { const { headers, queryStringParameters, pathParameters, body } = event; const { method, path } = event.requestContext.http; return { message: 'Success' }; }; ``` El tipo `RoutePayload` tiene la siguiente estructura: | Propiedad | Tipo | Descripción | Ejemplo | | ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `headers` | `Record\` | Encabezados HTTP (solo aquellos listados en `forwardedRequestHeaders`) | consulta la sección de abajo | | `queryStringParameters` | `Record\` | Parámetros de consulta (valores múltiples unidos con comas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | | `pathParameters` | `Record\` | Parámetros de ruta extraídos del patrón de la ruta | `/users/:id`, `/users/123` -> `{ id: '123' }` | | `body` | `object \| null` | Cuerpo de la solicitud analizado (JSON) | `{ id: 1 }` -> `{ id: 1 }` | | `rawBody` | `string \| undefined` | Cuerpo de la solicitud UTF-8 original, antes del análisis de JSON. Útil para verificar firmas de webhooks de estilo HMAC (p. ej., `X-Hub-Signature-256` de GitHub, Stripe). `undefined` cuando el entorno de ejecución no lo conservó. | | | `isBase64Encoded` | `boolean` | Indica si el cuerpo está codificado en base64 | | | `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | | | `requestContext.http.path` | `string` | Ruta de la solicitud sin procesar | | #### forwardedRequestHeaders De forma predeterminada, los encabezados HTTP de las solicitudes entrantes **no** se pasan a tu función de lógica por razones de seguridad. Para acceder a encabezados específicos, enuméralos explícitamente en el arreglo `forwardedRequestHeaders`: ```ts export default defineLogicFunction({ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'webhook-handler', handler, httpRouteTriggerSettings: { path: '/webhook', httpMethod: 'POST', isAuthRequired: false, forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], }, }); ``` En tu controlador, accede a los encabezados reenviados así: ```ts const handler = async (event: RoutePayload) => { const signature = event.headers['x-webhook-signature']; const contentType = event.headers['content-type']; // Validate webhook signature... return { received: true }; }; ``` Los nombres de los encabezados se normalizan a minúsculas. Accede a ellos usando claves en minúsculas (p. ej., `event.headers['content-type']`). #### Respuesta HTTP personalizada De forma predeterminada, devolver un valor sencillo desde tu controlador lo envía de vuelta como una respuesta `200` (JSON para objetos, `text/plain` para cadenas). Para controlar el código de estado y los encabezados de la respuesta, devuelve un `Response` desde `twenty-sdk/logic-function`: ```ts import { Response } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { return new Response('

Hello

', { status: 201, headers: { 'content-type': 'text/html' }, }); }; ``` Por razones de seguridad, los encabezados de la respuesta están restringidos a una lista de permitidos. Cualquier encabezado que no esté en la lista (por ejemplo, `Set-Cookie`, encabezados CORS como `Access-Control-Allow-Origin`, o encabezados personalizados `X-*`) se descarta silenciosamente antes de que se envíe la respuesta. Los encabezados de respuesta permitidos son: * `content-type` * `content-language` * `content-disposition` * `cache-control` * `retry-after` El código de estado debe ser un código de estado HTTP válido (entre 100 y 599). Los nombres de los encabezados de respuesta se comparan sin distinguir mayúsculas de minúsculas. #### Payload del disparador de evento de base de datos Cuando un disparador de evento de base de datos invoca tu función de lógica, esta recibe un `DatabaseEventPayload` por cada registro modificado. El payload combina metadatos sobre el espacio de trabajo y el objeto de origen con el evento a nivel de registro. ```ts import type { DatabaseEventPayload, ObjectRecordCreateEvent, ObjectRecordDestroyEvent, ObjectRecordUpdateEvent, } from 'twenty-sdk/logic-function'; type Person = { id: string; emails?: { primaryEmail?: string }; }; ``` La carga útil incluye: | Propiedad | Descripción | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `name` | Nombre del evento, como `person.updated`. | | `workspaceId` | Espacio de trabajo donde ocurrió el evento. | | `objectMetadata` | Metadatos del objeto que cambió. | | `recordId` | Id del registro que cambió. | | `userId`, `userWorkspaceId`, `workspaceMemberId` | Campos del actor cuando el evento fue causado por un usuario del espacio de trabajo. | | `propiedades` | Datos del registro para el evento, con `before`, `after`, `diff` y `updatedFields` según la operación. | | Evento | Datos del registro | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | `person.created` | `event.properties.after` | | `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` | | `person.destroyed` | `event.properties.before` | Para eliminaciones lógicas (soft deletes), `.deleted` sigue la estructura de estilo de actualización porque el campo `deletedAt` del registro cambia. Para eliminaciones permanentes, usa `.destroyed`. `databaseEventTriggerSettings.updatedFields` filtra qué eventos de actualización activan la función. `event.properties.updatedFields` te indica qué campos realmente cambiaron en el evento actual. Ejemplo de evento de creación: ```ts type PersonCreatedEvent = DatabaseEventPayload< ObjectRecordCreateEvent >; const handler = async (event: PersonCreatedEvent) => { const person = event.properties.after; return { personId: event.recordId, email: person.emails?.primaryEmail, }; }; ``` Ejemplo de evento de actualización: ```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, }; }; ``` Ejecutar solo en actualizaciones de correo electrónico: ```ts export default defineLogicFunction({ ..., databaseEventTriggerSettings: { eventName: 'person.updated', updatedFields: ['emails'], }, }); ``` Ejemplo de evento de eliminación: ```ts type PersonDestroyedEvent = DatabaseEventPayload< ObjectRecordDestroyEvent >; const handler = async (event: PersonDestroyedEvent) => { const personBeforeDestroy = event.properties.before; return { personId: event.recordId, email: personBeforeDestroy.emails?.primaryEmail, }; }; ``` #### Exponer una función como herramienta de IA o acción de flujo de trabajo Las funciones lógicas pueden exponerse en dos ámbitos, cada uno con su propio disparador: * **`toolTriggerSettings`** — hace que la función sea descubrible por las funciones de IA de Twenty (chat, MCP, llamadas a funciones). Usa el JSON Schema estándar, el formato que los LLM entienden de forma nativa. * **`workflowActionTriggerSettings`** — hace que la función aparezca como un paso en el constructor visual de flujos de trabajo. Usa el `InputSchema` completo de Twenty para que el constructor pueda renderizar editores de campos adecuados, selectores de variables y etiquetas. Una función puede optar por una, por la otra o por ambas. Se ubican junto a `cronTriggerSettings`, `databaseEventTriggerSettings` y `httpRouteTriggerSettings` — mismo patrón, misma estructura. ```ts src/logic-functions/enrich-company.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async (params: { companyName: string; domain?: string }) => { const client = new CoreApiClient(); const result = await client.mutation({ createTask: { __args: { data: { title: `Enrich data for ${params.companyName}`, body: `Domain: ${params.domain ?? 'unknown'}`, }, }, id: true, }, }); return { taskId: result.createTask.id }; }; export default defineLogicFunction({ universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', name: 'enrich-company', description: 'Enrich a company record with external data', timeoutSeconds: 10, handler, toolTriggerSettings: {}, }); ``` Puntos clave: * Una función puede mezclar superficies — declara tanto `toolTriggerSettings` como `workflowActionTriggerSettings` para exponerla en el chat Y en el constructor de flujos de trabajo. * Ambos, `toolTriggerSettings.inputSchema` y `workflowActionTriggerSettings.inputSchema`, son opcionales. Cuando se omiten, el generador del manifiesto los infiere a partir del código fuente del controlador (JSON Schema para la herramienta de IA, `InputSchema` de Twenty para la acción de flujo de trabajo). Proporciona uno explícitamente cuando quieras un tipado más rico — por ejemplo, con campos compatibles con `FieldMetadataType` como `CURRENCY` o `RELATION` para el constructor de flujos de trabajo, o con campos `description` que el agente de IA pueda leer: ```ts export default defineLogicFunction({ ..., toolTriggerSettings: { inputSchema: { type: 'object', properties: { companyName: { type: 'string', description: 'The name of the company to enrich', }, domain: { type: 'string', description: 'The company website domain (optional)', }, }, required: ['companyName'], }, }, }); ``` Para declarar tus parámetros **una sola vez** y atender ambas superficies, define un único JSON Schema (`InputJsonSchema`) y conviértelo para la acción de workflow con `jsonSchemaToInputSchema` de `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` usa directamente el JSON Schema, mientras que `workflowActionTriggerSettings.inputSchema` espera el `InputSchema` de Twenty: ```ts import { defineLogicFunction } from 'twenty-sdk/define'; import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function'; const inputSchema: InputJsonSchema = { type: 'object', properties: { companyName: { type: 'string', label: 'Company name' }, domain: { type: 'string', label: 'Domain' }, }, required: ['companyName'], }; export default defineLogicFunction({ ..., toolTriggerSettings: { inputSchema }, workflowActionTriggerSettings: { label: 'Enrich Company', icon: 'IconBuilding', inputSchema: jsonSchemaToInputSchema(inputSchema), }, }); ``` **Escribe una buena `description`.** Los agentes de IA dependen del campo `description` de la función para decidir cuándo usar la herramienta. Sé específico acerca de lo que hace la herramienta y cuándo debe invocarse.
**Utilidades en tiempo de ejecución.** `twenty-sdk/utils` vuelve a exportar pequeñas utilidades en tiempo de ejecución para que los handlers nunca importen directamente desde `twenty-shared`. Por ejemplo, `isDefined(value)` devuelve `false` tanto para `null` como para `undefined` — utilízalo para acotar de forma segura las entradas opcionales de los handlers, que pueden llegar como `null` en tiempo de ejecución incluso cuando están tipadas como `T | undefined`: ```ts import { isDefined } from 'twenty-sdk/utils'; const handler = async (params: { parentMessageId?: string }) => { if (isDefined(params.parentMessageId)) { // params.parentMessageId is narrowed to string here } }; ``` **Hooks de instalación** — los controladores de preinstalación y postinstalación — comparten este entorno de ejecución, pero se declaran con sus propias funciones 'define' y no aceptan configuraciones de disparador. Consulta [Hooks de instalación](/l/es/developers/extend/apps/config/install-hooks) para `definePreInstallLogicFunction` y `definePostInstallLogicFunction`. ## Clientes de API tipados (twenty-client-sdk) El paquete `twenty-client-sdk` proporciona dos clientes GraphQL tipados para interactuar con la API de Twenty desde tus funciones de lógica y componentes de frontend. | Cliente | Importar | Endpoint | ¿Generado? | | ------------------- | ---------------------------- | ---------------------------------------------------------------------- | --------------------------------------- | | `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — datos del espacio de trabajo (registros, objetos) | Sí, en tiempo de desarrollo/compilación | | `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuración del espacio de trabajo, cargas de archivos | No, viene preconstruido | `CoreApiClient` es el cliente principal para consultar y mutar datos del espacio de trabajo. Se **genera a partir del esquema de tu espacio de trabajo** durante `yarn twenty dev` o `yarn twenty dev:build`, por lo que está completamente tipado para coincidir con tus objetos y campos. ```ts import { CoreApiClient } from 'twenty-client-sdk/core'; const client = new CoreApiClient(); // Query records const { companies } = await client.query({ companies: { edges: { node: { id: true, name: true, domainName: { primaryLinkLabel: true, primaryLinkUrl: true, }, }, }, }, }); // Create a record const { createCompany } = await client.mutation({ createCompany: { __args: { data: { name: 'Acme Corp', }, }, id: true, name: true, }, }); ``` El cliente usa una sintaxis de conjunto de selección: pasa `true` para incluir un campo, usa `__args` para los argumentos y anida objetos para las relaciones. Obtienes autocompletado completo y verificación de tipos basados en el esquema de tu espacio de trabajo. **CoreApiClient se genera en tiempo de desarrollo/compilación.** Si intentas usarlo sin ejecutar primero `yarn twenty dev` o `yarn twenty dev:build`, lanzará un error. La generación ocurre automáticamente: la CLI inspecciona el esquema GraphQL de tu espacio de trabajo y genera un cliente tipado usando `@genql/cli`. #### Uso de CoreSchema para anotaciones de tipos `CoreSchema` proporciona tipos de TypeScript que coinciden con los objetos de tu espacio de trabajo; útil para tipar el estado de componentes o parámetros de funciones: ```ts import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; import { useState } from 'react'; const [company, setCompany] = useState< Pick | undefined >(undefined); const client = new CoreApiClient(); const result = await client.query({ company: { __args: { filter: { position: { eq: 1 } } }, id: true, name: true, }, }); setCompany(result.company); ``` `MetadataApiClient` viene preconstruido con el SDK (no se requiere generación). Consulta el endpoint `/metadata` para la configuración del espacio de trabajo, las aplicaciones y las cargas de archivos. ```ts import { MetadataApiClient } from 'twenty-client-sdk/metadata'; const metadataClient = new MetadataApiClient(); // List first 10 objects in the workspace const { objects } = await metadataClient.query({ objects: { edges: { node: { id: true, nameSingular: true, namePlural: true, labelSingular: true, isCustom: true, }, }, __args: { filter: {}, paging: { first: 10 }, }, }, }); ``` #### Subir archivos El `MetadataApiClient` incluye un método `uploadFile` para adjuntar archivos a los campos de tipo archivo: ```ts import { MetadataApiClient } from 'twenty-client-sdk/metadata'; import * as fs from 'fs'; const metadataClient = new MetadataApiClient(); const fileBuffer = fs.readFileSync('./invoice.pdf'); const uploadedFile = await metadataClient.uploadFile( fileBuffer, // file contents as a Buffer 'invoice.pdf', // filename 'application/pdf', // MIME type '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier ); console.log(uploadedFile); // { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } ``` | Parámetro | Tipo | Descripción | | ---------------------------------- | -------- | ----------------------------------------------------------------------------- | | `fileBuffer` | `Buffer` | El contenido sin procesar del archivo | | `filename` | `string` | El nombre del archivo (se utiliza para el almacenamiento y la visualización) | | `contentType` | `string` | Tipo MIME (de forma predeterminada es `application/octet-stream` si se omite) | | `fieldMetadataUniversalIdentifier` | `string` | El `universalIdentifier` del campo de tipo de archivo de tu objeto | Puntos clave: * Utiliza el `universalIdentifier` del campo (no su ID específico del espacio de trabajo), por lo que tu código de carga funciona en cualquier espacio de trabajo donde esté instalada tu aplicación. * La `url` devuelta es una URL firmada que puedes usar para acceder al archivo cargado. Cuando tu código se ejecuta en Twenty (funciones de lógica o componentes de frontend), la plataforma inyecta credenciales como variables de entorno: * `TWENTY_API_URL` — URL base de la API de Twenty * `TWENTY_APP_ACCESS_TOKEN` — Token de corta duración con alcance al rol de función predeterminado de tu aplicación No necesitas pasar estas credenciales a los clientes — leen de `process.env` automáticamente. Los permisos de la clave de API están determinados por el rol declarado con `defineApplicationRole()` (o referenciado mediante `defaultRoleUniversalIdentifier` en `application-config.ts`).