Zum Hauptinhalt springen
Apps befinden sich derzeit in der Alpha-Phase. Die Funktion ist funktionsfähig, entwickelt sich jedoch noch weiter.
Das Paket twenty-sdk stellt typisierte Bausteine zum Erstellen Ihrer App bereit. Diese Seite behandelt alle im SDK verfügbaren Entitätstypen und API-Clients.

DefineEntity-Funktionen

Das SDK stellt Funktionen bereit, um die Entitäten Ihrer App zu definieren. Sie müssen export default defineEntity({...}) verwenden, damit das SDK Ihre Entitäten erkennt. Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
Die Dateiorganisation liegt bei Ihnen. Die Entitätserkennung ist AST-basiert — das SDK findet Aufrufe von export default defineEntity(...), unabhängig davon, wo sich die Datei befindet. Das Gruppieren von Dateien nach Typ (z. B. logic-functions/, roles/) ist lediglich eine Konvention, keine Voraussetzung.
Rollen kapseln Berechtigungen für die Objekte und Aktionen Ihres Workspaces.
restricted-company-role.ts
import {
  defineRole,
  PermissionFlag,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';

export default defineRole({
  universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
  label: 'My new role',
  description: 'A role that can be used in your workspace',
  canReadAllObjectRecords: false,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],
  fieldPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
      canReadFieldValue: false,
      canUpdateFieldValue: false,
    },
  ],
  permissionFlags: [PermissionFlag.APPLICATIONS],
});
Jede App muss genau einen Aufruf von defineApplication haben, der Folgendes beschreibt:
  • Identität: Bezeichner, Anzeigename und Beschreibung.
  • Berechtigungen: welche Rolle ihre Funktionen und Frontend-Komponenten verwenden.
  • (Optional) Variablen: Schlüssel–Wert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
  • (Optional) Pre-/Post-Installationsfunktionen: Logikfunktionen, die vor oder nach der Installation ausgeführt werden.
src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';

export default defineApplication({
  universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
  displayName: 'My Twenty App',
  description: 'My first Twenty app',
  icon: 'IconWorld',
  applicationVariables: {
    DEFAULT_RECIPIENT_NAME: {
      universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
      description: 'Default recipient name for postcards',
      value: 'Jane Doe',
      isSecret: false,
    },
  },
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
Notizen:
  • universalIdentifier-Felder sind deterministische IDs, die Ihnen gehören. Erzeugen Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
  • applicationVariables werden zu Umgebungsvariablen für Ihre Funktionen und Frontend-Komponenten (z. B. ist DEFAULT_RECIPIENT_NAME als process.env.DEFAULT_RECIPIENT_NAME verfügbar).
  • defaultRoleUniversalIdentifier muss auf eine mit defineRole() definierte Rolle verweisen (siehe oben).
  • Pre- und Post-Installationsfunktionen werden während des Manifest-Builds automatisch erkannt — Sie müssen sie in defineApplication() nicht referenzieren.

Marktplatz-Metadaten

Wenn Sie planen, Ihre App zu veröffentlichen, steuern diese optionalen Felder, wie Ihre App im Marktplatz erscheint:
FeldBeschreibung
authorName des Autors oder des Unternehmens
categoryApp-Kategorie für die Filterung im Marktplatz
logoUrlPfad zu Ihrem App-Logo (z. B. public/logo.png)
screenshotsArray von Screenshot-Pfaden (z. B. public/screenshot-1.png)
aboutDescriptionLängere Markdown-Beschreibung für den Tab “Info”. Wenn weggelassen, verwendet der Marketplace die README.md des Pakets von npm
websiteUrlLink zu Ihrer Website
termsUrlLink zu den Nutzungsbedingungen
emailSupportSupport-E-Mail-Adresse
issueReportUrlLink zum Issue-Tracker

Rollen und Berechtigungen

Das Feld defaultRoleUniversalIdentifier in application-config.ts legt die Standardrolle fest, die von den Logikfunktionen und Frontend-Komponenten Ihrer App verwendet wird. Details finden Sie oben unter defineRole.
  • Das zur Laufzeit als TWENTY_APP_ACCESS_TOKEN injizierte Token wird aus dieser Rolle abgeleitet.
  • Der typisierte Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt.
  • Befolgen Sie das Least-Privilege-Prinzip: Erstellen Sie eine dedizierte Rolle nur mit den Berechtigungen, die Ihre Funktionen benötigen.
Standard-Funktionsrolle
Wenn Sie eine neue App erzeugen, erstellt die CLI eine Standard-Rolldatei:
src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';

export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
  'b648f87b-1d26-4961-b974-0908fd991061';

export default defineRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: 'Default function role',
  description: 'Default role for function Twenty client',
  canReadAllObjectRecords: true,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [],
  fieldPermissions: [],
  permissionFlags: [],
});
Der universalIdentifier dieser Rolle wird in application-config.ts als defaultRoleUniversalIdentifier referenziert:
  • *.role.ts definiert, was die Rolle darf.
  • application-config.ts verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
Notizen:
  • Beginnen Sie mit der vorab erstellten Rolle und schränken Sie sie schrittweise gemäß dem Least-Privilege-Prinzip ein.
  • Ersetzen Sie objectPermissions und fieldPermissions durch die Objekte und Felder, die Ihre Funktionen tatsächlich benötigen.
  • permissionFlags steuern den Zugriff auf Funktionen auf Plattformebene. Halten Sie sie minimal.
  • Ein funktionierendes Beispiel finden Sie unter: hello-world/src/roles/function-role.ts.
Benutzerdefinierte Objekte beschreiben sowohl Schema als auch Verhalten für Datensätze in Ihrem Workspace. Verwenden Sie defineObject(), um Objekte mit eingebauter Validierung zu definieren:
postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';

enum PostCardStatus {
  DRAFT = 'DRAFT',
  SENT = 'SENT',
  DELIVERED = 'DELIVERED',
  RETURNED = 'RETURNED',
}

