Saltar al contenido principal
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.
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.
  • 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:
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Puedes ver los registros con:
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. Importa el tipo RoutePayload desde twenty-sdk/logic-function:
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:
PropiedadTipoDescripciónEjemplo
headersRecord\<string, string | undefined>Encabezados HTTP (solo aquellos listados en forwardedRequestHeaders)consulta la sección de abajo
queryStringParametersRecord\<string, string | undefined>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' }
pathParametersRecord\<string, string | undefined>Parámetros de ruta extraídos del patrón de la ruta/users/:id, /users/123 -> { id: '123' }
bodyobject | nullCuerpo de la solicitud analizado (JSON){ id: 1 } -> { id: 1 }
rawBodystring | undefinedCuerpo 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ó.
isBase64EncodedbooleanIndica si el cuerpo está codificado en base64
requestContext.http.methodstringMétodo HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringRuta 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:
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í:
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:
import { Response } from 'twenty-sdk/logic-function';

const handler = async (event: RoutePayload) => {
  return new Response('<h1>Hello</h1>', {
    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.
import type {
  DatabaseEventPayload,
  ObjectRecordCreateEvent,
  ObjectRecordDestroyEvent,
  ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';

type Person = {
  id: string;
  emails?: { primaryEmail?: string };
};
La carga útil incluye:
PropiedadDescripción
nameNombre del evento, como person.updated.
workspaceIdEspacio de trabajo donde ocurrió el evento.
objectMetadataMetadatos del objeto que cambió.
recordIdId del registro que cambió.
userId, userWorkspaceId, workspaceMemberIdCampos del actor cuando el evento fue causado por un usuario del espacio de trabajo.
propiedadesDatos del registro para el evento, con before, after, diff y updatedFields según la operación.
EventoDatos del registro
person.createdevent.properties.after
person.updatedevent.properties.before, event.properties.after, event.properties.diff, event.properties.updatedFields
person.destroyedevent.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:
type PersonCreatedEvent = DatabaseEventPayload<
  ObjectRecordCreateEvent<Person>
>;

const handler = async (event: PersonCreatedEvent) => {
  const person = event.properties.after;

  return {
    personId: event.recordId,
    email: person.emails?.primaryEmail,
  };
};
Ejemplo de evento de actualización:
type PersonUpdatedEvent = DatabaseEventPayload<
  ObjectRecordUpdateEvent<Person>
>;

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:
export default defineLogicFunction({
  ...,
  databaseEventTriggerSettings: {
    eventName: 'person.updated',
    updatedFields: ['emails'],
  },
});
Ejemplo de evento de eliminación:
type PersonDestroyedEvent = DatabaseEventPayload<
  ObjectRecordDestroyEvent<Person>
>;

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.
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:
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'],
    },
  },
});
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.
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 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.
ClienteImportarEndpoint¿Generado?
CoreApiClienttwenty-client-sdk/core/graphql — datos del espacio de trabajo (registros, objetos)Sí, en tiempo de desarrollo/compilación
MetadataApiClienttwenty-client-sdk/metadata/metadata — configuración del espacio de trabajo, cargas de archivosNo, 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.
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:
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';

const [company, setCompany] = useState<
  Pick<CoreSchema.Company, 'id' | 'name'> | 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.
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:
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ámetroTipoDescripción
fileBufferBufferEl contenido sin procesar del archivo
filenamestringEl nombre del archivo (se utiliza para el almacenamiento y la visualización)
contentTypestringTipo MIME (de forma predeterminada es application/octet-stream si se omite)
fieldMetadataUniversalIdentifierstringEl 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).