1384 lines
69 KiB
Plaintext
1384 lines
69 KiB
Plaintext
---
|
||
title: Twenty-Apps
|
||
description: Twenty-Anpassungen als Code erstellen und verwalten.
|
||
---
|
||
|
||
<Warning>
|
||
Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfähig, entwickelt sich jedoch noch weiter.
|
||
</Warning>
|
||
|
||
## Was sind Apps?
|
||
|
||
Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und Logikfunktionen im Code — das beschleunigt Entwicklung, Wartung und den Rollout auf mehrere Workspaces.
|
||
|
||
**Was Sie heute tun können:**
|
||
|
||
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
|
||
* Logikfunktionen mit benutzerdefinierten Triggern erstellen
|
||
* Skills und Agenten für KI definieren
|
||
* Dieselbe App in mehreren Workspaces bereitstellen
|
||
|
||
## Voraussetzungen
|
||
|
||
* Node.js 24+ und Yarn 4
|
||
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
|
||
|
||
## Erste Schritte
|
||
|
||
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
|
||
|
||
```bash filename="Terminal"
|
||
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
|
||
npx create-twenty-app@latest my-twenty-app
|
||
cd my-twenty-app
|
||
|
||
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
|
||
yarn twenty app:dev
|
||
```
|
||
|
||
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||
|
||
```bash filename="Terminal"
|
||
# Standard (umfassend): alle Beispiele (Objekt, Feld, Logikfunktion, Frontend-Komponente, View, Navigationsmenüeintrag, Skill, Agent)
|
||
npx create-twenty-app@latest my-app
|
||
|
||
# Minimal: nur Kerndateien (application-config.ts und default-role.ts)
|
||
npx create-twenty-app@latest my-app --minimal
|
||
```
|
||
|
||
Von hier aus können Sie:
|
||
|
||
```bash filename="Terminal"
|
||
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
|
||
yarn twenty entity:add
|
||
|
||
# Die Funktionsprotokolle Ihrer Anwendung überwachen
|
||
yarn twenty function:logs
|
||
|
||
# Eine Funktion anhand ihres Namens ausführen
|
||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||
|
||
# Die Pre-Installationsfunktion ausführen
|
||
yarn twenty function:execute --preInstall
|
||
|
||
# Die Post-Installationsfunktion ausführen
|
||
yarn twenty function:execute --postInstall
|
||
|
||
# Die Anwendung für die Verteilung erstellen
|
||
yarn twenty app:build
|
||
|
||
# Die Anwendung auf npm oder einen Twenty-Server veröffentlichen
|
||
yarn twenty app:publish
|
||
|
||
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
|
||
yarn twenty app:uninstall
|
||
|
||
# Hilfe zu Befehlen anzeigen
|
||
yarn twenty help
|
||
```
|
||
|
||
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||
|
||
## Projektstruktur (vom Scaffolder erzeugt)
|
||
|
||
Wenn Sie `npx create-twenty-app@latest my-twenty-app` ausführen, erledigt der Scaffolder Folgendes:
|
||
|
||
* Kopiert eine minimale Basisanwendung nach `my-twenty-app/`
|
||
* Fügt eine lokale `twenty-sdk`-Abhängigkeit und die Yarn-4-Konfiguration hinzu
|
||
* Erstellt Konfigurationsdateien und Skripte, die an die `twenty`-CLI angebunden sind
|
||
* Erzeugt Kerndateien (Anwendungskonfiguration, Standardrolle für Logikfunktionen, Pre-Installations- und Post-Installationsfunktionen) sowie Beispieldateien entsprechend dem Scaffolding-Modus
|
||
|
||
Eine frisch erstellte App mit dem Standardmodus `--exhaustive` sieht so aus:
|
||
|
||
```text filename="my-twenty-app/"
|
||
my-twenty-app/
|
||
package.json
|
||
yarn.lock
|
||
.gitignore
|
||
.nvmrc
|
||
.yarnrc.yml
|
||
.yarn/
|
||
install-state.gz
|
||
.oxlintrc.json
|
||
tsconfig.json
|
||
README.md
|
||
public/ # Ordner für öffentliche Assets (Bilder, Schriftarten usw.)
|
||
src/
|
||
├── application-config.ts # Erforderlich - Hauptkonfiguration der Anwendung
|
||
├── roles/
|
||
│ └── default-role.ts # Standardrolle für Logikfunktionen
|
||
├── objects/
|
||
│ └── example-object.ts # Beispiel für eine benutzerdefinierte Objektdefinition
|
||
├── fields/
|
||
│ └── example-field.ts # Beispiel für eine eigenständige Felddefinition
|
||
├── logic-functions/
|
||
│ ├── hello-world.ts # Beispiel für eine Logikfunktion
|
||
│ ├── pre-install.ts # Pre-Installations-Logikfunktion
|
||
│ └── post-install.ts # Post-Installations-Logikfunktion
|
||
├── front-components/
|
||
│ └── hello-world.tsx # Beispiel für eine Frontend-Komponente
|
||
├── views/
|
||
│ └── example-view.ts # Beispiel für eine gespeicherte View-Definition
|
||
├── navigation-menu-items/
|
||
│ └── example-navigation-menu-item.ts # Beispiel für einen Navigationslink in der Seitenleiste
|
||
├── skills/
|
||
│ └── example-skill.ts # Beispiel für eine Skill-Definition eines KI-Agenten
|
||
└── agents/
|
||
└── example-agent.ts # Beispiel für eine KI-Agenten-Definition
|
||
```
|
||
|
||
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` und `logic-functions/post-install.ts`).
|
||
|
||
Auf hoher Ebene:
|
||
|
||
* **package.json**: Deklariert den App-Namen, die Version und die Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie ein `twenty`-Skript hinzu, das an die lokale `twenty`-CLI delegiert. Führe `yarn twenty help` aus, um alle verfügbaren Befehle aufzulisten.
|
||
* **.gitignore**: Ignoriert übliche Artefakte wie `node_modules`, `.yarn`, `generated/` (typisierter Client), `dist/`, `build/`, Coverage-Ordner, Logdateien und `.env*`-Dateien.
|
||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Fixieren und konfigurieren die vom Projekt verwendete Yarn-4-Toolchain.
|
||
* **.nvmrc**: Legt die vom Projekt erwartete Node.js-Version fest.
|
||
* **.oxlintrc.json** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
|
||
* **README.md**: Ein kurzes README im App-Root mit grundlegenden Anweisungen.
|
||
* **public/**: Ein Ordner zum Speichern öffentlicher Assets (Bilder, Schriftarten, statische Dateien), die zusammen mit Ihrer Anwendung bereitgestellt werden. Hier abgelegte Dateien werden während der Synchronisierung hochgeladen und sind zur Laufzeit zugänglich.
|
||
* **src/**: Der Hauptort, an dem Sie Ihre Anwendung als Code definieren
|
||
|
||
### Entitätserkennung
|
||
|
||
Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von **`export default define<Entity>({...})`** parst. Für jeden Entitätstyp gibt es eine entsprechende Hilfsfunktion, die aus `twenty-sdk` exportiert wird:
|
||
|
||
| Hilfsfunktion | Entitätstyp |
|
||
| ---------------------------------- | ------------------------------------------------------------------------ |
|
||
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
|
||
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
|
||
| `definePreInstallLogicFunction()` | Pre-Installations-Logikfunktion (wird vor der Installation ausgeführt) |
|
||
| `definePostInstallLogicFunction()` | Post-Installations-Logikfunktion (wird nach der Installation ausgeführt) |
|
||
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
|
||
| `defineRole()` | Rollendefinitionen |
|
||
| `defineField()` | Felderweiterungen für bestehende Objekte |
|
||
| `defineView()` | Gespeicherte View-Definitionen |
|
||
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
|
||
| `defineSkill()` | Skill-Definitionen für KI-Agenten |
|
||
| `defineAgent()` | KI-Agenten-Definitionen |
|
||
|
||
<Note>
|
||
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
|
||
</Note>
|
||
|
||
Beispiel für eine erkannte Entität:
|
||
|
||
```typescript
|
||
// This file can be named anything and placed anywhere in src/
|
||
import { defineObject, FieldType } from 'twenty-sdk';
|
||
|
||
export default defineObject({
|
||
universalIdentifier: '...',
|
||
nameSingular: 'postCard',
|
||
// ... rest of config
|
||
});
|
||
```
|
||
|
||
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
|
||
|
||
* `yarn twenty app:dev` generiert automatisch zwei typisierte API-Clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) und `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`).
|
||
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
|
||
|
||
## Authentifizierung
|
||
|
||
Wenn Sie `yarn twenty auth:login` zum ersten Mal ausführen, werden Sie nach Folgendem gefragt:
|
||
|
||
* API-URL (standardmäßig http://localhost:3000 oder Ihr aktuelles Workspace-Profil)
|
||
* API-Schlüssel
|
||
|
||
Ihre Anmeldedaten werden pro Benutzer in `~/.twenty/config.json` gespeichert. Sie können mehrere Profile verwalten und zwischen ihnen wechseln.
|
||
|
||
### Arbeitsbereiche verwalten
|
||
|
||
```bash filename="Terminal"
|
||
# Login interactively (recommended)
|
||
yarn twenty auth:login
|
||
|
||
# Login to a specific workspace profile
|
||
yarn twenty auth:login --workspace my-custom-workspace
|
||
|
||
# List all configured workspaces
|
||
yarn twenty auth:list
|
||
|
||
# Switch the default workspace (interactive)
|
||
yarn twenty auth:switch
|
||
|
||
# Switch to a specific workspace
|
||
yarn twenty auth:switch production
|
||
|
||
# Check current authentication status
|
||
yarn twenty auth:status
|
||
```
|
||
|
||
Sobald Sie mit `yarn twenty auth:switch` den Arbeitsbereich gewechselt haben, verwenden alle nachfolgenden Befehle standardmäßig diesen Arbeitsbereich. Sie können es weiterhin vorübergehend mit `--workspace <name>` überschreiben.
|
||
|
||
## SDK-Ressourcen verwenden (Typen & Konfiguration)
|
||
|
||
Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie in Ihrer App verwenden. Im Folgenden finden Sie die wichtigsten Bausteine, mit denen Sie am häufigsten arbeiten.
|
||
|
||
### Hilfsfunktionen
|
||
|
||
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
|
||
|
||
| Funktion | Zweck |
|
||
| ---------------------------------- | --------------------------------------------------------------- |
|
||
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
|
||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
|
||
| `definePreInstallLogicFunction()` | Eine Pre-Installations-Logikfunktion definieren (eine pro App) |
|
||
| `definePostInstallLogicFunction()` | Eine Post-Installations-Logikfunktion definieren (eine pro App) |
|
||
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
|
||
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
|
||
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
|
||
| `defineView()` | Gespeicherte Views für Objekte definieren |
|
||
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
|
||
| `defineSkill()` | Definieren Sie Skills für KI-Agenten |
|
||
| `defineAgent()` | Definieren Sie KI-Agenten mit System-Prompts |
|
||
|
||
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
|
||
|
||
### Objekte definieren
|
||
|
||
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:
|
||
|
||
```typescript
|
||
// src/app/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 entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
|
||
|
||
<Note>
|
||
**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.
|
||
</Note>
|
||
|
||
### Felder für bestehende Objekte definieren
|
||
|
||
Verwenden Sie `defineField()`, um benutzerdefinierte Felder zu bestehenden Objekten hinzuzufügen — sowohl zu Standardobjekten (wie `company`, `person`, `opportunity`) als auch zu benutzerdefinierten Objekten, die von anderen Apps definiert werden. Jedes Feld befindet sich in einer eigenen Datei und verweist auf das Zielobjekt über dessen `universalIdentifier`.
|
||
|
||
Um auf Standardobjekte zu verweisen, importieren Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` aus `twenty-sdk`. Diese Konstante stellt stabile Bezeichner für alle integrierten Objekte und deren Felder bereit:
|
||
|
||
```typescript
|
||
// src/fields/apollo-total-funding.field.ts
|
||
import {
|
||
defineField,
|
||
FieldType,
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||
} from 'twenty-sdk';
|
||
|
||
export default defineField({
|
||
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
|
||
objectUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||
type: FieldType.CURRENCY,
|
||
name: 'apolloTotalFunding',
|
||
label: 'Total Funding',
|
||
description: 'Total funding raised by the company',
|
||
icon: 'IconCash',
|
||
});
|
||
```
|
||
|
||
Hauptpunkte:
|
||
|
||
* `objectUniversalIdentifier` teilt Twenty mit, an welches Objekt das Feld angehängt werden soll. Verwenden Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` für Standardobjekte.
|
||
* Jedes Feld benötigt einen eigenen stabilen `universalIdentifier`, `name`, `type`, `label` und den Ziel-`objectUniversalIdentifier`.
|
||
* Sie können mit `yarn twenty entity:add` neue Felder anlegen, indem Sie die Feldoption wählen.
|
||
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` wird der Einfachheit halber auch als `STANDARD_OBJECT` exportiert — beide verweisen auf dieselbe Konstante.
|
||
|
||
Verfügbare Standardobjekte sind unter anderem: `attachment`, `blocklist`, `calendarChannel`, `calendarEvent`, `calendarEventParticipant`, `company`, `connectedAccount`, `dashboard`, `favorite`, `favoriteFolder`, `message`, `messageChannel`, `messageParticipant`, `messageThread`, `note`, `noteTarget`, `opportunity`, `person`, `task`, `taskTarget`, `timelineActivity`, `workflow`, `workflowAutomatedTrigger`, `workflowRun`, `workflowVersion` und `workspaceMember`.
|
||
|
||
Jedes Standardobjekt stellt außerdem seine Feldbezeichner bereit. Beispielsweise, um in Rollenberechtigungen auf ein bestimmtes Feld eines Standardobjekts zu verweisen:
|
||
|
||
```typescript
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
|
||
```
|
||
|
||
#### Beziehungsfelder bei bestehenden Objekten
|
||
|
||
Sie können auch Beziehungsfelder definieren, die bestehende Objekte mit Ihren benutzerdefinierten Objekten verknüpfen:
|
||
|
||
```typescript
|
||
// src/fields/people-on-call-recording.field.ts
|
||
import { defineField, FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
|
||
import { CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/call-recording';
|
||
import { CALL_RECORDING_ON_PERSON_ID } from 'src/fields/call-recording-on-person.field';
|
||
|
||
export default defineField({
|
||
universalIdentifier: '4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
|
||
objectUniversalIdentifier:
|
||
CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
type: FieldType.RELATION,
|
||
name: 'person',
|
||
label: 'Person',
|
||
relationTargetObjectMetadataUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||
relationTargetFieldMetadataUniversalIdentifier:
|
||
CALL_RECORDING_ON_PERSON_ID,
|
||
relationType: RelationType.MANY_TO_ONE,
|
||
});
|
||
```
|
||
|
||
### Anwendungskonfiguration (application-config.ts)
|
||
|
||
Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschreibt:
|
||
|
||
* **Was die App ist**: Bezeichner, Anzeigename und Beschreibung.
|
||
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
|
||
* **(Optional) Variablen**: Schlüssel–Wert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
|
||
* **(Optional) Pre-Installationsfunktion**: eine Logikfunktion, die vor der Installation der App ausgeführt wird.
|
||
* **(Optional) Post-Installationsfunktion**: eine Logikfunktion, die nach der Installation der App ausgeführt wird.
|
||
|
||
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
|
||
|
||
```typescript
|
||
// src/application-config.ts
|
||
import { defineApplication } from 'twenty-sdk';
|
||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||
|
||
export default defineApplication({
|
||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||
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 Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
|
||
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
|
||
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
|
||
* Pre-Installations- und Post-Installationsfunktionen werden während des Manifest-Builds automatisch erkannt. Siehe [Pre-Installationsfunktionen](#pre-install-functions) und [Post-Installationsfunktionen](#post-install-functions).
|
||
|
||
#### Rollen und Berechtigungen
|
||
|
||
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `defaultRoleUniversalIdentifier` in `application-config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
|
||
|
||
* Der zur Laufzeit als `TWENTY_API_KEY` injizierte API-Schlüssel wird von dieser Standard-Funktionsrolle 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, und verweisen Sie dann auf deren universellen Bezeichner.
|
||
|
||
##### Standard-Funktionsrolle (\*.role.ts)
|
||
|
||
Wenn Sie eine neue App erzeugen, erstellt die CLI auch eine Standard-Rolldatei. Verwenden Sie `defineRole()`, um Rollen mit eingebauter Validierung zu definieren:
|
||
|
||
```typescript
|
||
// 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: false,
|
||
canUpdateAllObjectRecords: false,
|
||
canSoftDeleteAllObjectRecords: false,
|
||
canDestroyAllObjectRecords: false,
|
||
canUpdateAllSettings: false,
|
||
canBeAssignedToAgents: false,
|
||
canBeAssignedToUsers: false,
|
||
canBeAssignedToApiKeys: false,
|
||
objectPermissions: [
|
||
{
|
||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||
canReadObjectRecords: true,
|
||
canUpdateObjectRecords: true,
|
||
canSoftDeleteObjectRecords: false,
|
||
canDestroyObjectRecords: false,
|
||
},
|
||
],
|
||
fieldPermissions: [
|
||
{
|
||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||
canReadFieldValue: false,
|
||
canUpdateFieldValue: false,
|
||
},
|
||
],
|
||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||
});
|
||
```
|
||
|
||
Der `universalIdentifier` dieser Rolle wird anschließend in `application-config.ts` als `defaultRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
|
||
|
||
* **\*.role.ts** definiert, was die Standard-Funktionsrolle 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/Felder, die Ihre Funktionen benötigen.
|
||
* `permissionFlags` steuern den Zugriff auf Funktionen auf Plattformebene. Halten Sie sie minimal; fügen Sie nur hinzu, was Sie benötigen.
|
||
* Ein funktionierendes Beispiel finden Sie in der Hello-World-App: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||
|
||
### Konfiguration von Logikfunktionen und Einstiegspunkt
|
||
|
||
Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
|
||
|
||
```typescript
|
||
// src/app/createPostCard.logic-function.ts
|
||
import { defineLogicFunction } from 'twenty-sdk';
|
||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||
import { CoreApiClient, type Person } from 'twenty-sdk/clients';
|
||
|
||
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,
|
||
triggers: [
|
||
// Public HTTP route trigger '/s/post-card/create'
|
||
{
|
||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||
type: 'route',
|
||
path: '/post-card/create',
|
||
httpMethod: 'GET',
|
||
isAuthRequired: false,
|
||
},
|
||
// Cron trigger (CRON pattern)
|
||
// {
|
||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||
// type: 'cron',
|
||
// pattern: '0 0 1 1 *',
|
||
// },
|
||
// Database event trigger
|
||
// {
|
||
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||
// type: 'databaseEvent',
|
||
// eventName: 'person.updated',
|
||
// updatedFields: ['name'],
|
||
// },
|
||
],
|
||
});
|
||
```
|
||
|
||
Häufige Trigger-Typen:
|
||
|
||
* **route**: Stellt Ihre Funktion unter einem HTTP-Pfad und einer Methode **unter dem Endpunkt `/s/`** bereit:
|
||
|
||
> z. B. `path: '/post-card/create',` -> Aufruf unter `<APP_URL>/s/post-card/create`
|
||
|
||
* **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`
|
||
|
||
Notizen:
|
||
|
||
* Das Array `triggers` ist optional. Funktionen ohne Trigger können als von anderen Funktionen aufgerufene Utility-Funktionen verwendet werden.
|
||
* Sie können mehrere Trigger-Typen in einer Funktion kombinieren.
|
||
|
||
### Pre-Installationsfunktionen
|
||
|
||
Eine Pre-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, bevor deine 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.
|
||
|
||
Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Pre-Installationsfunktion unter `src/logic-functions/pre-install.ts` erzeugt:
|
||
|
||
```typescript
|
||
// 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: '<generated-uuid>',
|
||
name: 'pre-install',
|
||
description: 'Runs before installation to prepare the application.',
|
||
timeoutSeconds: 300,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
Du kannst die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty function:execute --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 — du musst ihn nicht in `defineApplication()` referenzieren.
|
||
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Vorbereitungsvorgänge zu ermöglichen.
|
||
* Pre-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform vor der Installation oder manuell über `function:execute --preInstall` aufgerufen.
|
||
|
||
### Post-Installationsfunktionen
|
||
|
||
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.
|
||
|
||
Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Post-Installationsfunktion unter `src/logic-functions/post-install.ts` erzeugt:
|
||
|
||
```typescript
|
||
// 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: '<generated-uuid>',
|
||
name: 'post-install',
|
||
description: 'Runs after installation to set up the application.',
|
||
timeoutSeconds: 300,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
Du kannst die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty function:execute --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 — du musst 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.
|
||
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `function:execute --postInstall` aufgerufen.
|
||
|
||
### Routen-Trigger-Payload
|
||
|
||
<Warning>
|
||
**Breaking Change (v1.16, Januar 2026):** Das Format der Routen-Trigger-Payload hat sich geändert. Vor v1.16 wurden Query-Parameter, Pfadparameter und der Body direkt als Payload gesendet. Ab v1.16 sind sie innerhalb eines strukturierten `RoutePayload`-Objekts verschachtelt.
|
||
|
||
**Vor v1.16:**
|
||
```typescript
|
||
const handler = async (params) => {
|
||
const { param1, param2 } = params; // Direct access
|
||
};
|
||
```
|
||
|
||
**Nach v1.16:**
|
||
```typescript
|
||
const handler = async (event: RoutePayload) => {
|
||
const { param1, param2 } = event.body; // Access via .body
|
||
const { queryParam } = event.queryStringParameters;
|
||
const { id } = event.pathParameters;
|
||
};
|
||
```
|
||
|
||
**So migrieren Sie bestehende Funktionen:** Aktualisieren Sie Ihren Handler, sodass er nicht mehr direkt aus dem params-Objekt destrukturiert, sondern aus `event.body`, `event.queryStringParameters` oder `event.pathParameters`.
|
||
</Warning>
|
||
|
||
Wenn ein Routen-Trigger Ihre Logikfunktion aufruft, erhält sie ein `RoutePayload`-Objekt, das dem AWS HTTP API v2-Format entspricht. Importieren Sie den Typ aus `twenty-sdk`:
|
||
|
||
```typescript
|
||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||
|
||
const handler = async (event: RoutePayload) => {
|
||
// Access request data
|
||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||
|
||
// HTTP method and path are available in requestContext
|
||
const { method, path } = event.requestContext.http;
|
||
|
||
return { message: 'Success' };
|
||
};
|
||
```
|
||
|
||
Der Typ `RoutePayload` hat die folgende Struktur:
|
||
|
||
| Eigenschaft | Typ | Beschreibung |
|
||
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
|
||
| `headers` | `Record<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) |
|
||
| `queryStringParameters` | `Record<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) |
|
||
| `pathParameters` | `Record<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter (z. B. `/users/:id` → `{ id: '123' }`) |
|
||
| `inhalt` | `object \| null` | Geparster Request-Body (JSON) |
|
||
| `isBase64Encoded` | `boolesch` | Gibt an, ob der Body Base64-codiert ist |
|
||
| `requestContext.http.method` | `string` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) |
|
||
| `requestContext.http.path` | `string` | Rohpfad der Anfrage |
|
||
|
||
### Weiterleiten von HTTP-Headern
|
||
|
||
Standardmäßig werden HTTP-Header von eingehenden Anfragen aus Sicherheitsgründen nicht an Ihre Logikfunktion weitergegeben. Um auf bestimmte Header zuzugreifen, listen Sie diese explizit im Array `forwardedRequestHeaders` auf:
|
||
|
||
```typescript
|
||
export default defineLogicFunction({
|
||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||
name: 'webhook-handler',
|
||
handler,
|
||
triggers: [
|
||
{
|
||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||
type: 'route',
|
||
path: '/webhook',
|
||
httpMethod: 'POST',
|
||
isAuthRequired: false,
|
||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
In Ihrem Handler können Sie anschließend auf diese Header zugreifen:
|
||
|
||
```typescript
|
||
const handler = async (event: RoutePayload) => {
|
||
const signature = event.headers['x-webhook-signature'];
|
||
const contentType = event.headers['content-type'];
|
||
|
||
// Validate webhook signature...
|
||
return { received: true };
|
||
};
|
||
```
|
||
|
||
<Note>
|
||
Header-Namen werden in Kleinbuchstaben normalisiert. Greifen Sie mit Schlüsseln in Kleinbuchstaben darauf zu (zum Beispiel `event.headers['content-type']`).
|
||
</Note>
|
||
|
||
Sie können neue Funktionen auf zwei Arten erstellen:
|
||
|
||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Logikfunktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
|
||
* **Manuell**: Erstellen Sie eine neue `*.logic-function.ts`-Datei und verwenden Sie `defineLogicFunction()` nach demselben Muster.
|
||
|
||
### Eine Logikfunktion als Tool markieren
|
||
|
||
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 als Schritt in Workflow-Automatisierungen ausgewählt werden.
|
||
|
||
Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true` und geben Sie ein `toolInputSchema` an, das die erwarteten Eingabeparameter mithilfe von [JSON Schema](https://json-schema.org/) beschreibt:
|
||
|
||
```typescript
|
||
// src/logic-functions/enrich-company.logic-function.ts
|
||
import { defineLogicFunction } from 'twenty-sdk';
|
||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||
|
||
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,
|
||
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'],
|
||
},
|
||
});
|
||
```
|
||
|
||
Hauptpunkte:
|
||
|
||
* **`isTool`** (`boolean`, Standard: `false`): Wenn auf `true` gesetzt, wird die Funktion als Tool registriert und steht KI-Agenten und Workflow-Automatisierungen zur Verfügung.
|
||
* **`toolInputSchema`** (`object`, optional): Ein JSON-Schema-Objekt, das die Parameter beschreibt, die Ihre Funktion akzeptiert. KI-Agenten verwenden dieses Schema, um zu verstehen, welche Eingaben das Tool erwartet, und um Aufrufe zu validieren. Falls weggelassen, lautet der Standardwert für das Schema `{ type: 'object', properties: {} }` (keine Parameter).
|
||
* Funktionen mit `isTool: false` (oder nicht gesetzt) werden **nicht** als Tools bereitgestellt. Sie können weiterhin direkt ausgeführt oder von anderen Funktionen aufgerufen werden, erscheinen jedoch nicht in der Tool-Erkennung.
|
||
* **Tool-Benennung**: Wenn als Tool bereitgestellt, wird der Funktionsname automatisch zu `logic_function_<name>` normalisiert (in Kleinbuchstaben umgewandelt, nicht alphanumerische Zeichen durch Unterstriche ersetzt). Beispielsweise wird `enrich-company` zu `logic_function_enrich_company`.
|
||
* Sie können `isTool` mit Triggern kombinieren — eine Funktion kann gleichzeitig sowohl ein Tool (von KI-Agenten aufrufbar) als auch durch Ereignisse (Cron, Datenbankereignisse, Routen) ausgelöst werden.
|
||
|
||
<Note>
|
||
**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.
|
||
</Note>
|
||
|
||
### Frontend-Komponenten
|
||
|
||
Frontend-Komponenten ermöglichen es Ihnen, benutzerdefinierte React-Komponenten zu erstellen, die innerhalb der Twenty-UI gerendert werden. Verwenden Sie `defineFrontComponent()`, um Komponenten mit eingebauter Validierung zu definieren:
|
||
|
||
```typescript
|
||
// src/front-components/my-widget.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk';
|
||
|
||
const MyWidget = () => {
|
||
return (
|
||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||
<h1>My Custom Widget</h1>
|
||
<p>This is a custom front component for Twenty.</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'my-widget',
|
||
description: 'A custom widget component',
|
||
component: MyWidget,
|
||
});
|
||
```
|
||
|
||
Hauptpunkte:
|
||
|
||
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
|
||
* Das Feld `component` verweist auf Ihre React-Komponente.
|
||
* Komponenten werden während `yarn twenty app:dev` automatisch gebaut und synchronisiert.
|
||
|
||
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
|
||
|
||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
|
||
* **Manuell**: Erstellen Sie eine neue `.tsx`-Datei und verwenden Sie `defineFrontComponent()` nach demselben Muster.
|
||
|
||
#### 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.
|
||
|
||
#### 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** — 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.
|
||
|
||
```typescript
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'my-action',
|
||
description: 'Runs an action without opening the side panel',
|
||
component: MyAction,
|
||
isHeadless: true,
|
||
command: {
|
||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
|
||
label: 'Run my action',
|
||
},
|
||
});
|
||
```
|
||
|
||
#### Befehlsmenü-Einträge hinzufügen
|
||
|
||
Damit eine Front-Komponente als Eintrag im Befehlsmenü von Twenty erscheint, fügen Sie die Eigenschaft `command` zu `defineFrontComponent()` hinzu. Wenn Benutzer das Befehlsmenü öffnen (Cmd+K / Ctrl+K), erscheint der Eintrag und löst beim Klicken die Front-Komponente aus.
|
||
|
||
Das Objekt `command` akzeptiert die folgenden Felder:
|
||
|
||
| Feld | Typ | Beschreibung |
|
||
| --------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||
| `universalIdentifier` | `string` (erforderlich) | Eindeutige ID für den Befehlsmenü-Eintrag |
|
||
| `beschriftung` | `string` (erforderlich) | Angezeigtes Label im Befehlsmenü |
|
||
| `symbol` | `string` (optional) | Iconname (z. B. 'IconSparkles') |
|
||
| `isPinned` | `boolean` (optional) | Ob der Befehl oben im Menü angeheftet ist |
|
||
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` zeigt den Befehl überall an; `RECORD_SELECTION` zeigt ihn nur in Datensatzkontexten an |
|
||
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Beschränkt den Befehl auf einen bestimmten Objekttyp (z. B. Person) |
|
||
|
||
Hier ist ein Beispiel aus der Call-Recording-App, das einen auf Person-Datensätze beschränkten Befehl hinzufügt:
|
||
|
||
```typescript
|
||
import { defineFrontComponent } from 'twenty-sdk';
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
|
||
name: 'Summarize Person Call Recordings',
|
||
description: 'Generates a summary of call recordings for a person',
|
||
component: SummarizePersonRecordings,
|
||
command: {
|
||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
|
||
label: 'Summarize call recordings',
|
||
icon: 'IconSparkles',
|
||
isPinned: false,
|
||
availabilityType: 'RECORD_SELECTION',
|
||
availabilityObjectUniversalIdentifier:
|
||
'20202020-e674-48e5-a542-72570eee7213',
|
||
},
|
||
});
|
||
```
|
||
|
||
Wenn der Befehl synchronisiert wird, erscheint er im Befehlsmenü. Wenn die Front-Komponente Nicht-Headless ist, öffnet sich das Seitenpanel und die Komponente wird darin gerendert. Wenn sie Headless ist, wird die Komponente im Hintergrund gemountet und führt ihre Logik aus.
|
||
|
||
#### 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:
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
// 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',
|
||
},
|
||
});
|
||
```
|
||
|
||
#### Ausführungskontext
|
||
|
||
Jede Front-Komponente erhält einen Ausführungskontext, der Informationen darüber liefert, wo und wie sie ausgeführt wird. Greifen Sie mit Hooks aus `twenty-sdk` auf Kontextwerte zu:
|
||
|
||
| Hook | Rückgabetyp | Beschreibung |
|
||
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `useFrontComponentId()` | `string` | Die eindeutige ID der aktuellen Front-Komponenteninstanz |
|
||
| `useRecordId()` | `string \| null` | Die ID des aktuellen Datensatzes, wenn die Komponente in einem Datensatzkontext ausgeführt wird (z. B. ein Widget auf einer Datensatzseite oder ein auf einen Datensatz beschränkter Befehl). Andernfalls wird `null` zurückgegeben. |
|
||
| `useUserId()` | `string \| null` | Die ID des aktuellen Benutzers |
|
||
|
||
```typescript
|
||
import { useRecordId, useUserId } from 'twenty-sdk';
|
||
|
||
const MyWidget = () => {
|
||
const recordId = useRecordId();
|
||
const userId = useUserId();
|
||
|
||
return (
|
||
<div>
|
||
<p>Record: {recordId ?? 'none'}</p>
|
||
<p>User: {userId ?? 'anonymous'}</p>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
Der Kontext ist reaktiv — ändert sich der umgebende Datensatz, liefern die Hooks automatisch die aktualisierten Werte.
|
||
|
||
#### Host-API-Funktionen
|
||
|
||
Front-Komponenten laufen in einer isolierten Sandbox, können jedoch über eine Reihe vom Host bereitgestellter Funktionen mit der UI von Twenty interagieren. Importieren Sie sie direkt aus `twenty-sdk`:
|
||
|
||
```typescript
|
||
import {
|
||
navigate,
|
||
closeSidePanel,
|
||
enqueueSnackbar,
|
||
unmountFrontComponent,
|
||
openSidePanelPage,
|
||
openCommandConfirmationModal,
|
||
} from 'twenty-sdk';
|
||
```
|
||
|
||
| Funktion | Signatur | Beschreibung |
|
||
| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `navigieren` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigieren Sie zu einem typisierten App-Pfad innerhalb von Twenty |
|
||
| `closeSidePanel` | `() => Promise<void>` | Seitenpanel schließen |
|
||
| `enqueueSnackbar` | `(params) => Promise<void>` | Eine Snackbar-Benachrichtigung anzeigen. Parameter: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
|
||
| `unmountFrontComponent` | `() => Promise<void>` | Die aktuelle Front-Komponente unmounten (wird von Headless-Komponenten verwendet, um nach der Ausführung aufzuräumen) |
|
||
| `openSidePanelPage` | `(params) => Promise<void>` | Eine Seite im Seitenpanel öffnen. Parameter: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
|
||
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Einen Bestätigungsdialog anzeigen und auf die Antwort des Benutzers warten. Parameter: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
|
||
|
||
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
|
||
|
||
```typescript
|
||
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,
|
||
});
|
||
```
|
||
|
||
### Fähigkeiten
|
||
|
||
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:
|
||
|
||
```typescript
|
||
// 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.
|
||
|
||
Sie können neue Skills auf zwei Arten erstellen:
|
||
|
||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen eines neuen Skills.
|
||
* **Manuell**: Erstellen Sie eine neue Datei und verwenden Sie `defineSkill()` nach demselben Muster.
|
||
|
||
### Agenten
|
||
|
||
Mit Agents definieren Sie KI-Agenten mit System-Prompts, die in Ihrem Arbeitsbereich arbeiten können. Verwenden Sie `defineAgent()`, um Agenten mit eingebauter Validierung zu definieren:
|
||
|
||
```typescript
|
||
// src/agents/example-agent.ts
|
||
import { defineAgent } from 'twenty-sdk';
|
||
|
||
export default defineAgent({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'sales-assistant',
|
||
label: 'Sales Assistant',
|
||
description: 'An AI agent that helps with sales tasks',
|
||
icon: 'IconRobot',
|
||
prompt: `You are a sales assistant. Help users with:
|
||
1. Researching prospects and companies
|
||
2. Drafting personalized outreach messages
|
||
3. Tracking follow-ups and next steps
|
||
4. Analyzing deal pipeline and suggesting actions`,
|
||
});
|
||
```
|
||
|
||
Hauptpunkte:
|
||
|
||
* `name` ist eine eindeutige Kennung (als Zeichenfolge) für den Agenten (kebab-case empfohlen).
|
||
* `label` ist der menschenlesbare Anzeigename, der in der UI angezeigt wird.
|
||
* `prompt` enthält den System-Prompt — dies ist der Anweisungstext, der das Verhalten des Agenten definiert.
|
||
* `icon` (optional) legt das in der UI angezeigte Symbol fest.
|
||
* `description` (optional) liefert zusätzlichen Kontext zum Zweck des Agenten.
|
||
|
||
Sie können neue Agenten auf zwei Arten erstellen:
|
||
|
||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen eines neuen Agenten.
|
||
* **Manuell**: Erstellen Sie eine neue Datei und verwenden Sie `defineAgent()` nach demselben Muster.
|
||
|
||
### Generierte typisierte Clients
|
||
|
||
Zwei typisierte Clients werden von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/clients` gespeichert:
|
||
|
||
* **`CoreApiClient`** — fragt den `/graphql`-Endpunkt nach Arbeitsbereichsdaten ab
|
||
* **`MetadataApiClient`** — ruft über den Endpunkt `/metadata` die Arbeitsbereichskonfiguration und Datei-Uploads ab.
|
||
|
||
```typescript
|
||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||
|
||
const client = new CoreApiClient();
|
||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||
|
||
const metadataClient = new MetadataApiClient();
|
||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
|
||
```
|
||
|
||
`CoreApiClient` wird von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern. `MetadataApiClient` ist im SDK bereits enthalten.
|
||
|
||
#### Laufzeit-Anmeldedaten in Logikfunktionen
|
||
|
||
Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführung Ihres Codes Anmeldedaten als Umgebungsvariablen:
|
||
|
||
* `TWENTY_API_URL`: Basis-URL der Twenty-API, auf die Ihre App abzielt.
|
||
* `TWENTY_API_KEY`: Kurzlebiger Schlüssel, der auf die Standard-Funktionsrolle Ihrer Anwendung begrenzt ist.
|
||
|
||
Notizen:
|
||
|
||
* Sie müssen dem generierten Client weder URL noch API-Schlüssel übergeben. Er liest `TWENTY_API_URL` und `TWENTY_API_KEY` zur Laufzeit aus process.env.
|
||
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application-config.ts` über `defaultRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
|
||
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `defaultRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
|
||
|
||
#### Dateien hochladen
|
||
|
||
Der `MetadataApiClient` enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei in Ihren Arbeitsbereichsobjekten anzuhängen. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
|
||
|
||
```typescript
|
||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||
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 (defaults to 'application/octet-stream')
|
||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||
);
|
||
|
||
console.log(uploadedFile);
|
||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||
```
|
||
|
||
Die Methodensignatur:
|
||
|
||
```typescript
|
||
uploadFile(
|
||
fileBuffer: Buffer,
|
||
filename: string,
|
||
contentType: string,
|
||
fieldMetadataUniversalIdentifier: string,
|
||
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
|
||
```
|
||
|
||
| Parameter | Typ | Beschreibung |
|
||
| ---------------------------------- | -------------- | ------------------------------------------------------------------------------- |
|
||
| `fileBuffer` | `Buffer` | Der Rohinhalt der Datei |
|
||
| `filename` | `string` | Der Name der Datei (wird für Speicherung und Anzeige verwendet) |
|
||
| `contentType` | `string` | MIME-Typ der Datei (standardmäßig `application/octet-stream`, wenn weggelassen) |
|
||
| `fieldMetadataUniversalIdentifier` | `Zeichenkette` | Der `universalIdentifier` des Dateityp-Felds in Ihrem Objekt |
|
||
|
||
Hauptpunkte:
|
||
|
||
* Die Methode `uploadFile` ist auf dem `MetadataApiClient` verfügbar, weil die Upload-Mutation vom Endpunkt `/metadata` aufgelöst wird.
|
||
* Sie verwendet den `universalIdentifier` des Feldes (nicht dessen arbeitsbereichsspezifische ID), sodass Ihr Upload-Code in jedem Arbeitsbereich funktioniert, in dem Ihre App installiert ist — im Einklang damit, wie Apps Felder überall sonst referenzieren.
|
||
* Die zurückgegebene `url` ist eine signierte URL, mit der Sie auf die hochgeladene Datei zugreifen können.
|
||
|
||
### Hello-World-Beispiel
|
||
|
||
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||
|
||
## Erstellen Ihrer App
|
||
|
||
Sobald Sie Ihre App mit `app:dev` entwickelt haben, verwenden Sie `app:build`, um sie in ein verteilbares Paket zu kompilieren.
|
||
|
||
```bash filename="Terminal"
|
||
# Die App erstellen (Ausgabe nach .twenty/output/)
|
||
yarn twenty app:build
|
||
|
||
# Build ausführen und ein Tarball (.tgz) für die Verteilung erstellen
|
||
yarn twenty app:build --tarball
|
||
```
|
||
|
||
Der Build-Prozess:
|
||
|
||
1. **Parst und validiert das Manifest** — liest alle `defineX()`-Entitäten aus Ihren Quelldateien und validiert die Manifeststruktur.
|
||
2. **Kompiliert Logikfunktionen und Front-Komponenten** — bündelt TypeScript-Quellcode in ESM `.mjs`-Dateien mit esbuild.
|
||
3. **Erzeugt Checksummen** — berechnet MD5-Hashes für jede erstellte Datei, die im Manifest als `builtHandlerChecksum` / `builtComponentChecksum` gespeichert werden.
|
||
4. **Generiert den typisierten API-Client** — führt eine Introspektion des GraphQL-Schemas durch und generiert die typisierten Clients `CoreApiClient` und `MetadataApiClient`.
|
||
5. **Führt eine TypeScript-Typprüfung aus** — führt `tsc --noEmit` aus, um Typfehler vor der Veröffentlichung zu erkennen.
|
||
6. **Baut mit dem generierten Client neu** — führt einen zweiten Kompiliervorgang durch, damit die generierten Client-Typen enthalten sind.
|
||
7. **Erstellt optional einen Tarball** — wenn `--tarball` übergeben wird, wird `npm pack` ausgeführt, um eine `.tgz`-Datei zu erstellen, die für die Verteilung bereit ist.
|
||
|
||
Der Build-Output in `.twenty/output/` enthält:
|
||
|
||
```text
|
||
.twenty/output/
|
||
├── manifest.json # Manifest with checksums for all built files
|
||
├── package.json # Copied from app root
|
||
├── yarn.lock # Copied from app root
|
||
├── src/
|
||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||
│ └── front-components/ # Compiled .mjs front component files
|
||
├── public/ # Static assets (if any)
|
||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||
```
|
||
|
||
| Option | Beschreibung |
|
||
| ----------- | -------------------------------------------------------------- |
|
||
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
|
||
| `--tarball` | Den Output zusätzlich in einen `.tgz`-Tarball packen |
|
||
|
||
## Veröffentlichen Ihrer App
|
||
|
||
Verwenden Sie `app:publish`, um Ihre App zu verteilen — entweder zur npm-Registry oder direkt zu einem Twenty-Server.
|
||
|
||
### Bei npm veröffentlichen (Standard)
|
||
|
||
```bash filename="Terminal"
|
||
# Publish to npm (requires npm login)
|
||
yarn twenty app:publish
|
||
|
||
# Publish with a dist-tag (e.g. beta, next)
|
||
yarn twenty app:publish --tag beta
|
||
```
|
||
|
||
Dies baut die App und führt `npm publish` aus dem Verzeichnis `.twenty/output/` aus. Das veröffentlichte Paket kann dann von jedem Arbeitsbereich über den Twenty-Marktplatz installiert werden.
|
||
|
||
### Auf einem Twenty-Server veröffentlichen
|
||
|
||
```bash filename="Terminal"
|
||
# Publish directly to a Twenty server
|
||
yarn twenty app:publish --server https://app.twenty.com
|
||
```
|
||
|
||
Dies erstellt beim Build einen Tarball, lädt ihn über die GraphQL-Mutation `uploadAppTarball` auf den Server hoch und stößt die Installation in einem Schritt an. Dies ist nützlich für private Bereitstellungen oder Tests gegen einen bestimmten Server.
|
||
|
||
| Option | Beschreibung |
|
||
| ----------------- | ------------------------------------------------------------------ |
|
||
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
|
||
| `--server <url>` | Auf einen Twenty-Server anstelle von npm veröffentlichen |
|
||
| `--token <token>` | Authentifizierungstoken für den Zielserver |
|
||
| `--tag <tag>` | npm dist-tag (z. B. `beta`, `next`) — nur für npm-Veröffentlichung |
|
||
|
||
## Anwendungsregistrierung
|
||
|
||
Bevor eine App in einem Arbeitsbereich installiert werden kann, muss sie **registriert** werden. Eine Registrierung ist ein Metadatensatz, der beschreibt, woher die App stammt und wie sie authentifiziert wird. Dies wird in den meisten Fällen automatisch durch die CLI erledigt.
|
||
|
||
### Quelltypen
|
||
|
||
Jede Registrierung hat einen **Quelltyp**, der bestimmt, wie die Dateien der App während der Installation aufgelöst werden:
|
||
|
||
| Quelltyp | Wie Dateien aufgelöst werden | Typischer Anwendungsfall |
|
||
| --------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||
| `LOCAL` | Dateien werden in Echtzeit vom CLI-Watcher synchronisiert — die Installation wird übersprungen | Entwicklung mit `app:dev` |
|
||
| `NPM` | Über das Feld `sourcePackage` aus der npm-Registry abgerufen | Veröffentlichte Apps auf npm |
|
||
| `TARBALL` | Aus einer hochgeladenen, auf dem Server gespeicherten `.tgz`-Datei extrahiert | Private Apps, die mit `--server` veröffentlicht wurden |
|
||
|
||
### Wie die Registrierung erfolgt
|
||
|
||
* **`app:dev`** — erstellt beim ersten Ausführen des Dev-Modus für einen Arbeitsbereich automatisch eine `LOCAL`-Registrierung.
|
||
* **`app:publish --server`** — lädt einen Tarball hoch und erstellt (oder aktualisiert) eine `TARBALL`-Registrierung und installiert anschließend die App.
|
||
* **npm-Marktplatz** — `NPM`-Registrierungen werden erstellt, wenn Apps aus der npm-Registry in den Twenty-Marktplatzkatalog synchronisiert werden.
|
||
* **GraphQL-API** — Sie können Registrierungen auch programmgesteuert über die Mutation `createApplicationRegistration` erstellen.
|
||
|
||
### Registrierung vs. Installation
|
||
|
||
**Registrierung** und **Installation** sind unterschiedliche Konzepte:
|
||
|
||
* Eine **Registrierung** (`ApplicationRegistration`) ist ein globaler Metadatensatz, der die App beschreibt: ihren Namen, den Quelltyp, die OAuth-Anmeldedaten und den Status der Marktplatzlistung. Sie existiert unabhängig von jedem Arbeitsbereich.
|
||
* Eine **Installation** (`Application`) ist eine Instanz pro Arbeitsbereich. Wenn ein Benutzer eine App installiert, ermittelt Twenty das Paket aus der Quelle der Registrierung, schreibt die erstellten Dateien in den Speicher und synchronisiert das Manifest (wobei Objekte, Felder, Logikfunktionen usw. erstellt werden) in diesem Arbeitsbereich.
|
||
|
||
Eine Registrierung kann in vielen Arbeitsbereichen installiert werden. Jeder Arbeitsbereich erhält seine eigene Kopie der Dateien und des Datenmodells der App.
|
||
|
||
### OAuth-Anmeldedaten
|
||
|
||
Jede Registrierung enthält OAuth-Anmeldedaten (`oAuthClientId` und `oAuthClientSecret`), die bei der Erstellung generiert werden. Diese werden von der App verwendet, um API-Anfragen im Namen der Benutzer zu authentifizieren. Das Client-Secret wird bei der Erstellung **einmalig** zurückgegeben — bewahren Sie es sicher auf. Sie können es später über die Mutation `rotateApplicationRegistrationClientSecret` rotieren.
|
||
|
||
## Manuelle Einrichtung (ohne Scaffolder)
|
||
|
||
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie ein einzelnes Skript in Ihrer package.json ein:
|
||
|
||
```bash filename="Terminal"
|
||
yarn add -D twenty-sdk
|
||
```
|
||
|
||
Fügen Sie dann ein `twenty`-Skript hinzu:
|
||
|
||
```json filename="package.json"
|
||
{
|
||
"scripts": {
|
||
"twenty": "twenty"
|
||
}
|
||
}
|
||
```
|
||
|
||
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw.
|
||
|
||
## Fehlerbehebung
|
||
|
||
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
|
||
* Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist.
|
||
* Typen oder Client fehlen/veraltet: Starten Sie `yarn twenty app:dev` neu — der typisierte Client wird automatisch generiert.
|
||
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
|
||
|
||
Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322
|