export default defineObject({
  universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
  nameSingular: 'postCard',
  namePlural: 'postCards',
  labelSingular: 'Post Card',
  labelPlural: 'Post Cards',
  description: 'A post card object',
  icon: 'IconMail',
  fields: [
    {
      universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
      name: 'content',
      type: FieldType.TEXT,
      label: 'Content',
      description: "Postcard's content",
      icon: 'IconAbc',
    },
    {
      universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
      name: 'recipientName',
      type: FieldType.FULL_NAME,
      label: 'Recipient name',
      icon: 'IconUser',
    },
    {
      universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
      name: 'recipientAddress',
      type: FieldType.ADDRESS,
      label: 'Recipient address',
      icon: 'IconHome',
    },
    {
      universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
      name: 'status',
      type: FieldType.SELECT,
      label: 'Status',
      icon: 'IconSend',
      defaultValue: `'${PostCardStatus.DRAFT}'`,
      options: [
        { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
        { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
        { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
        { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
      ],
    },
    {
      universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
      name: 'deliveredAt',
      type: FieldType.DATE_TIME,
      label: 'Delivered at',
      icon: 'IconCheck',
      isNullable: true,
      defaultValue: null,
    },
  ],
});
Hauptpunkte:
  • Verwenden Sie defineObject() für eingebaute Validierung und bessere IDE-Unterstützung.
  • Der universalIdentifier muss eindeutig und über Deployments hinweg stabil sein.
  • Jedes Feld benötigt name, type, label und einen eigenen stabilen universalIdentifier.
  • Das Array fields ist optional — Sie können Objekte ohne benutzerdefinierte Felder definieren.
  • Sie können mit yarn twenty add neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
Basisfelder werden automatisch erstellt. Wenn Sie ein benutzerdefiniertes Objekt definieren, fügt Twenty automatisch Standardfelder hinzu wie id, name, createdAt, updatedAt, createdBy, updatedBy und deletedAt. Sie müssen diese nicht in Ihrem fields-Array definieren — fügen Sie nur Ihre benutzerdefinierten Felder hinzu. Sie können Standardfelder überschreiben, indem Sie in Ihrem fields-Array ein Feld mit demselben Namen definieren, dies wird jedoch nicht empfohlen.
Verwenden Sie defineField(), um Objekten, die Ihnen nicht gehören — etwa Standardobjekten von Twenty (Person, Company usw.) — Felder hinzuzufügen oder Objekten aus anderen Apps. Im Gegensatz zu Inline-Feldern in defineObject() benötigen eigenständige Felder einen objectUniversalIdentifier, um anzugeben, welches Objekt sie erweitern:
src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';

export default defineField({
  universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
  objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
  name: 'loyaltyTier',
  type: FieldType.SELECT,
  label: 'Loyalty Tier',
  icon: 'IconStar',
  options: [
    { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
    { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
    { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
  ],
});
Hauptpunkte:
  • Der objectUniversalIdentifier identifiziert das Zielobjekt. Für Standardobjekte verwenden Sie STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, die aus twenty-sdk exportiert werden.
  • Wenn Sie Felder inline in defineObject() definieren, benötigen Sie objectUniversalIdentifier nicht — er wird vom übergeordneten Objekt geerbt.
  • defineField() ist die einzige Möglichkeit, Felder zu Objekten hinzuzufügen, die Sie nicht mit defineObject() erstellt haben.
Relationen verbinden Objekte miteinander. In Twenty sind Relationen stets bidirektional — Sie definieren beide Seiten, und jede Seite referenziert die andere.Es gibt zwei Relationstypen:
BeziehungstypBeschreibungFremdschlüssel vorhanden?
MANY_TO_ONEViele Datensätze dieses Objekts verweisen auf einen Datensatz des ZielsJa (joinColumnName)
ONE_TO_MANYEin Datensatz dieses Objekts hat viele Datensätze des ZielsNein (inverse Seite)

Wie Relationen funktionieren

Jede Relation erfordert zwei Felder, die sich gegenseitig referenzieren:
  1. Die MANY_TO_ONE-Seite — befindet sich auf dem Objekt, das den Fremdschlüssel hält
  2. Die ONE_TO_MANY-Seite — befindet sich auf dem Objekt, dem die Sammlung gehört
Beide Felder verwenden FieldType.RELATION und verweisen über relationTargetFieldMetadataUniversalIdentifier gegenseitig aufeinander.

Beispiel: Postkarte hat viele Empfänger

Angenommen, eine PostCard kann an viele PostCardRecipient-Datensätze gesendet werden. Jeder Empfänger gehört genau zu einer Postkarte.Schritt 1: Definieren Sie die ONE_TO_MANY-Seite auf PostCard (die “eine” Seite):
src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';

export default defineField({
  universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCardRecipients',
  label: 'Post Card Recipients',
  icon: 'IconUsers',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
  universalSettings: {
    relationType: RelationType.ONE_TO_MANY,
  },
});
Schritt 2: Definieren Sie die MANY_TO_ONE-Seite auf PostCardRecipient (die “viele” Seite — hält den Fremdschlüssel):
src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';

export default defineField({
  universalIdentifier: POST_CARD_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCard',
  label: 'Post Card',
  icon: 'IconMail',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.CASCADE,
    joinColumnName: 'postCardId',
  },
});
Zyklische Importe: Beide Relationsfelder referenzieren gegenseitig den universalIdentifier des jeweils anderen. Um Probleme mit zyklischen Importen zu vermeiden, exportieren Sie Ihre Feld-IDs als benannte Konstanten aus jeder Datei und importieren Sie sie in der jeweils anderen Datei. Das Build-System löst dies zur Kompilierzeit auf.

Relationen zu Standardobjekten

Um eine Relation mit einem integrierten Twenty-Objekt (Person, Company usw.) zu erstellen, verwenden Sie STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS:
src/fields/person-on-self-hosting-user.field.ts
import {
  defineField,
  FieldType,
  RelationType,
  OnDeleteAction,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';

export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';

export default defineField({
  universalIdentifier: PERSON_FIELD_ID,
  objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'person',
  label: 'Person',
  description: 'Person matching with the self hosting user',
  isNullable: true,
  relationTargetObjectMetadataUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
  relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.SET_NULL,
    joinColumnName: 'personId',
  },
});

Eigenschaften von Relationsfeldern

EigenschaftErforderlichBeschreibung
typeJaMuss FieldType.RELATION sein
relationTargetObjectMetadataUniversalIdentifierJaDer universalIdentifier des Zielobjekts
relationTargetFieldMetadataUniversalIdentifierJaDer universalIdentifier des entsprechenden Felds auf dem Zielobjekt
universalSettings.relationTypeJaRelationType.MANY_TO_ONE oder RelationType.ONE_TO_MANY
universalSettings.onDeleteNur für MANY_TO_ONEWas passiert, wenn der referenzierte Datensatz gelöscht wird: CASCADE, SET_NULL, RESTRICT oder NO_ACTION
universalSettings.joinColumnNameNur für MANY_TO_ONEDatenbankspaltenname für den Fremdschlüssel (z. B. postCardId)

Inline-Relationsfelder in defineObject

Sie können Relationsfelder auch direkt innerhalb von defineObject() definieren. In diesem Fall lassen Sie objectUniversalIdentifier weg — er wird vom übergeordneten Objekt geerbt:
export default defineObject({
  universalIdentifier: '...',
  nameSingular: 'postCardRecipient',
  // ...
  fields: [
    {
      universalIdentifier: POST_CARD_FIELD_ID,
      type: FieldType.RELATION,
      name: 'postCard',
      label: 'Post Card',
      relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
      relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
      universalSettings: {
        relationType: RelationType.MANY_TO_ONE,
        onDelete: OnDeleteAction.CASCADE,
        joinColumnName: 'postCardId',
      },
    },
    // ... other fields
  ],
});
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';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';

const handler = async (params: RoutePayload) => {
  const client = new CoreApiClient();
  const name = 'name' in params.queryStringParameters
    ? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
    : '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: 'GET',
    isAuthRequired: false,
  },
  /*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
  • 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 exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Sie können Protokolle mit folgendem Befehl ansehen:
yarn twenty 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:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';

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 }
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']).

Eine Funktion als Tool bereitstellen

Logikfunktionen können als Tools für KI-Agenten und Workflows verfügbar gemacht werden. Wenn eine Funktion als Tool markiert ist, wird sie von den KI-Funktionen von Twenty auffindbar und kann in Workflow-Automatisierungen verwendet werden.Um eine Logikfunktion als Tool zu markieren, setzen Sie isTool: true:
src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
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,
  isTool: true,
});
Hauptpunkte:
  • Sie können isTool mit Triggern kombinieren — eine Funktion kann gleichzeitig sowohl ein Tool (von KI-Agenten aufrufbar) als auch durch Ereignisse ausgelöst werden.
  • toolInputSchema (optional): Ein JSON-Schema-Objekt, das die Parameter beschreibt, die Ihre Funktion akzeptiert. Das Schema wird automatisch durch statische Analyse des Quellcodes ermittelt, Sie können es jedoch auch explizit festlegen:
export default defineLogicFunction({
  ...,
  toolInputSchema: {
    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.
Eine Pre-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, bevor Ihre App in einem Arbeitsbereich installiert wird. Dies ist nützlich für Validierungsaufgaben, Überprüfungen von Voraussetzungen oder die Vorbereitung des Status des Arbeitsbereichs, bevor die Hauptinstallation fortgesetzt wird.
src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';

const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
  console.log('Pre install logic function executed successfully!', payload.previousVersion);
};

export default definePreInstallLogicFunction({
  universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
  name: 'pre-install',
  description: 'Runs before installation to prepare the application.',
  timeoutSeconds: 300,
  handler,
});
Sie können die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
yarn twenty exec --preInstall
Hauptpunkte:
  • Pre-Installationsfunktionen verwenden definePreInstallLogicFunction() — eine spezialisierte Variante, die Trigger-Einstellungen (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool) weglässt.
  • Der Handler erhält ein InstallLogicFunctionPayload mit { previousVersion: string } — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
  • Pro Anwendung ist nur eine Pre-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
  • Der universalIdentifier der Funktion wird während des Builds im Anwendungsmanifest automatisch als preInstallLogicFunctionUniversalIdentifier gesetzt — Sie müssen ihn nicht in defineApplication() referenzieren.
  • Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Vorbereitungsvorgänge zu ermöglichen.
Eine Post-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, nachdem Ihre App in einem Arbeitsbereich installiert wurde. Dies ist nützlich für einmalige Einrichtungsvorgänge wie das Befüllen mit Standarddaten, das Erstellen erster Datensätze oder das Konfigurieren von Arbeitsbereichseinstellungen.
src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';

const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
  console.log('Post install logic function executed successfully!', payload.previousVersion);
};

export default definePostInstallLogicFunction({
  universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 300,
  handler,
});
Sie können die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
yarn twenty exec --postInstall
Hauptpunkte:
  • Post-Installationsfunktionen verwenden definePostInstallLogicFunction() — eine spezialisierte Variante, die Trigger-Einstellungen (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool) weglässt.
  • Der Handler erhält ein InstallLogicFunctionPayload mit { previousVersion: string } — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
  • Pro Anwendung ist nur eine Post-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
  • Der universalIdentifier der Funktion wird während des Builds im Anwendungsmanifest automatisch als postInstallLogicFunctionUniversalIdentifier gesetzt — Sie müssen ihn nicht in defineApplication() referenzieren.
  • Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
Frontend-Komponenten sind React-Komponenten, die direkt innerhalb der Twenty-UI gerendert werden. Sie laufen in einem isolierten Web Worker unter Verwendung von Remote DOM — Ihr Code wird in einer Sandbox ausgeführt, rendert jedoch nativ auf der Seite, nicht in einem iframe.

Wo Front-Komponenten verwendet werden können

Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
  • Seitenpanel — Nicht-Headless-Front-Komponenten werden im rechten Seitenpanel geöffnet. Dies ist das Standardverhalten, wenn eine Front-Komponente über das Befehlsmenü ausgelöst wird.
  • Widgets (Dashboards und Datensatzseiten) — Front-Komponenten können als Widgets in Seitenlayouts eingebettet werden. Beim Konfigurieren eines Dashboards oder eines Datensatzseiten-Layouts können Benutzer ein Front-Komponenten-Widget hinzufügen.

Einfaches Beispiel

Der schnellste Weg, eine Frontend-Komponente in Aktion zu sehen, ist, sie als Befehl zu registrieren. Das Hinzufügen eines command-Felds mit isPinned: true lässt sie als Schnellaktionsschaltfläche oben rechts auf der Seite erscheinen — kein Seitenlayout erforderlich:
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk';

const HelloWorld = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Hello from my app!</h1>
      <p>This component renders inside Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
  command: {
    universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
    shortLabel: 'Hello',
    label: 'Hello World',
    icon: 'IconBolt',
    isPinned: true,
    availabilityType: 'GLOBAL',
  },
});
Nach dem Synchronisieren mit yarn twenty dev erscheint die Schnellaktion oben rechts auf der Seite:
Schnellaktionsschaltfläche oben rechts
Klicken Sie darauf, um die Komponente inline zu rendern.

Konfigurationsfelder

FeldErforderlichBeschreibung
universalIdentifierJaStabile eindeutige ID für diese Komponente
componentJaEine React-Komponentenfunktion
nameNeinAnzeigename
descriptionNeinBeschreibung dessen, was die Komponente macht
isHeadlessNeinAuf true setzen, wenn die Komponente keine sichtbare UI hat (siehe unten)
commandNeinDie Komponente als Befehl registrieren (siehe unten Befehlsoptionen)

Eine Frontend-Komponente auf einer Seite platzieren

Über Befehle hinaus können Sie eine Frontend-Komponente direkt in eine Datensatzseite einbetten, indem Sie sie als Widget in einem Seitenlayout hinzufügen. Details finden Sie im Abschnitt definePageLayout.

Headless vs. Nicht-Headless

Front-Komponenten gibt es in zwei Rendering-Modi, die durch die Option isHeadless gesteuert werden:Nicht-Headless (Standard) — Die Komponente rendert eine sichtbare UI. Wird sie über das Befehlsmenü ausgelöst, öffnet sie sich im Seitenpanel. Dies ist das Standardverhalten, wenn isHeadless false ist oder weggelassen wird.Headless (isHeadless: true) — Die Komponente wird unsichtbar im Hintergrund gemountet. Sie öffnet das Seitenpanel nicht. Headless-Komponenten sind für Aktionen konzipiert, die Logik ausführen und sich anschließend selbst unmounten — zum Beispiel das Ausführen einer asynchronen Aufgabe, das Navigieren zu einer Seite oder das Anzeigen eines Bestätigungsdialogs. Sie lassen sich gut mit den unten beschriebenen SDK-Command-Komponenten kombinieren.
src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  useEffect(() => {
    enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
  }, [recordId]);

  return null;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-tracker',
  description: 'Tracks record views silently',
  isHeadless: true,
  component: SyncTracker,
});
Da die Komponente null zurückgibt, überspringt Twenty das Rendern eines Containers dafür — im Layout entsteht kein Leerraum. Die Komponente hat dennoch Zugriff auf alle Hooks und die Host-Kommunikations-API.

SDK-Command-Komponenten

Das Paket twenty-sdk stellt vier Command-Hilfskomponenten bereit, die für Headless-Front-Komponenten ausgelegt sind. Jede Komponente führt beim Mounten eine Aktion aus, behandelt Fehler durch Anzeige einer Snackbar-Benachrichtigung und unmountet die Front-Komponente nach Abschluss automatisch.Importieren Sie sie aus twenty-sdk/command:
  • Command — Führt einen asynchronen Callback über das Prop execute aus.
  • CommandLink — Navigiert zu einem App-Pfad. Props: to, params, queryParams, options.
  • CommandModal — Öffnet einen Bestätigungsdialog. Bestätigt der Benutzer, wird der Callback execute ausgeführt. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Öffnet eine bestimmte Seite im Seitenpanel. Props: page, pageTitle, pageIcon.
Hier ist ein vollständiges Beispiel einer Headless-Front-Komponente, die Command verwendet, um eine Aktion aus dem Befehlsmenü auszuführen:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
  command: {
    universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
    label: 'Run my action',
    icon: 'IconPlayerPlay',
  },
});
Und ein Beispiel, das CommandModal verwendet, um vor der Ausführung um Bestätigung zu bitten:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
  command: {
    universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
    label: 'Delete draft',
    icon: 'IconTrash',
  },
});

Zugriff auf den Laufzeitkontext

Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
src/front-components/record-info.tsx
import {
  defineFrontComponent,
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
Verfügbare Hooks:
HookGibt zurückBeschreibung
useUserId()string oder nullDie ID des aktuellen Benutzers
useRecordId()string oder nullDie ID des aktuellen Datensatzes (wenn auf einer Datensatzseite platziert)
useFrontComponentId()stringDie ID dieser Komponenteninstanz
useFrontComponentExecutionContext(selector)variiertZugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion

Host-Kommunikations-API

Frontend-Komponenten können Navigation, Modals und Benachrichtigungen mittels Funktionen aus twenty-sdk auslösen:
FunktionBeschreibung
navigate(to, params?, queryParams?, options?)Zu einer Seite in der App navigieren
openSidePanelPage(params)Ein Seitenpanel öffnen
closeSidePanel()Seitenpanel schließen
openCommandConfirmationModal(params)Einen Bestätigungsdialog anzeigen
enqueueSnackbar(params)Eine Toast-Benachrichtigung anzeigen
unmountFrontComponent()Die Komponente entfernen
updateProgress(progress)Einen Fortschrittsindikator aktualisieren
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

Befehlsoptionen

Das Hinzufügen eines command-Felds zu defineFrontComponent registriert die Komponente im Befehlsmenü (Cmd+K). Wenn isPinned true ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
FeldErforderlichBeschreibung
universalIdentifierJaStabile eindeutige ID für den Befehl
labelJaVollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird
shortLabelNeinKürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird
iconNeinNeben dem Label angezeigter Icon-Name (z. B. ‘IconBolt’, ‘IconSend’)
isPinnedNeinBei true wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt
availabilityTypeNeinSteuert, wo der Befehl erscheint: ‘GLOBAL’ (immer verfügbar), ‘RECORD_SELECTION’ (nur wenn Datensätze ausgewählt sind) oder ‘FALLBACK’ (wird angezeigt, wenn keine anderen Befehle passen)
availabilityObjectUniversalIdentifierNeinBeschränken Sie den Befehl auf Seiten eines bestimmten Objekttyps (z. B. nur bei Company-Datensätzen)
conditionalAvailabilityExpressionNeinEin boolescher Ausdruck, um dynamisch zu steuern, ob der Befehl sichtbar ist (siehe unten)

Bedingte Verfügbarkeitsausdrücke

Mit dem Feld conditionalAvailabilityExpression können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus twenty-sdk, um Ausdrücke zu erstellen:
import {
  defineFrontComponent,
  pageType,
  numberOfSelectedRecords,
  objectPermissions,
  everyEquals,
  isDefined,
} from 'twenty-sdk';

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'bulk-action',
  component: BulkAction,
  command: {
    universalIdentifier: '...',
    label: 'Bulk Update',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: everyEquals(
      objectPermissions,
      'canUpdateObjectRecords',
      true,
    ),
  },
});
Kontextvariablen — sie repräsentieren den aktuellen Zustand der Seite:
VariableTypBeschreibung
pageTypestringAktueller Seitentyp (z. B. ‘RecordIndexPage’, ‘RecordShowPage’)
isInSidePanelbooleanOb die Komponente in einem Seitenpanel gerendert wird
numberOfSelectedRecordsnumberAnzahl der aktuell ausgewählten Datensätze
isSelectAllbooleanOb „Alle auswählen“ aktiv ist
selectedRecordsarrayDie ausgewählten Datensatzobjekte
favoriteRecordIdsarrayIDs der favorisierten Datensätze
objectPermissionsobjectBerechtigungen für den aktuellen Objekttyp
targetObjectReadPermissionsobjectLeseberechtigungen für das Zielobjekt
targetObjectWritePermissionsobjectSchreibberechtigungen für das Zielobjekt
featureFlagsobjectAktive Feature-Flags
objectMetadataItemobjectMetadaten des aktuellen Objekttyps
hasAnySoftDeleteFilterOnViewbooleanOb die aktuelle Ansicht einen Soft-Delete-Filter hat
Operatoren — Variablen zu booleschen Ausdrücken kombinieren:
OperatorBeschreibung
isDefined(value)true, wenn der Wert nicht null/undefined ist
isNonEmptyString(value)true, wenn der Wert eine nicht leere Zeichenfolge ist
includes(array, value)true, wenn das Array den Wert enthält
includesEvery(array, prop, value)true, wenn die Eigenschaft jedes Elements den Wert enthält
every(array, prop)true, wenn die Eigenschaft bei jedem Element truthy ist
everyDefined(array, prop)true, wenn die Eigenschaft bei jedem Element definiert ist
everyEquals(array, prop, value)true, wenn die Eigenschaft bei jedem Element dem Wert entspricht
some(array, prop)true, wenn die Eigenschaft bei mindestens einem Element truthy ist
someDefined(array, prop)true, wenn die Eigenschaft bei mindestens einem Element definiert ist
someEquals(array, prop, value)true, wenn die Eigenschaft bei mindestens einem Element dem Wert entspricht
someNonEmptyString(array, prop)true, wenn die Eigenschaft bei mindestens einem Element eine nicht leere Zeichenfolge ist
none(array, prop)true, wenn die Eigenschaft bei jedem Element falsy ist
noneDefined(array, prop)true, wenn die Eigenschaft bei jedem Element undefined ist
noneEquals(array, prop, value)true, wenn die Eigenschaft bei keinem Element dem Wert entspricht

Öffentliche Assets

Frontend-Komponenten können mit getPublicAssetUrl auf Dateien aus dem public/-Verzeichnis der App zugreifen:
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
Details finden Sie im Abschnitt Öffentliche Assets.

Styling

Frontend-Komponenten unterstützen mehrere Styling-Ansätze. Sie können verwenden:
  • Inline-Stylesstyle={{ color: 'red' }}
  • Twenty-UI-Komponenten — Import aus twenty-sdk/ui (Button, Tag, Status, Chip, Avatar und mehr)
  • Emotion — CSS-in-JS mit @emotion/react
  • Styled-componentsstyled.div-Muster
  • Tailwind CSS — Utility-Klassen
  • Beliebige CSS-in-JS-Bibliothek, die mit React kompatibel ist
import { defineFrontComponent } from 'twenty-sdk';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});
Skills definieren wiederverwendbare Anweisungen und Fähigkeiten, die KI-Agenten in Ihrem Arbeitsbereich verwenden können. Verwenden Sie defineSkill(), um Skills mit eingebauter Validierung zu definieren:
src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';

export default defineSkill({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'sales-outreach',
  label: 'Sales Outreach',
  description: 'Guides the AI agent through a structured sales outreach process',
  icon: 'IconBrain',
  content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
Hauptpunkte:
  • name ist eine eindeutige Kennung (als Zeichenfolge) für den Skill (kebab-case empfohlen).
  • label ist der menschenlesbare Anzeigename, der in der UI angezeigt wird.
  • content enthält die Skill-Anweisungen — dies ist der Text, den der KI-Agent verwendet.
  • icon (optional) legt das in der UI angezeigte Symbol fest.
  • description (optional) liefert zusätzlichen Kontext zum Zweck des Skills.
Agenten sind KI-Assistenten, die innerhalb Ihres Workspaces leben. Verwenden Sie defineAgent(), um Agenten mit einem benutzerdefinierten System-Prompt zu erstellen:
src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';

export default defineAgent({
  universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
  name: 'sales-assistant',
  label: 'Sales Assistant',
  description: 'Helps the sales team draft outreach emails and research prospects',
  icon: 'IconRobot',
  prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
Hauptpunkte:
  • name ist die eindeutige Kennzeichnungs-Zeichenfolge für den Agenten (kebab-case empfohlen).
  • label ist der in der UI angezeigte Anzeigename.
  • prompt ist der System-Prompt, der das Verhalten des Agenten definiert.
  • description (optional) liefert Kontext dazu, was der Agent tut.
  • icon (optional) legt das in der UI angezeigte Symbol fest.
  • modelId (optional) überschreibt das vom Agenten verwendete Standard-KI-Modell.
Ansichten sind gespeicherte Konfigurationen dafür, wie Datensätze eines Objekts angezeigt werden — einschließlich sichtbarer Felder, deren Reihenfolge sowie angewendeter Filter oder Gruppen. Verwenden Sie defineView(), um vorkonfigurierte Ansichten mit Ihrer App auszuliefern:
src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';

export default defineView({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'All example items',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconList',
  key: ViewKey.INDEX,
  position: 0,
  fields: [
    {
      universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
      fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
      position: 0,
      isVisible: true,
      size: 200,
    },
  ],
});
Hauptpunkte:
  • objectUniversalIdentifier gibt an, auf welches Objekt diese Ansicht angewendet wird.
  • key bestimmt den Ansichtstyp (z. B. ViewKey.INDEX für die Hauptlistenansicht).
  • fields steuert, welche Spalten erscheinen und in welcher Reihenfolge. Jedes Feld referenziert einen fieldMetadataUniversalIdentifier.
  • Für erweiterte Konfigurationen können Sie außerdem filters, filterGroups, groups und fieldGroups definieren.
  • position steuert die Reihenfolge, wenn mehrere Ansichten für dasselbe Objekt existieren.
Navigationsmenüeinträge fügen der Workspace-Seitenleiste benutzerdefinierte Einträge hinzu. Verwenden Sie defineNavigationMenuItem(), um auf Ansichten, externe URLs oder Objekte zu verlinken:
src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';

export default defineNavigationMenuItem({
  universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
  name: 'example-navigation-menu-item',
  icon: 'IconList',
  color: 'blue',
  position: 0,
  type: NavigationMenuItemType.VIEW,
  viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
Hauptpunkte:
  • type bestimmt, worauf der Menüeintrag verweist: NavigationMenuItemType.VIEW für eine gespeicherte Ansicht oder NavigationMenuItemType.LINK für eine externe URL.
  • Für Ansichtslinks setzen Sie viewUniversalIdentifier. Für externe Links setzen Sie link.
  • position steuert die Reihenfolge in der Seitenleiste.
  • icon und color (optional) passen das Erscheinungsbild an.
Seitenlayouts ermöglichen es Ihnen, das Aussehen einer Datensatzdetailseite anzupassen — welche Tabs erscheinen, welche Widgets sich in jedem Tab befinden und wie sie angeordnet sind. Verwenden Sie definePageLayout(), um benutzerdefinierte Layouts mit Ihrer App auszuliefern:
src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';

export default definePageLayout({
  universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
  name: 'Example Record Page',
  type: 'RECORD_PAGE',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  tabs: [
    {
      universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
      title: 'Hello World',
      position: 50,
      icon: 'IconWorld',
      layoutMode: PageLayoutTabLayoutMode.CANVAS,
      widgets: [
        {
          universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
          title: 'Hello World',
          type: 'FRONT_COMPONENT',
          configuration: {
            configurationType: 'FRONT_COMPONENT',
            frontComponentUniversalIdentifier:
              HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
          },
        },
      ],
    },
  ],
});
Hauptpunkte:
  • type ist typischerweise 'RECORD_PAGE', um die Detailansicht eines bestimmten Objekts anzupassen.
  • objectUniversalIdentifier gibt an, auf welches Objekt dieses Layout angewendet wird.
  • Jeder tab definiert einen Abschnitt der Seite mit title, position und layoutMode (CANVAS für ein freies Layout).
  • Jedes widget innerhalb eines Tabs kann eine Frontend-Komponente, eine Relationenliste oder andere eingebaute Widget-Typen rendern.
  • position auf Tabs steuert deren Reihenfolge. Verwenden Sie höhere Werte (z. B. 50), um benutzerdefinierte Tabs hinter den integrierten zu platzieren.

Öffentliche Assets (Ordner public/)

Der Ordner public/ im Stammverzeichnis Ihrer App enthält statische Dateien — Bilder, Icons, Schriftarten oder sonstige Assets, die Ihre App zur Laufzeit benötigt. Diese Dateien werden automatisch in Builds aufgenommen, während des Dev-Modus synchronisiert und auf den Server hochgeladen. Für Dateien im Verzeichnis public/ gilt:
  • Öffentlich zugänglich — nach der Synchronisierung mit dem Server werden Assets unter einer öffentlichen URL bereitgestellt. Zum Zugriff ist keine Authentifizierung erforderlich.
  • In Frontend-Komponenten verfügbar — verwenden Sie Asset-URLs, um Bilder, Icons oder andere Medien in Ihren React-Komponenten anzuzeigen.
  • In Logikfunktionen verfügbar — referenzieren Sie Asset-URLs in E-Mails, API-Antworten oder in beliebiger serverseitiger Logik.
  • Für Marketplace-Metadaten verwendet — die Felder logoUrl und screenshots in defineApplication() referenzieren Dateien aus diesem Ordner (z. B. public/logo.png). Diese werden im Marketplace angezeigt, wenn Ihre App veröffentlicht wird.
  • Im Dev-Modus automatisch synchronisiert — wenn Sie in public/ eine Datei hinzufügen, aktualisieren oder löschen, wird sie automatisch mit dem Server synchronisiert. Kein Neustart erforderlich.
  • In Builds enthaltenyarn twenty build bündelt alle öffentlichen Assets in der Distributionsausgabe.

Zugriff auf öffentliche Assets mit getPublicAssetUrl

Verwenden Sie den Helper getPublicAssetUrl aus twenty-sdk, um die vollständige URL einer Datei in Ihrem public/-Verzeichnis zu erhalten. Dies funktioniert sowohl in Logikfunktionen als auch in Frontend-Komponenten. In einer Logikfunktion:
src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk';

const handler = async (): Promise<any> => {
  const logoUrl = getPublicAssetUrl('logo.png');
  const invoiceUrl = getPublicAssetUrl('templates/invoice.png');

  // Fetch the file content (no auth required — public endpoint)
  const response = await fetch(invoiceUrl);
  const buffer = await response.arrayBuffer();

  return { logoUrl, size: buffer.byteLength };
};

export default defineLogicFunction({
  universalIdentifier: 'a1b2c3d4-...',
  name: 'send-invoice',
  description: 'Sends an invoice with the app logo',
  timeoutSeconds: 10,
  handler,
});
In einer Frontend-Komponente:
src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

export default defineFrontComponent(() => {
  const logoUrl = getPublicAssetUrl('logo.png');

  return <img src={logoUrl} alt="App logo" />;
});
Das Argument path ist relativ zum public/-Ordner Ihrer App. Sowohl getPublicAssetUrl('logo.png') als auch getPublicAssetUrl('public/logo.png') ergeben dieselbe URL — das Präfix public/ wird, falls vorhanden, automatisch entfernt.

Verwendung von npm-Paketen

Sie können in Ihrer App beliebige npm-Pakete installieren und verwenden. Sowohl Logikfunktionen als auch Frontend-Komponenten werden mit esbuild gebündelt, das alle Abhängigkeiten in die Ausgabe einbettet — zur Laufzeit sind keine node_modules erforderlich.

Ein Paket installieren

yarn add axios
Importieren Sie es anschließend in Ihrem Code:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk';
import axios from 'axios';

const handler = async (): Promise<any> => {
  const { data } = await axios.get('https://api.example.com/data');

  return { data };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-data',
  description: 'Fetches data from an external API',
  timeoutSeconds: 10,
  handler,
});
Dasselbe funktioniert für Frontend-Komponenten:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { format } from 'date-fns';

const DateWidget = () => {
  return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'date-widget',
  component: DateWidget,
});

Wie das Bundling funktioniert

Der Build-Schritt (yarn twenty dev oder yarn twenty build) verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Alle importierten Pakete werden in das Bundle eingebettet. Logikfunktionen laufen in einer Node.js-Umgebung. Eingebaute Node.js-Module (fs, path, crypto, http usw.) stehen zur Verfügung und müssen nicht installiert werden. Frontend-Komponenten laufen in einem Web Worker. Eingebaute Node.js-Module sind nicht verfügbar — nur Browser-APIs und npm-Pakete, die in einer Browserumgebung funktionieren. In beiden Umgebungen stehen twenty-client-sdk/core und twenty-client-sdk/metadata als vorab bereitgestellte Module zur Verfügung — sie werden nicht gebündelt, sondern zur Laufzeit vom Server aufgelöst.

Entitäten mit yarn twenty add erstellen

Anstatt Entitätsdateien manuell zu erstellen, können Sie den interaktiven Scaffolder verwenden:
yarn twenty add
Dies fordert Sie auf, einen Entitätstyp auszuwählen, und führt Sie durch die erforderlichen Felder. Er erzeugt eine einsatzbereite Datei mit einem stabilen universalIdentifier und dem korrekten defineEntity()-Aufruf. Sie können den Entitätstyp auch direkt übergeben, um die erste Eingabeaufforderung zu überspringen:
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent

Verfügbare Entitätstypen

EntitätstypBefehlGenerierte Datei
Objektyarn twenty add objectsrc/objects/<name>.ts
Feldyarn twenty add fieldsrc/fields/<name>.ts
Logikfunktionyarn twenty add logicFunctionsrc/logic-functions/<name>.ts
Frontend-Komponenteyarn twenty add frontComponentsrc/front-components/<name>.tsx
Rolleyarn twenty add rolesrc/roles/<name>.ts
Skillyarn twenty add skillsrc/skills/<name>.ts
Agentyarn twenty add agentsrc/agents/<name>.ts
Ansichtyarn twenty add viewsrc/views/<name>.ts
Navigationsmenüeintragyarn twenty add navigationMenuItemsrc/navigation-menu-items/<name>.ts
Seitenlayoutyarn twenty add pageLayoutsrc/page-layouts/<name>.ts

Was der Scaffolder generiert

Jeder Entitätstyp hat seine eigene Vorlage. Zum Beispiel fragt yarn twenty add object nach:
  1. Name (Singular) — z. B. invoice
  2. Name (Plural) — z. B. invoices
  3. Label (Singular) — automatisch aus dem Namen befüllt (z. B. Invoice)
  4. Label (Plural) — automatisch befüllt (z. B. Invoices)
  5. Ansicht und Navigationseintrag erstellen? — wenn Sie mit Ja antworten, erzeugt der Scaffolder außerdem eine passende Ansicht und einen Sidebar-Link für das neue Objekt.
Andere Entitätstypen haben einfachere Eingabeaufforderungen — die meisten fragen nur nach einem Namen. Der Entitätstyp field ist detaillierter: Er fragt nach Feldname, Label, Typ (aus einer Liste aller verfügbaren Feldtypen wie TEXT, NUMBER, SELECT, RELATION usw.) sowie dem universalIdentifier des Zielobjekts.

Benutzerdefinierter Ausgabepfad

Verwenden Sie den Schalter --path, um die generierte Datei an einem benutzerdefinierten Ort abzulegen:
yarn twenty add logicFunction --path src/custom-folder

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 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 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, auf die in defaultRoleUniversalIdentifier in Ihrer application-config.ts verwiesen wird.

Ihre App testen

Das SDK stellt programmgesteuerte APIs bereit, mit denen Sie Ihre App aus Testcode heraus bauen, bereitstellen, installieren und deinstallieren können. In Kombination mit Vitest und den typisierten API-Clients können Sie Integrationstests schreiben, die prüfen, dass Ihre App End-to-End gegen einen echten Twenty-Server funktioniert.

Einrichtung

Die erzeugte App enthält bereits Vitest. Wenn Sie es manuell einrichten, installieren Sie die Abhängigkeiten:
yarn add -D vitest vite-tsconfig-paths
Erstellen Sie eine vitest.config.ts im Stammverzeichnis Ihrer App:
vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['tsconfig.spec.json'],
      ignoreConfigErrors: true,
    }),
  ],
  test: {
    testTimeout: 120_000,
    hookTimeout: 120_000,
    include: ['src/**/*.integration-test.ts'],
    setupFiles: ['src/__tests__/setup-test.ts'],
    env: {
      TWENTY_API_URL: 'http://localhost:2020',
      TWENTY_API_KEY: 'your-api-key',
    },
  },
});
Erstellen Sie eine Setup-Datei, die vor dem Testlauf überprüft, dass der Server erreichbar ist:
src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';

const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');

beforeAll(async () => {
  // Verify the server is running
  const response = await fetch(`${TWENTY_API_URL}/healthz`);

  if (!response.ok) {
    throw new Error(
      `Twenty server is not reachable at ${TWENTY_API_URL}. ` +
        'Start the server before running integration tests.',
    );
  }

  // Write a temporary config for the SDK
  fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });

  fs.writeFileSync(
    path.join(TEST_CONFIG_DIR, 'config.json'),
    JSON.stringify({
      remotes: {
        local: {
          apiUrl: process.env.TWENTY_API_URL,
          apiKey: process.env.TWENTY_API_KEY,
        },
      },
      defaultRemote: 'local',
    }, null, 2),
  );
});

Programmgesteuerte SDK-APIs

Der Subpfad twenty-sdk/cli exportiert Funktionen, die Sie direkt aus Testcode aufrufen können:
FunktionBeschreibung
appBuildDie App bauen und optional ein Tarball packen
appDeployEin Tarball auf den Server hochladen
appInstallDie App im aktiven Arbeitsbereich installieren
appUninstallDie App aus dem aktiven Arbeitsbereich deinstallieren
Jede Funktion gibt ein Ergebnisobjekt mit success: boolean und entweder data oder error zurück.

Einen Integrationstest schreiben

Hier ist ein vollständiges Beispiel, das die App baut, bereitstellt und installiert und anschließend prüft, dass sie im Arbeitsbereich erscheint:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const APP_PATH = process.cwd();

describe('App installation', () => {
  beforeAll(async () => {
    const buildResult = await appBuild({
      appPath: APP_PATH,
      tarball: true,
      onProgress: (message: string) => console.log(`[build] ${message}`),
    });

    if (!buildResult.success) {
      throw new Error(`Build failed: ${buildResult.error?.message}`);
    }

    const deployResult = await appDeploy({
      tarballPath: buildResult.data.tarballPath!,
      onProgress: (message: string) => console.log(`[deploy] ${message}`),
    });

    if (!deployResult.success) {
      throw new Error(`Deploy failed: ${deployResult.error?.message}`);
    }

    const installResult = await appInstall({ appPath: APP_PATH });

    if (!installResult.success) {
      throw new Error(`Install failed: ${installResult.error?.message}`);
    }
  });

  afterAll(async () => {
    await appUninstall({ appPath: APP_PATH });
  });

  it('should find the installed app in the workspace', async () => {
    const metadataClient = new MetadataApiClient();

    const result = await metadataClient.query({
      findManyApplications: {
        id: true,
        name: true,
        universalIdentifier: true,
      },
    });

    const installedApp = result.findManyApplications.find(
      (app: { universalIdentifier: string }) =>
        app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
    );

    expect(installedApp).toBeDefined();
  });
});

Tests ausführen

Stellen Sie sicher, dass Ihr lokaler Twenty-Server läuft, und führen Sie dann Folgendes aus:
yarn test
Oder im Watch-Modus während der Entwicklung:
yarn test:watch

Typprüfung

Sie können die Typprüfung Ihrer App auch ohne Tests ausführen:
yarn twenty typecheck
Dies führt tsc --noEmit aus und meldet etwaige Typfehler.

CLI-Referenz

Zusätzlich zu dev, build, add und typecheck bietet die CLI Befehle zum Ausführen von Funktionen, Anzeigen von Logs und Verwalten von App-Installationen.

Funktionen ausführen (yarn twenty exec)

Eine Logikfunktion manuell ausführen, ohne sie über HTTP, Cron oder ein Datenbankereignis auszulösen:
# Execute by function name
yarn twenty exec -n create-new-post-card

# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf

# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'

# Execute pre-install or post-install functions
yarn twenty exec --preInstall
yarn twenty exec --postInstall

Funktionsprotokolle ansehen (yarn twenty logs)

Ausführungsprotokolle für die Logikfunktionen Ihrer App streamen:
# Stream all function logs
yarn twenty logs

# Filter by function name
yarn twenty logs -n create-new-post-card

# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Dies unterscheidet sich von yarn twenty server logs, das die Docker-Container-Logs anzeigt. yarn twenty logs zeigt die Funktionsausführungsprotokolle Ihrer App vom Twenty-Server.

Eine App deinstallieren (yarn twenty uninstall)

Entfernen Sie Ihre App aus dem aktiven Arbeitsbereich:
yarn twenty uninstall

# Skip the confirmation prompt
yarn twenty uninstall --yes

Remotes verwalten

Ein Remote ist ein Twenty-Server, mit dem sich Ihre App verbindet. Während der Einrichtung erstellt das Scaffolding-Tool automatisch eines für Sie. Sie können jederzeit weitere Remotes hinzufügen oder zwischen ihnen wechseln.
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add

# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local

# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote

# List all configured remotes
yarn twenty remote list

# Switch the active remote
yarn twenty remote switch <name>
Ihre Anmeldedaten werden in ~/.twenty/config.json gespeichert.

CI mit GitHub Actions

Das Scaffolding-Tool erzeugt einen einsatzbereiten GitHub-Actions-Workflow in .github/workflows/ci.yml. Er führt Ihre Integrationstests automatisch bei jedem Push auf main und bei Pull Requests aus. Der Workflow:
  1. Checkt Ihren Code aus
  2. Startet einen temporären Twenty-Server mit der Aktion twentyhq/twenty/.github/actions/spawn-twenty-docker-image
  3. Installiert Abhängigkeiten mit yarn install --immutable
  4. Führt yarn test aus, wobei TWENTY_API_URL und TWENTY_API_KEY aus den Aktionsausgaben injiziert werden.
.github/workflows/ci.yml
name: CI

on:
  push:
    branches:
      - main
  pull_request: {}

env:
  TWENTY_VERSION: latest

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Spawn Twenty instance
        id: twenty
        uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
        with:
          twenty-version: ${{ env.TWENTY_VERSION }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Enable Corepack
        run: corepack enable

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --immutable

      - name: Run integration tests
        run: yarn test
        env:
          TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
          TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
Sie müssen keine Secrets konfigurieren — die Aktion spawn-twenty-docker-image startet einen flüchtigen Twenty-Server direkt im Runner und gibt die Verbindungsdetails aus. Das Secret GITHUB_TOKEN wird automatisch von GitHub bereitgestellt. Um eine bestimmte Twenty-Version statt latest festzulegen, ändern Sie die Umgebungsvariable TWENTY_VERSION oben im Workflow.