Zum Hauptinhalt springen
Logikfunktionen sind serverseitige TypeScript-Funktionen, die auf der Twenty-Plattform ausgeführt werden. Sie können durch HTTP-Anfragen, cron-Zeitpläne oder Datenbankereignisse ausgelöst werden — und außerdem als Tools für KI-Agenten bereitgestellt werden.
Jede Funktionsdatei verwendet defineLogicFunction(), um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
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 *',
  },*/
});
Verfügbare Trigger-Typen:
  • httpRoute: Stellt Ihre Funktion unter einem HTTP-Pfad und einer Methode unter dem Endpunkt /s/ bereit:
z. B. path: '/post-card/create' ist unter https://your-twenty-server.com/s/post-card/create aufrufbar
Um eine routenausgelöste Logikfunktion von einer (headless) Front-Komponente aus aufzurufen, siehe Aufrufen einer Logikfunktion.
  • cron: Führt Ihre Funktion nach Zeitplan mithilfe eines CRON-Ausdrucks aus.
  • databaseEvent: Wird bei Lebenszyklusereignissen von Workspace-Objekten ausgeführt. Wenn die Ereignisoperation updated ist, können bestimmte zu überwachende Felder im Array updatedFields angegeben werden. Wenn das Array undefiniert oder leer ist, löst jede Aktualisierung die Funktion aus.
z. B. person.updated, *.created, company.*
Sie können eine Funktion auch manuell über die CLI ausführen:
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Sie können Protokolle mit folgendem Befehl ansehen:
yarn twenty dev:function:logs

Routen-Trigger-Payload

Wenn ein Route-Trigger Ihre Logikfunktion aufruft, erhält sie ein RoutePayload-Objekt, das dem AWS-HTTP-API-v2-Format folgt. Importieren Sie den Typ RoutePayload aus 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' };
};
Der Typ RoutePayload hat die folgende Struktur:
EigenschaftTypBeschreibungBeispiel
headersRecord\<string, string | undefined>HTTP-Header (nur die in forwardedRequestHeaders aufgelisteten)siehe Abschnitt unten
queryStringParametersRecord\<string, string | undefined>Query-String-Parameter (mehrere Werte mit Kommas verbunden)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord\<string, string | undefined>Aus dem Routenmuster extrahierte Pfadparameter/users/:id, /users/123 -> { id: '123' }
bodyobject | nullGeparster Request-Body (JSON){ id: 1 } -> { id: 1 }
rawBodystring | undefinedUrsprünglicher UTF-8-Request-Body vor dem JSON-Parsing. Nützlich zur Verifizierung von Webhook-Signaturen im HMAC-Stil (z. B. GitHubs X-Hub-Signature-256, Stripe). undefined, wenn die Laufzeitumgebung es nicht beibehalten hat.
isBase64EncodedbooleanGibt an, ob der Body Base64-codiert ist
requestContext.http.methodstringHTTP-Methode (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringRohpfad der Anfrage

forwardedRequestHeaders

Standardmäßig werden HTTP-Header von eingehenden Anfragen aus Sicherheitsgründen nicht an Ihre Logikfunktion weitergegeben. Um auf bestimmte Header zuzugreifen, listen Sie diese im Array forwardedRequestHeaders auf:
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'],
  },
});
Greifen Sie in Ihrem Handler wie folgt auf die weitergeleiteten Header zu:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
Header-Namen werden in Kleinbuchstaben normalisiert. Greifen Sie mit Schlüsseln in Kleinbuchstaben darauf zu (z. B. event.headers['content-type']).

Benutzerdefinierte HTTP-Antwort

Standardmäßig sendet das Zurückgeben eines einfachen Werts aus Ihrem Handler diesen als 200-Antwort zurück (JSON für Objekte, text/plain für Zeichenketten). Um den Statuscode und die Antwort-Header zu steuern, geben Sie eine Response aus twenty-sdk/logic-function zurück:
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' },
  });
};
Aus Sicherheitsgründen sind Antwort-Header auf eine Allowlist beschränkt. Jeder Header, der nicht auf der Liste steht (z. B. Set-Cookie, CORS-Header wie Access-Control-Allow-Origin oder benutzerdefinierte X-*-Header), wird stillschweigend verworfen, bevor die Antwort gesendet wird. Die erlaubten Antwort-Header sind:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
Der Statuscode muss ein gültiger HTTP-Statuscode sein (zwischen 100 und 599). Antwort-Header-Namen werden ohne Beachtung der Groß-/Kleinschreibung verglichen.

Datenbank-Event-Trigger-Payload

Wenn ein Datenbank-Event-Trigger Ihre Logic Function aufruft, erhält sie eine DatabaseEventPayload pro geändertem Datensatz. Die Payload kombiniert Metadaten über den Quell-Workspace und das Objekt mit dem Ereignis auf Datensatzebene.
import type {
  DatabaseEventPayload,
  ObjectRecordCreateEvent,
  ObjectRecordDestroyEvent,
  ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';

type Person = {
  id: string;
  emails?: { primaryEmail?: string };
};
Die Nutzlast umfasst:
EigenschaftBeschreibung
nameEreignisname, z. B. person.updated.
workspaceIdArbeitsbereich, in dem das Ereignis stattgefunden hat.
objectMetadataMetadaten für das Objekt, das geändert wurde.
recordIdID des geänderten Datensatzes.
userId, userWorkspaceId, workspaceMemberIdAkteurfelder, wenn das Ereignis von einem Benutzer des Arbeitsbereichs ausgelöst wurde.
propertiesDatensatzdaten für das Ereignis mit before, after, diff und updatedFields, abhängig von der Operation.
EreignisDatensatzdaten
person.createdevent.properties.after
person.updatedevent.properties.before, event.properties.after, event.properties.diff, event.properties.updatedFields
person.destroyedevent.properties.before
Bei Soft Deletes folgt .deleted der Aktualisierungsstruktur, da sich das Feld deletedAt des Datensatzes ändert. Für dauerhafte Löschvorgänge verwende .destroyed.
databaseEventTriggerSettings.updatedFields filtert, welche Aktualisierungsereignisse die Funktion auslösen. event.properties.updatedFields gibt an, welche Felder beim aktuellen Ereignis tatsächlich geändert wurden.
Beispiel für ein Created-Ereignis:
type PersonCreatedEvent = DatabaseEventPayload<
  ObjectRecordCreateEvent<Person>
>;

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

  return {
    personId: event.recordId,
    email: person.emails?.primaryEmail,
  };
};
Beispiel für ein Updated-Ereignis:
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,
  };
};
Nur bei E-Mail-Aktualisierungen auslösen:
export default defineLogicFunction({
  ...,
  databaseEventTriggerSettings: {
    eventName: 'person.updated',
    updatedFields: ['emails'],
  },
});
Beispiel für ein Destroyed-Ereignis:
type PersonDestroyedEvent = DatabaseEventPayload<
  ObjectRecordDestroyEvent<Person>
>;

const handler = async (event: PersonDestroyedEvent) => {
  const personBeforeDestroy = event.properties.before;

  return {
    personId: event.recordId,
    email: personBeforeDestroy.emails?.primaryEmail,
  };
};

Eine Funktion als KI-Tool oder Workflow-Aktion verfügbar machen

Logikfunktionen können auf zwei Oberflächen verfügbar gemacht werden, jeweils mit eigenem Trigger:
  • toolTriggerSettings — macht die Funktion über die KI-Funktionen von Twenty (Chat, MCP, Funktionsaufrufe) auffindbar. Verwendet das standardmäßige JSON Schema, das Format, das LLMs nativ verstehen.
  • workflowActionTriggerSettings — lässt die Funktion als Schritt im visuellen Workflow-Builder erscheinen. Verwendet das umfangreiche InputSchema von Twenty, sodass der Builder geeignete Feldeditoren, Variablenauswahlen und Beschriftungen rendern kann.
Eine Funktion kann sich für eine, die andere oder beide entscheiden. Sie stehen neben cronTriggerSettings, databaseEventTriggerSettings und httpRouteTriggerSettings — gleiches Muster, gleiche Struktur.
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: {},
});
Hauptpunkte:
  • Eine Funktion kann Oberflächen mischen — deklarieren Sie sowohl toolTriggerSettings als auch workflowActionTriggerSettings, um sie im Chat UND im Workflow-Builder bereitzustellen.
  • toolTriggerSettings.inputSchema und workflowActionTriggerSettings.inputSchema sind beide optional. Wenn sie weggelassen werden, leitet der Manifest-Builder sie aus dem Handler-Quellcode ab (JSON Schema für das KI-Tool, das InputSchema von Twenty für die Workflow-Aktion). Geben Sie eines explizit an, wenn Sie eine reichere Typisierung wünschen — zum Beispiel mit FieldMetadataType-fähigen Feldern wie CURRENCY oder RELATION für den Workflow-Builder oder mit description-Feldern, die der KI-Agent lesen kann:
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'],
    },
  },
});
Schreiben Sie eine gute description. KI-Agenten verlassen sich auf das description-Feld der Funktion, um zu entscheiden, wann das Tool verwendet werden soll. Seien Sie konkret darin, was das Tool tut und wann es aufgerufen werden soll.
Installations-Hooks – Vorinstallations- und Nachinstallations-Handler – teilen sich diese Laufzeit, werden aber mit ihren eigenen define-Funktionen deklariert und verwenden keine Trigger-Einstellungen. Siehe Installations-Hooks für definePreInstallLogicFunction und definePostInstallLogicFunction.

Typisierte API-Clients (twenty-client-sdk)

Das Paket twenty-client-sdk stellt zwei typisierte GraphQL-Clients bereit, um aus Ihren Logikfunktionen und Frontend-Komponenten mit der Twenty-API zu interagieren.
ClientImportierenEndpunktGeneriert?
CoreApiClienttwenty-client-sdk/core/graphql — Arbeitsbereichsdaten (Datensätze, Objekte)Ja, zur Entwicklungs-/Build-Zeit
MetadataApiClienttwenty-client-sdk/metadata/metadata — Arbeitsbereichskonfiguration, Datei-UploadsNein, wird vorgefertigt ausgeliefert
Der CoreApiClient ist der Haupt-Client zum Abfragen und Ändern von Arbeitsbereichsdaten. Er wird während yarn twenty dev oder yarn twenty dev:build aus Ihrem Arbeitsbereichsschema generiert und ist daher vollständig typisiert, passend zu Ihren Objekten und Feldern.
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,
  },
});
Der Client verwendet eine Selection-Set-Syntax: Übergeben Sie true, um ein Feld einzuschließen, verwenden Sie __args für Argumente, und verschachteln Sie Objekte für Relationen. Sie erhalten vollständige Autovervollständigung und Typprüfung basierend auf Ihrem Arbeitsbereichsschema.
Der CoreApiClient wird zur Entwicklungs-/Build-Zeit generiert. Wenn Sie ihn verwenden, ohne zuvor yarn twenty dev oder yarn twenty dev:build ausgeführt zu haben, wird ein Fehler ausgelöst. Die Generierung erfolgt automatisch — die CLI inspiziert das GraphQL-Schema Ihres Arbeitsbereichs und erzeugt mit @genql/cli einen typisierten Client.

Verwendung von CoreSchema für Typannotationen

CoreSchema stellt TypeScript-Typen bereit, die Ihren Arbeitsbereichsobjekten entsprechen — nützlich zum Typisieren von Komponentenzustand oder Funktionsparametern:
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 ist im SDK bereits vorgefertigt enthalten (keine Generierung erforderlich). Er fragt den Endpunkt /metadata nach Arbeitsbereichskonfiguration, Anwendungen und Datei-Uploads ab.
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 },
    },
  },
});

Dateien hochladen

Der MetadataApiClient enthält eine Methode uploadFile, um Dateien an Felder des Typs Datei anzuhängen:
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://...' }
ParameterTypBeschreibung
fileBufferBufferDer Rohinhalt der Datei
filenamestringDer Name der Datei (wird für Speicherung und Anzeige verwendet)
contentTypestringMIME-Typ (standardmäßig application/octet-stream, wenn weggelassen)
fieldMetadataUniversalIdentifierstringDer universalIdentifier des Dateityp-Felds in Ihrem Objekt
Hauptpunkte:
  • Sie verwendet den universalIdentifier des Feldes (nicht dessen arbeitsbereichsspezifische ID), sodass Ihr Upload-Code in jedem Arbeitsbereich funktioniert, in dem Ihre App installiert ist.
  • Die zurückgegebene url ist eine signierte URL, mit der Sie auf die hochgeladene Datei zugreifen können.
Wenn Ihr Code auf Twenty ausgeführt wird (Logikfunktionen oder Frontend-Komponenten), injiziert die Plattform Anmeldedaten als Umgebungsvariablen:
  • TWENTY_API_URL — Basis-URL der Twenty-API
  • TWENTY_APP_ACCESS_TOKEN — Kurzlebiger Schlüssel, der auf die Standard-Funktionsrolle Ihrer Anwendung begrenzt ist
Sie müssen diese nicht an die Clients übergeben — sie lesen automatisch aus process.env. Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, die mit defineApplicationRole() deklariert wird (oder über defaultRoleUniversalIdentifier in application-config.ts referenziert wird).