snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
---
title: Beste Praktiken
---
Dieses Dokument beschreibt die Best Practices, die Sie bei der Arbeit am Backend befolgen sollten.
## Verfolgen Sie einen modularen Ansatz
Das Backend folgt einem modularen Ansatz, was ein grundlegendes Prinzip bei der Arbeit mit NestJS ist. Stellen Sie sicher, dass Sie Ihren Code in wiederverwendbare Module aufteilen, um eine saubere und organisierte Codebasis aufrechtzuerhalten.
Jedes Modul sollte ein bestimmtes Feature oder eine Funktionalität kapseln und einen klar definierten Umfang haben. Dieser modulare Ansatz ermöglicht eine klare Trennung der Verantwortlichkeiten und beseitigt unnötige Komplexitäten.
## Dienste zur Verwendung in Modulen bereitstellen
Erstellen Sie immer Dienste mit einer klaren Einzelverantwortung, was die Lesbarkeit und Wartbarkeit des Codes verbessert. Benennen Sie die Dienste beschreibend und konsistent.
Sie sollten auch Dienste bereitstellen, die Sie in anderen Modulen verwenden möchten. Das Bereitstellen von Diensten für andere Module ist durch das leistungsstarke Abhängigkeitsinjektionssystem von NestJS möglich und fördert die lose Kopplung zwischen den Komponenten.
## Vermeiden Sie die Verwendung des Typs `any`
Wenn Sie eine Variable als `any` deklarieren, führt der TypeScript-Typenprüfer keine Typprüfung durch, was es möglich macht, der Variablen Werte beliebigen Typs zuzuweisen. TypeScript verwendet Typinferenz, um den Typ einer Variable basierend auf dem Wert zu bestimmen. Durch die Deklaration als `any` kann TypeScript den Typ nicht mehr ableiten. Dies erschwert es, typbezogene Fehler während der Entwicklung zu erkennen, was zu Laufzeitfehlern führt und den Code weniger wartbar, weniger zuverlässig und schwerer verständlich für andere macht.
Deshalb sollte alles einen Typ haben. Wenn Sie also ein neues Objekt mit Vor- und Nachnamen erstellen, sollten Sie eine Schnittstelle oder einen Typ definieren, der Vor- und Nachnamen enthält und die Struktur des Objekts definiert, mit dem Sie arbeiten.

View File

@@ -0,0 +1,39 @@
---
title: Benutzerdefinierte Objekte
---
Objekte sind Strukturen, die es Ihnen ermöglichen, Daten (Aufzeichnungen, Attribute und Werte) zu speichern, die spezifisch für eine Organisation sind. Twenty bietet sowohl Standard- als auch benutzerdefinierte Objekte.
Standardobjekte sind systemeigene Objekte mit einer Reihe von Attributen, die allen Benutzern zur Verfügung stehen. Beispiele für Standardobjekte in Twenty sind Unternehmen und Person. Standardobjekte verfügen über Standardfelder, die ebenfalls allen Twenty-Nutzern zur Verfügung stehen, wie z.B. Unternehmen.displayName.
Benutzerdefinierte Objekte sind Objekte, die Sie erstellen können, um Informationen zu speichern, die einzigartig für Ihre Organisation sind. Sie sind nicht eingebaut; Mitglieder Ihres Arbeitsbereichs können benutzerdefinierte Objekte erstellen und anpassen, um Informationen zu speichern, für die Standardobjekte nicht geeignet sind.
## Schema auf hoher Ebene
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Schema auf hoher Ebene" />
</div>
<br />
## Wie es funktioniert
Benutzerdefinierte Objekte stammen aus Metadatentabellen, die Form, Namen und Typ der Objekte bestimmen. Alle diese Informationen sind in der Metadaten-Schema-Datenbank vorhanden, die aus Tabellen besteht:
* **Datenquelle**: Gibt an, wo die Daten vorhanden sind.
* **Objekt**: Beschreibt das Objekt und verlinkt zu einer Datenquelle.
* **Feld**: Umreißt die Felder eines Objekts und verbindet es mit dem Objekt.
Um ein benutzerdefiniertes Objekt hinzuzufügen, wird das Arbeitsbereichsmitglied die /metadata API abfragen. Dies aktualisiert die Metadaten entsprechend und berechnet ein GraphQL-Schema basierend auf den Metadaten, das in einem GQL-Cache für die spätere Verwendung gespeichert wird.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Fragen Sie die /metadata API ab, um benutzerdefinierte Objekte hinzuzufügen" />
</div>
<br />
Um Daten abzurufen, wird der Prozess durchgeführt, indem Abfragen über den /graphql-Endpunkt erstellt und über den Abfrage-Resolver weitergeleitet werden.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Fragen Sie den /graphql-Endpunkt ab, um Daten abzurufen" />
</div>

View File

@@ -0,0 +1,46 @@
---
title: Feature-Flags
---
Feature-Flags werden verwendet, um experimentelle Funktionen auszublenden. Für Twenty werden sie auf der Arbeitsbereichsebene und nicht auf Benutzerebene festgelegt.
## Hinzufügen eines neuen Feature-Flags
Fügen Sie in `FeatureFlagKey.ts` das Feature-Flag hinzu:
```ts
type FeatureFlagKey =
| 'IS_FEATURENAME_ENABLED'
| ...;
```
Fügen Sie es auch dem Enum in `feature-flag.entity.ts` hinzu:
```ts
enum FeatureFlagKeys {
IsFeatureNameEnabled = 'IS_FEATURENAME_ENABLED',
...
}
```
Um ein Funktions-Flag auf einem **Backend**-Feature anzuwenden, verwenden Sie:
```ts
@Gate({
featureFlag: 'IS_FEATURENAME_ENABLED',
})
```
Um ein Funktions-Flag auf einem **Frontend**-Feature anzuwenden, verwenden Sie:
```ts
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
```
## Konfigurieren Sie Funktions-Flags für die Bereitstellung
Ändern Sie den entsprechenden Eintrag in der Tabelle `core.featureFlag`:
| iD | schlüssel | workspaceId | wert |
| -------- | ------------------------ | ------------------ | ------ |
| Zufällig | `IS_FEATURENAME_ENABLED` | Arbeitsbereichs-ID | `wahr` |

View File

@@ -0,0 +1,125 @@
---
title: Ordnerarchitektur
info: Ein detaillierter Einblick in unsere Server-Ordnerarchitektur
---
Die Server-Verzeichnisstruktur ist wie folgt:
```
server
└───ability
└───constants
└───core
└───database
└───decorators
└───filters
└───guards
└───health
└───integrations
└───metadata
└───workspace
└───utils
```
## Fähigkeit
Definiert Berechtigungen und enthält Handler für jedes Element.
## Dekoratoren
Definiert benutzerdefinierte Dekoratoren in NestJS für zusätzliche Funktionen.
Weitere Details finden Sie unter [benutzerdefinierte Dekoratoren](https://docs.nestjs.com/custom-decorators).
## Filter
Enthält Ausnahmefilter, um mögliche Ausnahmen bei GraphQL-Endpunkten zu behandeln.
## Guards
Weitere Details finden Sie unter [Guards](https://docs.nestjs.com/guards).
## Health
Enthält eine öffentlich zugängliche REST-API (healthz), die ein JSON zurückgibt, um zu bestätigen, dass die Datenbank wie erwartet funktioniert.
## Metadaten
Definiert benutzerdefinierte Objekte und stellt eine GraphQL-API (graphql/metadaten) bereit.
## Arbeitsbereich
Erzeugt und stellt ein benutzerdefiniertes GraphQL-Schema basierend auf den Metadaten bereit.
### Verzeichnisstruktur des Workspaces
```
workspace
└───workspace-schema-builder
└───factories
└───graphql-types
└───database
└───interfaces
└───object-definitions
└───services
└───storage
└───utils
└───workspace-resolver-builder
└───factories
└───interfaces
└───workspace-query-builder
└───factories
└───interfaces
└───workspace-query-runner
└───interfaces
└───utils
└───workspace-datasource
└───workspace-manager
└───workspace-migration-runner
└───utils
└───workspace.module.ts
└───workspace.factory.spec.ts
└───workspace.factory.ts
```
Der Stamm des Arbeitsbereichsverzeichnisses umfasst die Datei `arbeitsbereich.fabrik.ts`, die die Funktion `createGraphQLSchema` enthält. Diese Funktion generiert arbeitsbereichsspezifische Schemata, indem sie die Metadaten nutzt, um ein Schema für einzelne Arbeitsbereiche zu gestalten. Durch die Trennung von Schema- und Resolver-Erstellung verwenden wir die Funktion `makeExecutableSchema`, die diese diskreten Elemente kombiniert.
Diese Strategie dient nicht nur der Organisation, sondern hilft auch bei der Optimierung, wie z.B. dem Caching generierter Typdefinitionen zur Leistungs- und Skalierbarkeitssteigerung.
### Workspace-Schema-Builder
Generiert das GraphQL-Schema und umfasst:
#### Fabriken:
Spezialisierte Konstruktoren, um GraphQL-bezogene Konstrukte zu generieren.
* Die type.factory übersetzt Feldmetadaten in GraphQL-Typen unter Verwendung des `TypeMapperService`.
* Die type-definition.factory erstellt GraphQL-Eingabe- oder Ausgabeobjekte, die aus `objektMetadaten` abgeleitet sind.
#### GraphQL-Typen
Umfasst Aufzählungen, Eingaben, Objekte und Skalare und dient als Bausteine für die Schemakonstruktion.
#### Schnittstellen und Objektdefinitionen
Enthält die Blaupausen für GraphQL-Entitäten und umfasst sowohl vordefinierte als auch benutzerdefinierte Typen wie `MONEY` oder `URL`.
#### Dienste
Enthält den Service, der dafür verantwortlich ist, FieldMetadataType dem passenden GraphQL-Scalar oder den entsprechenden Abfrage-Modifikatoren zuzuordnen.
#### Speicher
Enthält die Klasse `TypeDefinitionsStorage`, die wiederverwendbare Typdefinitionen enthält und die Duplizierung von GraphQL-Typen verhindert.
### Workspace-Resolver-Builder
Erstellt Resolverfunktionen für die Abfrage und Änderung des GraphQL-Schemas.
Jede Fabrik in diesem Verzeichnis ist für die Erstellung eines bestimmten Resolvertyps verantwortlich, wie die `FindManyResolverFactory`, die für eine vielseitige Anwendung auf verschiedenen Tabellen entwickelt wurde.
### Arbeitsbereich Anfrage Ausführer
Führt die generierten Anfragen auf der Datenbank aus und analysiert das Ergebnis.

View File

@@ -0,0 +1,41 @@
---
title: Nachrichtenwarteschlange
---
Warteschlangen erleichtern die Durchführung asynchroner Vorgänge. Sie können für die Ausführung von Hintergrundaufgaben verwendet werden, wie z.B. das Versenden einer Willkommens-E-Mail bei der Registrierung.
Jeder Anwendungsfall hat seine eigene Warteschlangenklasse, die von `MessageQueueServiceBase` erweitert wird.
Derzeit unterstützen wir nur `bull-mq`[bull-mq](https://bullmq.io/) als Warteschlangentreiber.
## Schritte zur Erstellung und Nutzung einer neuen Warteschlange
1. Fügen Sie einen Warteschlangennamen für Ihre neue Warteschlange unter dem Enum `MESSAGE_QUEUES` hinzu.
2. Stellen Sie die Factory-Implementierung der Warteschlange mit dem Warteschlangennamen als Abhängigkeits-Token bereit.
3. Injizieren Sie die von Ihnen erstellte Warteschlange im erforderlichen Modul/Service mit dem Warteschlangennamen als Abhängigkeits-Token.
4. Fügen Sie eine Worker-Klasse mit tokenbasierter Injektion hinzu, genau wie der Producer.
### Beispielverwendung
```ts
class Resolver {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
async onSomeAction() {
//business logic
await this.queue.add(someData);
}
}
//async worker
class CustomWorker {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
this.initWorker();
}
async initWorker() {
await this.queue.work(async ({ id, data }) => {
//worker logic
});
}
}
```

View File

@@ -0,0 +1,87 @@
---
title: Backend Befehle
icon: terminal
---
## Nützliche Befehle
Diese Befehle sollten aus dem Verzeichnis packages/twenty-server ausgeführt werden.
Von jedem anderen Verzeichnis aus können Sie `npx nx {command} twenty-server` (oder `npx nx run twenty-server:{command}`) verwenden.
### Erstmalige Einrichtung
```
npx nx database:reset twenty-server # setup the database with dev seeds
```
### Server starten
```
npx nx run twenty-server:start
```
### Lint
```
npx nx run twenty-server:lint # pass --fix to fix lint errors
```
### Test
```
npx nx run twenty-server:test:unit # run unit tests
npx nx run twenty-server:test:integration # run integration tests
```
Hinweis: Sie können `npx nx run twenty-server:test:integration:with-db-reset` ausführen, falls Sie die Datenbank zurücksetzen müssen, bevor Sie die Integrationstests durchführen.
### Datenbank zurücksetzen
Wenn Sie die Datenbank zurücksetzen und seeden möchten, können Sie den folgenden Befehl ausführen:
```bash
npx nx run twenty-server:database:reset
```
### Migrationen
#### Für Objekte in Core/Metadata-Schemas (TypeORM)
```bash
npx nx run twenty-server:database:migrate:generate
```
## Technologie-Stack
Twenty verwendet in erster Linie NestJS für das Backend.
Prisma war das erste ORM, das wir verwendeten. Um Benutzern die Möglichkeit zu geben, benutzerdefinierte Felder und Objekte zu erstellen, war eine niedrigere Ebene sinnvoller, da wir eine feinkörnige Kontrolle benötigen. Das Projekt verwendet jetzt TypeORM.
So sieht der Tech-Stack jetzt aus.
**Kern**
* [NestJS](https://nestjs.com/)
* [TypeORM](https://typeorm.io/)
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
**Datenbank**
* [Postgres](https://www.postgresql.org/)
**Externe Integrationen**
* [Sentry](https://sentry.io/welcome/) zum Verfolgen von Fehlern
**Tests**
* [Jest](https://jestjs.io/)
**Tools**
* [Yarn](https://yarnpkg.com/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
**Entwicklung**
* [AWS EKS](https://aws.amazon.com/eks/)

View File

@@ -0,0 +1,85 @@
---
title: Zapier App
---
Synchronisieren Sie mühelos Twenty mit über 3000 Apps mit [Zapier](https://zapier.com/). Automatisieren Sie Aufgaben, steigern Sie die Produktivität und stärken Sie Ihre Kundenbeziehungen!
## Über Zapier
Zapier ist ein Tool, das es Ihnen ermöglicht, Workflows zu automatisieren, indem Sie die Apps verbinden, die Ihr Team täglich nutzt. Das grundlegende Konzept von Zapier sind automatische Workflows, genannt Zaps, die Trigger und Aktionen umfassen.
Erfahren Sie mehr darüber, wie Zapier funktioniert [hier](https://zapier.com/how-it-works).
## Einrichtung
### Schritt 1: Installieren Sie die Zapier-Pakete
```bash
cd packages/twenty-zapier
yarn
```
### Schritt 2: Anmelden mit dem CLI
Verwenden Sie Ihre Zapier-Anmeldedaten, um sich über das CLI anzumelden:
```bash
zapier login
```
### Schritt 3: Einrichten von Umgebungsvariablen
Führen Sie im Ordner `packages/twenty-zapier` aus:
```bash
cp .env.example .env
```
Führen Sie die Anwendung lokal aus, gehen Sie zu [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks), und generieren Sie einen API-Schlüssel.
Ersetzen Sie den Wert **YOUR_API_KEY** in der Datei `.env` durch den gerade generierten API-Schlüssel.
## Entwicklung
<Warning>
Stellen Sie sicher, dass Sie `yarn build` vor jedem `zapier`-Befehl ausführen.
</Warning>
### Test
```bash
yarn test
```
### Lint
```bash
yarn format
```
### Beobachten und kompilieren Sie, während Sie den Code bearbeiten
```bash
yarn watch
```
### Validieren Sie Ihre Zapier-App
```bash
yarn validate
```
### Stellen Sie Ihre Zapier-App bereit
```bash
yarn deploy
```
### Listet alle Zapier CLI-Befehle auf
```bash
zapier
```

View File

@@ -0,0 +1,79 @@
---
title: Fehlermeldungen, Anfragen & Pull Requests
icon: bug
info: Probleme melden, Funktionen vorschlagen und Code beisteuern
---
## Fehler melden
Um einen Fehler zu melden, [erstellen Sie bitte ein Issue auf GitHub](https://github.com/twentyhq/twenty/issues/new).
Sie können auch um Hilfe auf [Discord](https://discord.gg/cx5n4Jzs57) bitten.
## Feature-Anfragen
Wenn Sie sich nicht sicher sind, ob es sich um einen Fehler handelt, und glauben, dass es eher eine Funktionsanforderung ist, sollten Sie wahrscheinlich [stattdessen eine Diskussion eröffnen](https://github.com/twentyhq/twenty/discussions/new).
## Einen Pull Request einreichen
Codebeiträge zu Twenty beginnen mit einem Pull Request (PR).
### Bevor Sie beginnen
1. Prüfen Sie [bestehende Issues](https://github.com/twentyhq/twenty/issues) auf verwandte Arbeiten
2. Für neue Features zunächst ein Issue eröffnen, um dies zu besprechen
3. Lesen Sie unseren [Verhaltenskodex](https://github.com/twentyhq/twenty/blob/main/.github/CODE_OF_CONDUCT.md)
### Forken und klonen
1. Das Repository auf GitHub forken
2. Ihren Fork klonen:
```bash
git clone https://github.com/YOUR_USERNAME/twenty.git
cd twenty
```
3. Upstream-Remote hinzufügen:
```bash
git remote add upstream https://github.com/twentyhq/twenty.git
```
### Einen Branch erstellen
```bash
git checkout -b feature/your-feature-name
```
Verwenden Sie aussagekräftige Branch-Namen:
* `feature/add-export-button`
* `fix/login-redirect-issue`
* `docs/update-api-guide`
### Nehmen Sie Ihre Änderungen vor
1. Schreiben Sie sauberen, gut dokumentierten Code
2. Befolgen Sie den bestehenden Code-Stil
3. Fügen Sie Tests für neue Funktionalität hinzu
4. Aktualisieren Sie bei Bedarf die Dokumentation
### Reichen Sie Ihren PR ein
1. Pushen Sie Ihren Branch:
```bash
git push origin feature/your-feature-name
```
2. Öffnen Sie einen PR auf GitHub
3. Füllen Sie die PR-Vorlage aus
4. Verknüpfen Sie zugehörige Issues
### PR-Checkliste
* [ ] Der Code entspricht den Stilrichtlinien des Projekts
* [ ] Die Tests bestehen lokal
* [ ] Die Dokumentation ist aktualisiert
* [ ] Die PR-Beschreibung erläutert die Änderungen

View File

@@ -0,0 +1,329 @@
---
title: Beste Praktiken
icon: star
---
Dieses Dokument beschreibt die besten Praktiken, die Sie beim Arbeiten am Frontend beachten sollten.
## Zustandsverwaltung
React und Jotai übernehmen die Zustandsverwaltung im Code.
### Verwenden Sie Jotai-Atome, um den Zustand zu speichern
Es ist eine gute Praxis, so viele Atome zu erstellen, wie Sie benötigen, um Ihren Zustand zu speichern.
<Warning>
Es ist besser, zusätzliche Atome zu verwenden, als zu versuchen, mit Prop Drilling allzu knapp zu arbeiten.
</Warning>
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
}
```
### Verwenden Sie `useRef` nicht, um den Zustand zu speichern.
Vermeiden Sie die Verwendung von `useRef`, um den Zustand zu speichern.
Wenn Sie den Zustand speichern möchten, sollten Sie `useState` oder Jotai-Atome mit `useAtomState` verwenden.
Sehen Sie sich [an, wie Re-Renderings verwaltet werden können](#managing-re-renders), falls Sie das Gefühl haben, dass Sie `useRef` benötigen, um einige Re-Renderings zu verhindern.
## Re-Renderings verwalten
Re-Renderings können in React schwer zu verwalten sein.
Hier sind einige Regeln, die befolgt werden sollten, um unnötige Re-Renderings zu vermeiden.
Beachten Sie, dass Sie **immer** Re-Renderings vermeiden können, indem Sie deren Ursache verstehen.
### Arbeiten Sie auf der Root-Ebene
Das Vermeiden von Re-Renderings in neuen Funktionen wird jetzt erleichtert, indem sie auf Root-Ebene eliminiert werden.
Die `PageChangeEffect`-Sidecar-Komponente enthält nur einen `useEffect`-Hook, der die gesamte Logik für den Seitenwechsel enthält.
Auf diese Weise wissen Sie, dass es nur einen Ort gibt, der ein Re-Rendering auslösen kann.
### Denken Sie immer zweimal nach, bevor Sie `useEffect` in Ihre Codebasis aufnehmen.
Re-Renderings werden oft durch unnötige `useEffect`-Verwendungen verursacht.
Sie sollten überlegen, ob Sie `useEffect` benötigen oder ob Sie die Logik in eine Event-Handler-Funktion verschieben können.
Es ist in der Regel einfach, die Logik in eine `handleClick` oder `handleChange`-Funktion zu verschieben.
Sie können sie auch in Bibliotheken wie Apollo finden: `onCompleted`, `onError`, usw.
### Verwenden Sie eine Geschwisterkomponente, um `useEffect`- oder Datenabruf-Logik auszulagern.
Wenn Sie das Gefühl haben, Ihrer Root-Komponente einen `useEffect` hinzufügen zu müssen, sollten Sie erwägen, ihn in eine Sidecar-Komponente auszulagern.
Dasselbe können Sie auch für die Datenabruflogik mit Apollo-Hooks anwenden.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <div>{data}</div>;
};
export const App = () => (
<PageComponent />
);
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <></>;
};
export const App = () => (
<>
<PageData />
<PageComponent />
</>
);
```
### Verwenden Sie Atom-Familienzustände und Selektoren
Atom-Familienzustände und Selektoren sind eine großartige Möglichkeit, Re-Renders zu vermeiden.
Sie sind nützlich, wenn Sie eine Liste von Elementen speichern müssen.
### Sie sollten nicht `React.memo(MyComponent)` verwenden.
Vermeiden Sie die Verwendung von `React.memo()`, da es nicht die Ursache für das Re-Rendering löst, sondern stattdessen die Re-Render-Kette unterbricht, was zu unerwartetem Verhalten führen kann und es sehr schwierig macht, den Code zu refaktorisieren.
### Beschränken Sie die Nutzung von `useCallback` oder `useMemo`.
Oft sind sie nicht erforderlich und machen den Code schwerer zu lesen und zu warten für einen Leistungsgewinn, der kaum wahrnehmbar ist.
## Console.logs
`console.log`-Anweisungen bieten während der Entwicklung wertvolle Echtzeit-Einblicke in Variablenwerte und den Codefluss. Aber wenn sie im Produktivcode belassen werden, kann dies zu mehreren Problemen führen:
1. **Leistung**: Übermäßiges Logging kann die Laufzeitleistung beeinflussen, insbesondere bei clientseitigen Anwendungen.
2. **Sicherheit**: Das Protokollieren sensibler Daten kann kritische Informationen offenlegen für jeden, der die Konsole des Browsers inspiziert.
3. **Sauberkeit**: Das Ausfüllen der Konsole mit Logs kann wichtige Warnungen oder Fehler verdecken, die Entwickler oder Tools sehen müssen.
4. **Professionalität**: Endbenutzer oder Kunden, die die Konsole überprüfen und eine Vielzahl von Protokollanweisungen sehen, könnten die Qualität und den Feinschliff des Codes in Frage stellen.
Stellen Sie sicher, dass Sie alle `console.logs` entfernen, bevor Sie den Code in die Produktion übertragen.
## Namensgebung
### Variablenbenennung
Variablennamen sollten den Zweck oder die Funktion der Variable genau beschreiben.
#### Das Problem mit generischen Namen
Generische Namen in der Programmierung sind nicht ideal, weil ihnen die Spezifität fehlt, was zu Mehrdeutigkeit und verminderter Lesbarkeit des Codes führt. Solche Namen vermitteln nicht den Zweck der Variablen oder Funktion, was es Entwicklern erschwert, die Absicht des Codes ohne tiefere Untersuchung zu verstehen. Dies kann zu erhöhten Debugging-Zeiten, höherer Fehleranfälligkeit und Schwierigkeiten bei der Wartung und Zusammenarbeit führen. In der Zwischenzeit macht eine beschreibende Namensgebung den Code selbsterklärend und einfacher zu navigieren, was die Codequalität und die Produktivität der Entwickler verbessert.
```tsx
// ❌ Bad, uses a generic name that doesn't communicate its
// purpose or content clearly
const [value, setValue] = useState('');
```
```tsx
// ✅ Good, uses a descriptive name
const [email, setEmail] = useState('');
```
#### Einige Wörter, die in Variablennamen zu vermeiden sind
* Dummy
### Ereignis-Handler
Ereignis-Handler-Namen sollten mit `handle` beginnen, während `on` als Präfix dient, um Ereignisse in Komponenten-Props zu benennen.
```tsx
// ❌ Bad
const onEmailChange = (val: string) => {
// ...
};
```
```tsx
// ✅ Good
const handleEmailChange = (val: string) => {
// ...
};
```
## Optionale Props
Vermeiden Sie es, den Standardwert für ein optionales Prop zu übergeben.
**BEISPIEL**
Betrachten Sie die unten definierte `EmailField`-Komponente:
```tsx
type EmailFieldProps = {
value: string;
disabled?: boolean;
};
const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
<TextInput value={value} disabled={disabled} fullWidth />
);
```
**Verwendung**
```tsx
// ❌ Bad, passing in the same value as the default value adds no value
const Form = () => <EmailField value="username@email.com" disabled={false} />;
```
```tsx
// ✅ Good, assumes the default value
const Form = () => <EmailField value="username@email.com" />;
```
## Komponente als Props
Versuchen Sie nach Möglichkeit, nicht instanziierte Komponenten als Props zu übergeben, damit untergeordnete Komponenten selbst entscheiden können, welche Props sie weiterreichen müssen.
Das häufigste Beispiel dafür sind Icon-Komponenten:
```tsx
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
// In MyComponent
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
const theme = useTheme();
return (
<div>
<MyIcon size={theme.icon.size.md}>
</div>
)
};
```
Damit React versteht, dass die Komponente eine Komponente ist, müssen Sie PascalCase verwenden, damit sie später mit `<MyIcon>` instanziiert werden kann.
## Prop Drilling: Beschränken Sie es auf das Nötigste
Prop Drilling im React-Kontext bezieht sich auf die Praxis, Zustandvariablen und deren Setter durch viele Komponentenebenen zu leiten, auch wenn Zwischenkomponenten sie nicht verwenden. Obwohl es manchmal notwendig ist, kann übermäßiges Prop Drilling zu Folgendem führen:
1. **Verminderte Lesbarkeit**: Die Nachverfolgung, woher ein Prop stammt oder wo es verwendet wird, kann in einer tief verschachtelten Komponentenstruktur verworren werden.
2. **Wartungsherausforderungen**: Änderungen in der Prop-Struktur einer Komponente können Anpassungen in mehreren Komponenten erfordern, selbst wenn sie das Prop nicht direkt verwenden.
3. **Verringerte Wiederverwendbarkeit von Komponenten**: Eine Komponente, die viele Props nur zum Weiterreichen erhält, wird weniger universell und schwieriger in unterschiedlichen Kontexten wiederzuverwenden.
Wenn Sie das Gefühl haben, dass Sie übermäßig Prop Drilling einsetzen, sehen Sie sich die [Best Practices für das Zustandsmanagement](#state-management) an.
## Importe
Beim Importieren sollten Sie die vorgesehenen Aliase anstelle der vollständigen oder relativen Pfade verwenden.
**Die Aliase**
```js
{
alias: {
"~": path.resolve(__dirname, "src"),
"@": path.resolve(__dirname, "src/modules"),
"@testing": path.resolve(__dirname, "src/testing"),
},
}
```
**Verwendung**
```tsx
// ❌ Bad, specifies the entire relative path
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
// ✅ Good, utilises the designated aliases
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from 'twenty-ui/testing';
```
## Schemavalidierung
[Zod](https://github.com/colinhacks/zod) ist der Schema-Validator für ungetypte Objekte:
```js
const validationSchema = z
.object({
exist: z.boolean(),
email: z
.string()
.email('Email must be a valid email'),
password: z
.string()
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
})
.required();
type Form = z.infer<typeof validationSchema>;
```
## Breaking Changes
Führen Sie immer gründliche manuelle Tests durch, bevor Sie fortfahren, um sicherzustellen, dass keine Modifikationen anderswo Störungen verursacht haben, da Testen noch nicht umfassend integriert ist.

View File

@@ -0,0 +1,110 @@
---
title: Ordnerarchitektur
icon: folder-tree
info: Ein detaillierter Einblick in unsere Ordnerarchitektur
---
In diesem Leitfaden erkunden Sie die Details der Projektverzeichnisstruktur und wie sie zur Organisation und Wartbarkeit von Twenty beiträgt.
Indem Sie diesem Ordnerarchitekturkonzept folgen, können Sie Dateien, die sich auf spezifische Funktionen beziehen, leichter finden und sicherstellen, dass die Anwendung skalierbar und wartbar ist.
```
front
└───modules
│ └───module1
│ │ └───submodule1
│ └───module2
│ └───ui
│ │ └───display
│ │ └───inputs
│ │ │ └───buttons
│ │ └───...
└───pages
└───...
```
## Seiten
Enthält die Top-Level-Komponenten, die durch die Anwendungsrouten definiert werden. Sie importieren weitere Low-Level-Komponenten aus dem Ordner modules (weitere Details unten).
## Module
Jedes Modul repräsentiert eine Funktion oder eine Gruppe von Funktionen und umfasst deren spezifische Komponenten, Zustände und Betriebslogik.
Sie sollten alle der untenstehenden Struktur folgen. Sie können Module innerhalb von Modulen (als Submodule bezeichnet) verschachteln, und die gleichen Regeln gelten.
```
module1
└───components
│ └───component1
│ └───component2
└───constants
└───contexts
└───graphql
│ └───fragments
│ └───queries
│ └───mutations
└───hooks
│ └───internal
└───states
│ └───selectors
└───types
└───utils
```
### Kontexte
Ein Context ist eine Möglichkeit, Daten durch den Komponentenbaum weiterzugeben, ohne props auf jeder Ebene manuell weitergeben zu müssen.
Weitere Details finden Sie unter [React Context](https://react.dev/reference/react#context-hooks).
### GraphQL
Enthält Fragmente, Abfragen und Mutationen.
Weitere Details finden Sie unter [GraphQL](https://graphql.org/learn/).
* Fragmente
Ein Fragment ist ein wiederverwendbares Stück einer Abfrage, das an verschiedenen Stellen verwendet werden kann. Durch die Verwendung von Fragmenten lässt sich doppelter Code leichter vermeiden.
Weitere Details finden Sie unter [GraphQL Fragmente](https://graphql.org/learn/queries/#fragments).
* Abfragen
Weitere Details finden Sie unter [GraphQL Abfragen](https://graphql.org/learn/queries/).
* Mutationen
Weitere Details finden Sie unter [GraphQL Mutationen](https://graphql.org/learn/queries/#mutations).
### Hooks
Weitere Details finden Sie unter [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks).
### Zustände
Enthält die State-Management-Logik. [Jotai](https://jotai.org) übernimmt dies.
* Selektoren: Abgeleitete Atome (mit `createAtomSelector`) berechnen Werte aus anderen Atomen und werden automatisch memoisiert.
Die integrierte Zustandsverwaltung von React verwaltet weiterhin den Zustand innerhalb einer Komponente.
### Utils
Sollte nur wiederverwendbare reine Funktionen enthalten. Andernfalls erstellen Sie benutzerdefinierte Hooks im `hooks`-Ordner.
## UI
Enthält alle wiederverwendbaren UI-Komponenten, die in der Anwendung verwendet werden.
Dieser Ordner kann Unterordner wie `data`, `display`, `feedback` und `input` für bestimmte Komponententypen enthalten. Jede Komponente sollte in sich geschlossen und wiederverwendbar sein, sodass Sie sie in verschiedenen Teilen der Anwendung verwenden können.
Indem Sie die UI-Komponenten von den anderen Komponenten im `modules`-Ordner trennen, ist es einfacher, ein konsistentes Design beizubehalten und Änderungen an der UI vorzunehmen, ohne andere Bereiche (Geschäftslogik) der Codebasis zu beeinflussen.
## Schnittstelle und Abhängigkeiten
Sie können Code anderer Module aus jedem Modul importieren, mit Ausnahme des `ui`-Ordners. Dadurch bleibt der Code leicht testbar.
### Intern
Jeder Teil (Hooks, Zustände, ...) eines Moduls kann einen `internal`-Ordner haben, der Teile enthält, die nur innerhalb des Moduls verwendet werden.

View File

@@ -0,0 +1,91 @@
---
title: Frontend-Befehle
icon: terminal
---
## Nützliche Befehle
### Die App starten
```bash
npx nx start twenty-front
```
### GraphQL-Schema basierend auf API-GraphQL-Schema regenerieren
```bash
npx nx run twenty-front:graphql:generate --configuration=metadata
```
ODER
```bash
npx nx run twenty-front:graphql:generate
```
### Lint
```bash
npx nx run twenty-front:lint # pass --fix to fix lint errors
```
## Übersetzungen
```bash
npx nx run twenty-front:lingui:extract
npx nx run twenty-front:lingui:compile
```
### Test
```bash
npx nx run twenty-front:test # run jest tests
npx nx run twenty-front:storybook:serve:dev # run storybook
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
```
## Technologie-Stack
Das Projekt hat einen sauberen und einfachen Stack mit minimalem Boilerplate-Code.
**App**
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Tests**
* [Jest](https://jestjs.io/)
* [Storybook](https://storybook.js.org/)
**Tools**
* [Yarn](https://yarnpkg.com/)
* [Craco](https://craco.js.org/docs/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
## Architektur
### Routing
[React Router](https://reactrouter.com/) übernimmt das Routing.
Um unnötige [re-renders](/l/de/developers/contribute/capabilities/frontend-development/best-practices-front#managing-re-renders) zu vermeiden, befindet sich die gesamte Routing-Logik in einem `useEffect` in `PageChangeEffect`.
### Zustandsverwaltung
[Jotai](https://jotai.org/) übernimmt die Zustandsverwaltung.
Siehe [Best Practices](/l/de/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) für mehr Informationen zur Zustandsverwaltung.
## Tests
[Jest](https://jestjs.io/) dient als Werkzeug für Unittests, während [Storybook](https://storybook.js.org/) für Komponententests verwendet wird.
Jest ist hauptsächlich für das Testen von Dienstprogrammfunktionen und nicht für die Komponenten selbst gedacht.
Storybook dient zum Testen des Verhaltens isolierter Komponenten sowie zur Darstellung des Designsystems.

View File

@@ -0,0 +1,178 @@
---
title: Tastenkombinationen
---
## Einführung
Wenn Sie eine Tastenkombination abhören müssen, verwenden Sie normalerweise den `onKeyDown` Event-Listener.
In `twenty-front` jedoch könnten Sie Konflikte zwischen gleichen Tastenkombinationen haben, die in verschiedenen Komponenten, die gleichzeitig eingebunden sind, verwendet werden.
Zum Beispiel, wenn Sie eine Seite haben, die auf die Enter-Taste hört, und ein Modal das auf die Enter-Taste hört, und ein Select-Element innerhalb dieses Modals auf die Enter-Taste hört, könnte ein Konflikt entstehen, wenn alle gleichzeitig eingebunden sind.
## Der `useScopedHotkeys` Hook
Um dieses Problem zu lösen, verfügen wir über einen benutzerdefinierten Hook, der es ermöglicht, auf Tastenkombinationen zu hören, ohne Konflikte.
Sie platzieren ihn in einer Komponente, und er wird auf die Tastenkombinationen nur hören, wenn die Komponente eingebunden ist UND wenn der spezifizierte **Tastenkombinationsbereich** aktiv ist.
## Wie hört man in der Praxis auf Tastenkombinationen?
Es gibt zwei Schritte, um das Abhören von Tastenkombinationen einzurichten:
1. Setzen Sie den [Tastenkombinationsbereich](#what-is-a-hotkey-scope-), der auf Tastenkombinationen hören wird
2. Verwenden Sie den `useScopedHotkeys` Hook, um auf Tastenkombinationen zu hören
Das Einrichten von Tastenkombinationsbereichen ist auch bei einfachen Seiten erforderlich, da andere UI-Elemente wie das linke Menü oder das Befehlsmenü ebenfalls auf Tastenkombinationen hören könnten.
## Anwendungsfälle für Tastenkombinationen
Im Allgemeinen haben Sie zwei Anwendungsfälle, die Tastenkombinationen erfordern:
1. Auf einer Seite oder einer Komponente, die auf einer Seite eingebunden ist
2. In einer Modal-Komponente, die wegen einer Benutzeraktion den Fokus erhält
Der zweite Anwendungsfall kann rekursiv auftreten: ein Dropdown in einem Modal zum Beispiel.
### Auf Tastenkombinationen in einer Seite hören
Beispiel:
```tsx
const PageListeningEnter = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// 1. Set the hotkey scope in a useEffect
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleEnterPage,
);
// Revert to the previous hotkey scope when the component is unmounted
return () => {
goBackToPreviousHotkeyScope();
};
}, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);
// 2. Use the useScopedHotkeys hook
useScopedHotkeys(
Key.Enter,
() => {
// Some logic executed on this page when the user presses Enter
// ...
},
ExampleHotkeyScopes.ExampleEnterPage,
);
return <div>My page that listens for Enter</div>;
};
```
### Auf Tastenkombinationen in einer Modal-Komponente hören
Für dieses Beispiel verwenden wir eine Modalkomponente, die auf die Escape-Taste hört, um ihrem übergeordneten Element mitzuteilen, dass sie geschlossen werden soll.
Hier ändert die Benutzerinteraktion den Bereich.
```tsx
const ExamplePageWithModal = () => {
const [showModal, setShowModal] = useState(false);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
// 1. Revert to the previous hotkey scope when the modal is closed
setShowModal(false);
goBackToPreviousHotkeyScope();
};
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
Dann in der Modalkomponente:
```tsx
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
// 2. Use the useScopedHotkeys hook to listen for Escape.
// Note that escape is a common hotkey that could be used by many other components
// So it's important to use a hotkey scope to avoid conflicts
useScopedHotkeys(
Key.Escape,
() => {
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
return <div>My modal component</div>;
};
```
Es ist wichtig, dieses Muster zu verwenden, wenn du nicht sicher bist, dass die Verwendung eines useEffect mit mount/unmount ausreicht, um Konflikte zu vermeiden.
Diese Konflikte können schwer zu debuggen sein, und es kann öfter passieren, als nicht mit useEffects.
## Was ist ein Tastenkombinationsbereich?
Ein Tastenkombinationsbereich ist ein String, der einen Kontext darstellt, in dem die Tastenkombinationen aktiv sind. Er wird im Allgemeinen als Enum kodiert.
Wenn Sie den Tastenkombinationsbereich ändern, werden die Tastenkombinationen aktiviert, die auf diesen Bereich hören, und die Tastenkombinationen, die auf andere Bereiche hören, werden deaktiviert.
Sie können immer nur einen Bereich gleichzeitig setzen.
Als Beispiel sind die Tastenkombinationsbereiche für jede Seite im `PageHotkeyScope` Enum definiert:
```tsx
export enum PageHotkeyScope {
Settings = 'settings',
CreateWorkspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
PlanRequired = 'plan-required',
ShowPage = 'show-page',
PersonShowPage = 'person-show-page',
CompanyShowPage = 'company-show-page',
CompaniesPage = 'companies-page',
PeoplePage = 'people-page',
OpportunitiesPage = 'opportunities-page',
ProfilePage = 'profile-page',
WorkspaceMemberPage = 'workspace-member-page',
TaskPage = 'task-page',
}
```
Intern wird der aktuell ausgewählte Bereich in einem Jotai-Atom gespeichert, das in der gesamten Anwendung geteilt wird:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',
defaultValue: INITIAL_HOTKEYS_SCOPE,
});
```
Aber dieses Atom sollte niemals manuell bearbeitet werden! Wir werden im nächsten Abschnitt sehen, wie man es verwendet.
## Wie funktioniert es intern?
Wir haben eine dünne Schicht über [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) erstellt, die es leistungsfähiger macht und unnötige Neu-Renderings vermeidet.
Wir erstellen außerdem ein Jotai-Atom, um den Zustand des Tastenkombinationsbereichs zu verwalten und in der gesamten Anwendung verfügbar zu machen.

View File

@@ -0,0 +1,8 @@
---
title: Storybook
description: Durchsuchen Sie die UI-Komponentenbibliothek von Twenty
---
Sehen Sie sich unsere vollständige Komponentenbibliothek und Dokumentation in Storybook an.
[Storybook öffnen →](https://storybook.twenty.com)

View File

@@ -0,0 +1,291 @@
---
title: Styleguide
icon: paintbrush
---
Dieses Dokument enthält die Regeln, die beim Schreiben von Code beachtet werden müssen.
Das Ziel ist es, eine konsistente Codebasis zu haben, die leicht lesbar und einfach zu warten ist.
Hierfür ist es besser, etwas ausführlicher zu sein als zu knapp.
Denken Sie daran, dass Code häufiger gelesen als geschrieben wird, insbesondere bei einem Open-Source-Projekt, zu dem jeder beitragen kann.
Es gibt viele Regeln, die hier nicht definiert sind, die aber automatisch durch Linters überprüft werden.
## React
### Verwenden Sie funktionale Komponenten
Verwenden Sie immer TSX-Funktionskomponenten.
Vermeiden Sie `import` mit `const`, da es schwieriger zu lesen und schwerer mit Code-Vervollständigung zu importieren ist.
```tsx
// ❌ Bad, harder to read, harder to import with code completion
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Props
Erstellen Sie den Typ der Eigenschaften (props) und nennen Sie ihn `(ComponentName)Props`, wenn es nicht notwendig ist, ihn zu exportieren.
Verwenden Sie Destrukturierung der Props.
```tsx
// ❌ Bad, no type
export const MyComponent = (props) => <div>Hello {props.name}</div>;
// ✅ Good, type
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### Vermeiden Sie die Verwendung von `React.FC` oder `React.FunctionComponent`, um Prop-Typen zu definieren
```tsx
/* ❌ - Bad, defines the component type annotations with `FC`
* - With `React.FC`, the component implicitly accepts a `children` prop
* even if it's not defined in the prop type. This might not always be
* desirable, especially if the component doesn't intend to render
* children.
*/
const EmailField: React.FC<{
value: string;
}> = ({ value }) => <TextInput value={value} disabled fullWidth />;
```
```tsx
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
* component's props
* - This method doesn't automatically include the children prop. If
* you want to include it, you have to specify it in OwnProps.
*/
type EmailFieldProps = {
value: string;
};
const EmailField = ({ value }: EmailFieldProps) => (
<TextInput value={value} disabled fullWidth />
);
```
#### Kein einzelnes Variablen-Propspreading in JSX-Elementen
Vermeiden Sie das Propspreading einzelner Variablen in JSX-Elementen, wie `{...props}`. Diese Praxis führt oft zu weniger lesbarem und schwer wartbarem Code, da unklar ist, welche Props die Komponente erhält.
```tsx
/* ❌ - Bad, spreads a single variable prop into the underlying component
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
}
```
```tsx
/* ✅ - Good, Explicitly lists all props
* - Enhances readability and maintainability
*/
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
};
```
Rational
* Auf einen Blick ist klarer, welche Props der Code übergibt, was das Verständnis und die Wartung erleichtert.
* Es verhindert eine enge Kopplung von Komponenten über ihre Props.
* Linting-Tools erleichtern das Erkennen falsch geschriebener oder unbenutzter Props, wenn Sie Props explizit auflisten.
## JavaScript
### Verwenden Sie den Nullish-Coalescing-Operator `??`
```tsx
// ❌ Bad, can return 'default' even if value is 0 or ''
const value = process.env.MY_VALUE || 'default';
// ✅ Good, will return 'default' only if value is null or undefined
const value = process.env.MY_VALUE ?? 'default';
```
### Verwenden Sie optionales Chaining `?.`
```tsx
// ❌ Bad
onClick && onClick();
// ✅ Good
onClick?.();
```
## TypeScript
### Verwenden Sie `type` anstelle von `interface`
Verwenden Sie immer `type` anstelle von `interface`, da sie fast immer überlappen und `type` flexibler ist.
```tsx
// ❌ Bad
interface MyInterface {
name: string;
}
// ✅ Good
type MyType = {
name: string;
};
```
### Verwenden Sie String-Literale anstelle von Enums
[String-Literale](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) sind die bevorzugte Methode, um enum-ähnliche Werte in TypeScript zu handhaben. Sie sind einfacher mit Pick und Omit zu erweitern und bieten eine bessere Entwicklererfahrung, vor allem mit Code-Vervollständigung.
Warum TypeScript empfiehlt, Enums zu vermeiden, sehen Sie [hier](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
```
```tsx
// ✅ Good, utilizes a string literal
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL und interne Bibliotheken
Sie sollten die von GraphQL Codegen generierten Enums verwenden.
Es ist auch besser, ein Enum zu verwenden, wenn eine interne Bibliothek verwendet wird, damit die interne Bibliothek keinen String-Literal-Typ freigeben muss, der nicht zur internen API gehört.
Beispiel:
```TSX
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
```
## Styling
### Verwenden Sie StyledComponents
Stylen Sie die Komponenten mit [Linaria styled](https://github.com/callstack/linaria).
```tsx
// ❌ Bad
<div className="my-class">Hello World</div>
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
Prefixen Sie stilisierte Komponenten mit "Styled", um sie von "echten" Komponenten zu unterscheiden.
```tsx
// ❌ Bad
const Title = styled.div`
color: red;
`;
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
### Themenbindung
Die Nutzung des Themas für den Großteil der Komponenten-Styling ist der bevorzugte Ansatz.
#### Einheiten von Messungen
Vermeiden Sie die Verwendung direkter `px`- oder `rem`-Werte innerhalb der gestylten Komponenten. Die erforderlichen Werte sind in der Regel bereits im Thema definiert, daher wird empfohlen, das Thema für diese Zwecke zu nutzen.
#### Farben
Vermeiden Sie es, neue Farben einzuführen; verwenden Sie stattdessen die vorhandene Palette aus dem Thema. Sollte es eine Situation geben, in der die Palette nicht übereinstimmt, hinterlassen Sie bitte einen Kommentar, damit das Team dies korrigieren kann.
```tsx
// ❌ Bad, directly specifies style values without utilizing the theme
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
font-weight: 400;
margin-left: 4px;
border-radius: 50px;
`;
```
```tsx
// ✅ Good, utilizes the theme
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
## Durchsetzung von No-Type Imports
Vermeiden Sie Typ-Importe. Um diesen Standard durchzusetzen, überprüft eine Oxlint-Regel alle Typ-Importe und meldet sie. Dies trägt zur Konsistenz und Lesbarkeit des TypeScript-Codes bei.
```tsx
// ❌ Bad
import { type Meta, type StoryObj } from '@storybook/react';
// ❌ Bad
import type { Meta, StoryObj } from '@storybook/react';
// ✅ Good
import { Meta, StoryObj } from '@storybook/react';
```
### Warum keine Typ-Importe?
* **Konsistenz**: Durch das Vermeiden von Typ-Importen und die Verwendung eines einzigen Ansatzes für sowohl Typ- als auch Wertimporte bleibt die Modulimportstruktur der Codebasis konsistent.
* **Lesbarkeit**: Keine Typ-Importe verbessern die Lesbarkeit des Codes, da klar wird, wann Werte oder Typen importiert werden. Dies reduziert die Zweideutigkeit und erleichtert das Verständnis des Zwecks der importierten Symbole.
* **Wartbarkeit**: Es verbessert die Wartbarkeit der Codebasis, da Entwickler Typ-Only-Imports beim Überprüfen oder Ändern von Code identifizieren und lokalisieren können.
### Oxlint-Regel
Eine Oxlint-Regel, `typescript/consistent-type-imports`, setzt den No-Type-Import-Standard durch. Diese Regel generiert Fehler oder Warnungen bei Verstößen gegen Typ-Importe.
Bitte beachten Sie, dass diese Regel speziell seltene Randfälle behandelt, in denen unbeabsichtigte Typ-Importe auftreten. TypeScript selbst lehnt diese Praxis ab, wie in den [TypeScript 3.8 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html) erwähnt. In den meisten Situationen sollten Typ-Only-Imports nicht benötigt werden.
Um sicherzustellen, dass Ihr Code mit dieser Regel übereinstimmt, achten Sie darauf, Oxlint als Teil Ihres Entwicklungsworkflows auszuführen.

View File

@@ -0,0 +1,60 @@
---
title: Mit Figma arbeiten
info: Erfahren Sie, wie Sie im Figma von Twenty zusammenarbeiten können.
---
Figma ist ein kollaboratives Interface-Design-Tool, das dazu beiträgt, die Kommunikationsbarriere zwischen Designern und Entwicklern zu überwinden.
Dieser Leitfaden erklärt, wie Sie mit Figma zusammenarbeiten können.
## Zugriff
1. **Auf den freigegebenen Link zugreifen:** Sie können auf die Figma-Datei des Projekts [hier](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty) zugreifen.
2. **Anmelden:** Wenn Sie nicht bereits angemeldet sind, werden Sie von Figma dazu aufgefordert.
Schlüsselfunktionen sind nur für angemeldete Benutzer verfügbar, wie der Entwicklermodus und die Möglichkeit, einen dedizierten Rahmen auszuwählen.
<Warning>
Ohne ein Konto können Sie nicht effektiv zusammenarbeiten.
</Warning>
## Figma-Struktur
In der linken Seitenleiste können Sie auf die verschiedenen Seiten der Figma-Datei von Twenty zugreifen. So sind sie organisiert:
* **Komponentenseite:** Dies ist die erste Seite. Der Designer verwendet sie zur Erstellung und Organisation der wiederverwendbaren Designelemente, die in der gesamten Designdatei verwendet werden. Zum Beispiel Schaltflächen, Icons, Symbole oder andere wiederverwendbare Komponenten. Sie dient dazu, die Konsistenz im gesamten Design beizubehalten.
* **Hauptseite:** Die zweite Seite ist die Hauptseite, die die vollständige Benutzeroberfläche des Projekts zeigt. Sie können ***Play*** drücken, um den vollständigen App-Prototypen zu verwenden.
* **Funktionsseiten:** Die anderen Seiten sind normalerweise den in Arbeit befindlichen Funktionen gewidmet. Sie enthalten das Design spezifischer Funktionen oder Module der Anwendung oder Website. Normalerweise sind sie noch in Arbeit.
## Nützliche Tipps
Mit Lesezugriff können Sie das Design nicht bearbeiten, aber Sie können alle Funktionen nutzen, die nützlich sind, um die Designs in Code zu konvertieren.
### Verwenden Sie den Dev-Modus
Der Dev-Modus von Figma verbessert die Produktivität von Entwicklern, indem er eine einfache Navigation im Design, effektive Ressourcenverwaltung, effiziente Kommunikationstools, Toolbox-Integrationen, schnelle Codeschnipsel und wichtige Ebeneninformationen bereitstellt, die die Lücke zwischen Design und Entwicklung schließen. Weitere Informationen über den Dev-Modus finden Sie [hier](https://www.figma.com/dev-mode/).
Wechseln Sie in der rechten Teil der Toolbar in den "Entwickler"-Modus, um Designspezifikationen anzuzeigen, CSS zu kopieren und auf Ressourcen zuzugreifen.
### Den Prototyp verwenden
Klicken Sie auf ein beliebiges Element auf der Leinwand und drücken Sie die Schaltfläche “Play” oben rechts in der Oberfläche, um die Prototyp-Ansicht zu öffnen. Der Prototypenmodus ermöglicht es Ihnen, mit dem Design zu interagieren, als wäre es das endgültige Produkt. Er zeigt den Fluss zwischen Bildschirmen und wie sich Interface-Elemente wie Schaltflächen, Links oder Menüs bei Interaktionen verhalten.
1. **Übergänge und Animationen verstehen:** Im Prototypenmodus können Sie Übergänge oder Animationen sehen, die ein Designer zwischen Bildschirmen oder UI-Elementen hinzugefügt hat, um Entwicklern klare visuelle Anweisungen zu beabsichtigtem Verhalten und Stil zu bieten.
2. **Implementierungsklarheit:** Ein Prototyp kann auch helfen, Unklarheiten zu reduzieren. Entwickler können damit interagieren, um ein besseres Verständnis der Funktionalität oder des Aussehens bestimmter Elemente zu erlangen.
Für umfassendere Details und Anleitungen zum Erlernen der Figma-Plattform können Sie die offizielle [Figma-Dokumentation](https://help.figma.com/hc/en-us) besuchen.
### Abstände messen
Wählen Sie ein Element aus, halten Sie die `Option`-Taste (Mac) oder `Alt`-Taste (Windows) gedrückt und fahren Sie dann mit der Maus über ein anderes Element, um den Abstand zwischen ihnen anzuzeigen.
### Figma-Erweiterung für VSCode (Empfohlen)
[Figma für VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension) ermöglicht es Ihnen, durch Design-Dateien zu navigieren und diese zu inspizieren, mit Designern zusammenzuarbeiten, Änderungen zu verfolgen und die Implementierung zu beschleunigen - alles, ohne Ihren Texteditor zu verlassen.
Es ist Teil unserer empfohlenen Erweiterungen.
## Zusammenarbeit
1. **Kommentare verwenden:** Sie sind eingeladen, die Kommentarfunktion zu nutzen, indem Sie auf das Blasensymbol im linken Teil der Toolbar klicken.
2. **Cursor-Chat:** Eine nette Funktion von Figma ist der Cursor-Chat. Drücken Sie einfach `;` auf Mac und `/` unter Windows, um eine Nachricht zu senden, wenn Sie sehen, dass jemand anderes Figma zur gleichen Zeit wie Sie verwendet.

View File

@@ -0,0 +1,318 @@
---
title: Lokale Einrichtung
icon: laptop-code
description: Der Leitfaden für Mitwirkende (oder neugierige Entwickler), die Twenty lokal ausführen möchten.
---
## Voraussetzungen
<Tabs>
<Tab title="Linux und macOS">
Bevor Sie Twenty installieren und verwenden können, stellen Sie sicher, dass Sie Folgendes auf Ihrem Computer installiert haben:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
`npm` wird nicht funktionieren, Sie sollten stattdessen `yarn` verwenden. Yarn wird jetzt mit Node.js geliefert, so dass Sie es nicht separat installieren müssen.
Sie müssen nur `corepack enable` ausführen, um Yarn zu aktivieren, wenn Sie das noch nicht getan haben.
</Warning>
</Tab>
<Tab title="Windows (WSL)">
1. Installieren Sie WSL
Öffnen Sie PowerShell als Administrator und führen Sie aus:
```powershell
wsl --install
```
Sie sollten nun eine Aufforderung sehen, Ihren Computer neu zu starten. Wenn nicht, starten Sie ihn manuell neu.
Nach dem Neustart wird ein PowerShell-Fenster geöffnet und Ubuntu installiert. Dies kann einige Zeit in Anspruch nehmen.
Sie werden aufgefordert, einen Benutzernamen und ein Passwort für Ihre Ubuntu-Installation zu erstellen.
2. Git installieren und konfigurieren
```bash
sudo apt-get install git
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
```
3. nvm, node.js und yarn installieren
<Warning>
Verwenden Sie `nvm`, um die korrekte `node`-Version zu installieren. Die `.nvmrc` stellt sicher, dass alle Mitwirkenden die gleiche Version verwenden.
</Warning>
```bash
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
Schließen und öffnen Sie Ihr Terminal erneut, um nvm zu verwenden. Führen Sie dann die folgenden Befehle aus.
```bash
nvm install # installs recommended node version
nvm use # use recommended node version
corepack enable
```
</Tab>
</Tabs>
---
## Schritt 1: Git klonen
Führen Sie in Ihrem Terminal den folgenden Befehl aus.
<Tabs>
<Tab title="SSH (Empfohlen)">
Wenn Sie SSH-Schlüssel noch nicht eingerichtet haben, können Sie [hier](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh) erfahren, wie das geht.
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
<Tab title="HTTPS">
```bash
git clone https://github.com/twentyhq/twenty.git
```
</Tab>
</Tabs>
## Schritt 2: Positionieren Sie sich im Stammverzeichnis
```bash
cd twenty
```
Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus auszuführen.
## Schritt 3: Einrichten einer PostgreSQL-Datenbank
<Tabs>
<Tab title="Linux">
**Option 1 (bevorzugt):** Um Ihre Datenbank lokal bereitzustellen:
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrem Linux-Rechner zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Hinweis: Möglicherweise müssen Sie `sudo -u postgres` dem Befehl vor `psql` hinzufügen, um Berechtigungsfehler zu vermeiden.
**Option 2:** Wenn Sie Docker installiert haben:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (bevorzugt):** Um Ihre Datenbank lokal mit `brew` bereitzustellen:
```bash
brew install postgresql@16
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"
brew services start postgresql@16
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Sie können überprüfen, ob der PostgreSQL-Server läuft, indem Sie folgendes ausführen:
```bash
brew services list
```
Das Installationsprogramm erstellt den Benutzer `postgres` möglicherweise nicht standardmäßig bei der Installation
über Homebrew auf macOS. Stattdessen wird eine PostgreSQL-Rolle erstellt, die Ihrem macOS
Benutzernamen (z. B. "john") entspricht.
Um zu überprüfen und, falls erforderlich, den Benutzer `postgres` zu erstellen, führen Sie folgende Schritte aus:
```bash
# Connect to PostgreSQL
psql postgres
or
psql -U $(whoami) -d postgres
```
Sobald Sie sich an der psql-Eingabeaufforderung (postgres=#) befinden, führen Sie aus:
```bash
# List existing PostgreSQL roles
\du
```
Sie werden eine Ausgabe ähnlich der folgenden sehen:
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
john | Superuser | {}
```
Wenn Sie keine `postgres`-Rolle sehen, fahren Sie mit dem nächsten Schritt fort.
Erstellen Sie die Rolle `postgres` manuell:
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
Dadurch wird eine Superuser-Rolle namens `postgres` mit Anmeldezugriff erstellt.
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
postgres | Superuser | {}
john | Superuser | {}
```
**Option 2:** Wenn Sie Docker installiert haben:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
Alle folgenden Schritte sind im WSL-Terminal auszuführen (innerhalb Ihrer virtuellen Maschine)
**Option 1:** Um Ihr PostgreSQL lokal bereitzustellen:
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrer Linux-VM zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Hinweis: Möglicherweise müssen Sie `sudo -u postgres` dem Befehl vor `psql` hinzufügen, um Berechtigungsfehler zu vermeiden.
**Option 2:** Wenn Sie Docker installiert haben:
Die Ausführung von Docker auf WSL fügt eine zusätzliche Komplexitätsschicht hinzu.
Verwenden Sie diese Option nur, wenn Sie mit den zusätzlichen Schritten vertraut sind, einschließlich der Aktivierung von [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
Sie können nun über `localhost:5432` auf die Datenbank zugreifen.
Wenn Sie die oben genannte Docker-Option verwendet haben, lauten die Standardanmeldedaten Benutzer `postgres` und Passwort `postgres`. Für native PostgreSQL-Installationen verwenden Sie die auf Ihrem Rechner konfigurierten Anmeldedaten und Rollen.
## Schritt 4: Einrichten einer Redis-Datenbank (Cache)
Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten.
<Tabs>
<Tab title="Linux">
**Option 1:** Bereitstellung von Redis lokal:
Verwenden Sie den folgenden Link, um Redis auf Ihrem Linux-Rechner zu installieren: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Option 2:** Wenn Sie Docker installiert haben:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (bevorzugt):** Bereitstellung von Redis lokal mit `brew`:
```bash
brew install redis
```
Starten Sie Ihren Redis-Server:
```bash
brew services start redis
```
**Option 2:** Wenn Sie Docker installiert haben:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
**Option 1:** Um Redis lokal bereitzustellen:
Verwenden Sie den folgenden Link, um Redis auf Ihrer Linux-VM zu installieren: [Redis-Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Option 2:** Wenn Sie Docker installiert haben:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
Wenn Sie eine Client-GUI benötigen, empfehlen wir [Redis Insight](https://redis.io/insight/) (kostenlose Version verfügbar).
## Schritt 5: Einrichten von Umgebungsvariablen
Verwenden Sie Umgebungsvariablen oder `.env`-Dateien, um Ihr Projekt zu konfigurieren. Weitere Informationen [hier](/l/de/developers/self-host/capabilities/setup).
Kopieren Sie die `.env.example`-Dateien in `/front` und `/server`:
```bash
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**Multi-Workspace-Modus:** Standardmäßig läuft Twenty im Einzel-Workspace-Modus, in dem nur ein Workspace erstellt werden kann. Um die Multi-Workspace-Unterstützung zu aktivieren (nützlich zum Testen von Subdomain-basierten Funktionen), setzen Sie `IS_MULTIWORKSPACE_ENABLED=true` in der `.env`-Datei Ihres Servers. Siehe [Multi-Workspace-Modus](/l/de/developers/self-host/capabilities/setup#multi-workspace-mode) für Details.
</Info>
## Schritt 6: Abhängigkeiten installieren
Um den Twenty-Server zu bauen und einige Daten in Ihre Datenbank zu seeden, führen Sie den folgenden Befehl aus:
```bash
yarn
```
Bitte beachten Sie, `npm` oder `pnpm` funktionieren nicht.
## Schritt 7: Das Projekt ausführen
<Tabs>
<Tab title="Linux">
Je nach Ihrer Linux-Distribution könnte der Redis-Server automatisch gestartet werden.
Wenn nicht, überprüfen Sie den [Redis Installationsleitfaden](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) für Ihre Distribution.
</Tab>
<Tab title="Mac OS">
Redis sollte bereits laufen. Falls nicht, führen Sie aus:
```bash
brew services start redis
```
</Tab>
<Tab title="Windows (WSL)">
Je nach Ihrer Linux-Distribution könnte der Redis-Server automatisch gestartet werden.
Wenn nicht, überprüfen Sie den [Redis Installationsleitfaden](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) für Ihre Distribution.
</Tab>
</Tabs>
Richten Sie Ihre Datenbank mit folgendem Befehl ein:
```bash
npx nx database:reset twenty-server
```
Starten Sie den Server, den Worker und die Frontend-Dienste:
```bash
npx nx start twenty-server
npx nx worker twenty-server
npx nx start twenty-front
```
Alternativ können Sie alle Dienste auf einmal starten:
```bash
npx nx start
```
## Schritt 8: Verwenden Sie Twenty
**Frontend**
Das Frontend von Twenty läuft unter [http://localhost:3001](http://localhost:3001).
Sie können sich mit dem Standard-Demokonto anmelden: `tim@apple.dev` (Passwort: `tim@apple.dev`)
**Backend**
* Der Server von Twenty wird unter [http://localhost:3000](http://localhost:3000) verfügbar sein.
* Die GraphQL-API kann unter [http://localhost:3000/graphql](http://localhost:3000/graphql) erreicht werden.
* Die REST-API ist erreichbar unter [http://localhost:3000/rest](http://localhost:3000/rest)
## Fehlerbehebung
Sollten Sie auf ein Problem stoßen, schauen Sie sich die [Fehlerbehebung](/l/de/developers/self-host/capabilities/troubleshooting) für Lösungen an.

View File

@@ -0,0 +1,77 @@
---
title: Befehle
icon: terminal
description: Nützliche Befehle für die Entwicklung von Twenty.
---
Befehle können vom Repository-Stammverzeichnis mit `npx nx` ausgeführt werden. Verwende `npx nx run {project}:{command}` für eine explizite Zielangabe.
## Die App starten
```bash
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
npx nx start twenty-server # Backend server (http://localhost:3000)
npx nx run twenty-server:worker # Background worker
```
## Datenbank
```bash
npx nx database:reset twenty-server # Reset and seed database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
```
## Linting
```bash
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
npx nx lint:diff-with-main twenty-server
npx nx lint twenty-front --configuration=fix # Auto-fix
```
## Typprüfung
```bash
npx nx typecheck twenty-front
npx nx typecheck twenty-server
```
## Tests
```bash
# Frontend
npx nx test twenty-front # Jest unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:test twenty-front # Storybook tests
# Backend
npx nx run twenty-server:test:unit # Unit tests
npx nx run twenty-server:test:integration # Integration tests
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
# Single file (fastest)
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
```
## GraphQL
```bash
npx nx run twenty-front:graphql:generate # Regenerate types
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
```
## Übersetzungen
```bash
npx nx run twenty-front:lingui:extract # Extract strings
npx nx run twenty-front:lingui:compile # Compile translations
```
## Build
```bash
npx nx build twenty-shared # Must be built first
npx nx build twenty-front
npx nx build twenty-server
```

View File

@@ -0,0 +1,31 @@
---
title: Mitwirken
description: Tragen Sie zur Open-Source-Entwicklung von Twenty bei.
---
<Frame>
<img src="/images/user-guide/github/github-header.png" alt="KI" />
</Frame>
## Überblick
Twenty ist Open Source und freut sich über Beiträge aus der Community. Ob Sie Fehler beheben, Funktionen hinzufügen oder die Dokumentation verbessern, Ihre Beiträge tragen dazu bei, Twenty für alle zu verbessern.
## Möglichkeiten zum Mitwirken
* **Fehler melden**: Helfen Sie, Probleme zu identifizieren und zu dokumentieren
* **Funktionen einreichen**: Schlagen Sie neue Funktionalität vor und implementieren Sie sie
* **Dokumentation verbessern**: Machen Sie unsere Dokumentation klarer und hilfreicher
* **Frontend-Entwicklung**: Arbeiten Sie an der React-basierten UI
* **Backend-Entwicklung**: Tragen Sie zum NestJS-Server bei
## Erste Schritte
<CardGroup cols={2}>
<Card title="Fehlermeldungen und Anfragen" icon="bug" href="/l/de/developers/contribute/capabilities/bug-and-requests">
Melden Sie Probleme oder schlagen Sie neue Funktionen vor
</Card>
<Card title="Frontend-Entwicklung" icon="browser" href="/l/de/developers/contribute/capabilities/frontend-development/folder-architecture-front">
Tragen Sie zur UI bei
</Card>
</CardGroup>

View File

@@ -0,0 +1,176 @@
---
title: Styleguide
icon: paintbrush
description: Code-Konventionen und bewährte Verfahren für Beiträge zu Twenty.
---
## React
### Ausschließlich funktionale Komponenten
Verwenden Sie immer TSX-Funktionskomponenten mit benannten Exporten.
```tsx
// ❌ Bad
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Props
Erstellen Sie einen Typ namens `{ComponentName}Props`. Verwenden Sie Destrukturierung. Verwenden Sie `React.FC` nicht.
```tsx
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### Kein Prop-Spreading mit einer einzelnen Variablen
```tsx
// ❌ Bad
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## Zustandsverwaltung
### Jotai-Atome für globalen Zustand
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
```
* Bevorzugen Sie Atome gegenüber Prop-Drilling
* Verwenden Sie `useRef` nicht für den Zustand — verwenden Sie `useState` oder Atome
* Verwenden Sie Atomfamilien und Selektoren für Listen
### Vermeiden Sie unnötige Re-Renders
* Lagern Sie `useEffect` und das Daten-Fetching in gleichrangige Sidecar-Komponenten aus
* Bevorzugen Sie Event-Handler (`handleClick`, `handleChange`) gegenüber `useEffect`
* Verwenden Sie `React.memo()` nicht — beheben Sie stattdessen die Grundursache
* Beschränken Sie die Nutzung von `useCallback`/`useMemo`
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
// ✅ Good — extract into sibling
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
const [data] = useAtomState(dataState);
return <div>{data}</div>;
};
```
## TypeScript
* **`type` statt `interface`** — flexibler, leichter zu kombinieren
* **String-Literale statt Enums** — außer für GraphQL-Codegen-Enums und interne Bibliotheks-APIs
* **Kein `any`** — striktes TypeScript wird durchgesetzt
* **Keine Type-Imports** — verwenden Sie reguläre Imports (erzwungen durch Oxlint `typescript/consistent-type-imports`)
* **Verwenden Sie [Zod](https://github.com/colinhacks/zod)** für die Laufzeitvalidierung ungetypter Objekte
## JavaScript
```tsx
// Use nullish-coalescing (??) instead of ||
const value = process.env.MY_VALUE ?? 'default';
// Use optional chaining
onClick?.();
```
## Namensgebung
* **Variablen**: camelCase, aussagekräftig (`email` statt `value`, `fieldMetadata` statt `fm`)
* **Konstanten**: SCREAMING_SNAKE_CASE
* **Typen/Klassen**: PascalCase
* **Dateien/Verzeichnisse**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **Event-Handler**: `handleClick` (nicht `onClick` für die Handler-Funktion)
* **Komponenten-Props**: mit dem Komponentennamen präfixieren (`ButtonProps`)
* **Styled Components**: mit `Styled` präfixieren (`StyledTitle`)
## Styling
Verwenden Sie [Linaria](https://github.com/callstack/linaria) Styled Components. Verwenden Sie Theme-Werte — vermeiden Sie hartkodierte `px`, `rem` oder Farben.
```tsx
// ❌ Bad
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
margin-left: 4px;
`;
// ✅ Good
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
```
## Importe
Verwenden Sie Aliase statt relativer Pfade:
```tsx
// ❌ Bad
import { Foo } from '../../../../../testing/decorators/Foo';
// ✅ Good
import { Foo } from '~/testing/decorators/Foo';
import { Bar } from '@/modules/bar/components/Bar';
```
## Ordnerstruktur
```
front
└── modules/ # Feature modules
│ └── module1/
│ ├── components/
│ ├── constants/
│ ├── contexts/
│ ├── graphql/ (fragments, queries, mutations)
│ ├── hooks/
│ ├── states/ (atoms, selectors)
│ ├── types/
│ └── utils/
└── pages/ # Route-level components
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
* Module können aus anderen Modulen importieren, aber `ui/` sollte abhängigkeitsfrei bleiben
* Verwenden Sie `internal/`-Unterordner für modulinternen Code
* Komponenten unter 300 Zeilen, Services unter 500 Zeilen

View File

@@ -0,0 +1,55 @@
---
title: APIs
icon: plug
description: Vom Schema Ihres Arbeitsbereichs generierte REST- und GraphQL-APIs.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## Schema-pro-Mandant-APIs
Es gibt keine statische API-Referenz für Twenty. Jeder Arbeitsbereich hat sein eigenes Schema — wenn Sie ein benutzerdefiniertes Objekt hinzufügen (z. B. `Invoice`), erhält es sofort REST- und GraphQL-Endpunkte, die mit den integrierten Objekten wie `Company` oder `Person` identisch sind. Die API wird aus dem Schema generiert, daher verwenden die Endpunkte Ihre Objekt- und Feldnamen direkt — keine undurchsichtigen IDs.
Ihre arbeitsbereichsspezifische API-Dokumentation ist nach dem Erstellen eines API-Schlüssels unter **Einstellungen → API & Webhooks** verfügbar. Sie umfasst einen interaktiven Playground, in dem Sie echte Aufrufe gegen Ihre Daten ausführen können.
## Zwei APIs
**Core-API** — `/rest/` und `/graphql/`
CRUD für Datensätze: Personen, Unternehmen, Verkaufschancen, Ihre benutzerdefinierten Objekte. Abfragen, filtern, Beziehungen durchlaufen.
**Metadaten-API** — `/rest/metadata/` und `/metadata/`
Schemaverwaltung: Objekte, Felder und Beziehungen erstellen/ändern/löschen. So ändern Sie Ihr Datenmodell programmatisch.
Beide sind als REST und GraphQL verfügbar. GraphQL bietet Batch-Upserts und die Möglichkeit, Beziehungen in einer einzigen Abfrage zu durchlaufen. Die zugrunde liegenden Daten sind in beiden Fällen gleich.
## Basis-URLs
| Umgebung | Basis-URL |
| ------------- | ------------------------- |
| Cloud | `https://api.twenty.com/` |
| Selbsthosting | `https://{your-domain}/` |
## Authentifizierung
```
Authorization: Bearer YOUR_API_KEY
```
Erstellen Sie einen API-Schlüssel unter **Settings > APIs & Webhooks > + Create key**. Kopieren Sie ihn sofort — er wird nur einmal angezeigt. Schlüssel können unter **Settings → Members → Roles → Assignment tab** auf eine bestimmte Rolle beschränkt werden, um ihren Zugriff einzuschränken.
<VimeoEmbed videoId="928786722" title="API-Schlüssel erstellen" />
Für OAuth-basierten Zugriff (externe Apps, die im Namen von Nutzern handeln), siehe [OAuth](/l/de/developers/extend/oauth).
## Batch-Vorgänge
Sowohl REST als auch GraphQL unterstützen Batching von bis zu 60 Datensätzen pro Anfrage — erstellen, aktualisieren oder löschen. GraphQL unterstützt außerdem Batch-Upsert (Erstellen-oder-Aktualisieren in einem Aufruf) mit Pluralnamen wie `CreateCompanies`.
## Rate Limits
| Limit | Wert |
| ----------- | ------------------------ |
| Anfragen | 100 Aufrufe pro Minute |
| Batch-Größe | 60 Datensätze pro Aufruf |

View File

@@ -0,0 +1,124 @@
---
title: App-Konfiguration
description: Deklarieren Sie die Identität, die Standardrolle, Variablen und Marktplatz-Metadaten Ihrer App mit `defineApplication`.
icon: rocket
---
Jede App muss genau einen Aufruf von `defineApplication` haben. Dieser deklariert:
* **Identität** — universeller Bezeichner, Anzeigename, Beschreibung.
* **Berechtigungen** — unter welcher Rolle ihre Logikfunktionen und Frontend-Komponenten ausgeführt werden.
* **Variablen** *(optional)* — Schlüssel–Wert-Paare, die Ihrem Code als Umgebungsvariablen zur Verfügung gestellt werden.
* **Pre-install-/Post-install-/Uninstall-Hooks** *(optional)* — siehe [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
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. In Logikfunktionen (serverseitig) sind sie als `process.env.VARIABLE_NAME` verfügbar. In Frontend-Komponenten verwenden Sie `getApplicationVariable('VARIABLE_NAME')` aus `twenty-sdk/front-component`. Variablen, die mit `isSecret: true` gekennzeichnet sind, werden nur in Logikfunktionen injiziert. Frontend-Komponenten erhalten nur nicht-geheime Variablen.
* Die Standardrolle wird automatisch aus der Rollen-Datei erkannt, die mit [`defineApplicationRole()`](/l/de/developers/extend/apps/config/roles) markiert ist – Sie müssen sie nicht aus `defineApplication()` referenzieren.
* Pre-Installations-, Post-Installations- und Deinstallationsfunktionen werden während des Manifest-Builds automatisch erkannt — Sie müssen sie in `defineApplication()` nicht referenzieren.
* Die explizite Übergabe von `defaultRoleUniversalIdentifier` wird für die Abwärtskompatibilität weiterhin unterstützt, ist jedoch zugunsten von `defineApplicationRole()` veraltet.
* `serverVariables` sind instanzbezogene Konfigurationen und Geheimnisse (z. B. API-Schlüssel). Im Gegensatz zu `applicationVariables` haben sie im Manifest keinen Wert definiert – die Workspace-Operatorin bzw. der Workspace-Operator trägt sie in den App-Einstellungen ein, und sie werden erst dann in Logikfunktionen eingespeist, wenn sie gesetzt sind.
* Beide Arten von Variablen akzeptieren `isDeprecated: true`. Verwenden Sie dies, um eine Variable außer Betrieb zu nehmen, anstatt sie zu löschen: Wenn der Schlüssel deklariert bleibt, wird der gespeicherte Wert beibehalten (beim Löschen wird der vom Operator eingegebene Wert zerstört), und die Variable wird weiterhin injiziert, sodass Ihr Code darauf zurückfallen kann — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Eine veraltete Variable verschwindet aus den App-Einstellungen, sobald sie keinen Wert mehr hat, und wird niemals bei der Konfigurationsprüfung der App berücksichtigt, sodass `isDeprecated` gegenüber `isRequired` Vorrang hat.
* Um eine benutzerdefinierte Konfigurationsoberfläche im Tab **Settings** der App anzuzeigen (anstelle des standardmäßigen Variablenkonfigurationsbereichs), deklarieren Sie eine Front-Komponente mit [`defineSettingsFrontComponent()`](/l/de/developers/extend/apps/layout/front-components#custom-settings-component) in einer eigenen Datei. Pro App ist nur eine zulässig. Systemverwaltete Bereiche (Auto-Upgrade, App-URL, Verbindungen) bleiben immer sichtbar.
## Variablentypen
Sowohl `applicationVariables` als auch `serverVariables` akzeptieren einen optionalen `type` (und für `SELECT` / `MULTI_SELECT` eine `options`-Liste). Unterstützte Typen: `TEXT` (Standard), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Der `type` beeinflusst nur die **Darstellung und Validierung** – er wählt das passende Eingabefeld in der UI der Arbeitsbereichseinstellungen aus (Schalter, Zahlenfeld, Dropdown, Datumsauswahl, JSON-Editor, …) und ermöglicht es dem Build, deine Konfiguration zu validieren (zum Beispiel müssen `SELECT` / `MULTI_SELECT` nicht-leere `options` deklarieren). Er ändert **nicht**, wie der Wert deinen Code erreicht.
Werte werden **immer als Strings bereitgestellt** – das ist inhärent bei Umgebungsvariablen (`process.env.*` enthält ausschließlich Strings). Wenn deine Logikfunktion ausgeführt wird, serialisiert der Executor jeden Wert anhand seines deklarierten `type` beim Aufbau von `process.env`, sodass das Stringformat konsistent ist, unabhängig davon, wie der Wert gesetzt wurde (Manifest-Standardwert, Einstellungs-UI oder eine vorherige Version):
| Typ | `process.env`-String |
| ------------------------------------- | ------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | der Rohwert (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | Dezimal-String (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | JSON-Array (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | JSON-Objekt (`'{"retries":3}'`) |
Wandle den String zurück in den erwarteten Typ um:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Das Gleiche gilt für Frontend-Komponenten, die Werte über `getApplicationVariable('VARIABLE_NAME')` lesen – der zurückgegebene Wert ist ein String; wandle ihn bei Bedarf um.
## Standard-Funktionsrolle
Die mit [`defineApplicationRole()`](/l/de/developers/extend/apps/config/roles) deklarierte Rolle steuert, worauf die Logikfunktionen und Frontend-Komponenten der App zugreifen können:
* Die zur Laufzeit in Ihre Logikfunktionen injizierten Token werden aus dieser Rolle abgeleitet. Ein Aufruf, der als Person agiert, wird zusätzlich durch die Möglichkeiten dieser Person eingeschränkt und kann daher niemals eine der beiden Berechtigungen überschreiten. Siehe [Wessen Zugriff ein Aufruf verwendet](/l/de/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Der typisierte API-Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt.
* Befolgen Sie das Least-Privilege-Prinzip: Deklarieren Sie nur die Berechtigungen, die Ihre Funktionen benötigen.
Wenn Sie eine neue App erzeugen, erstellt die CLI eine Starter-Rolldatei unter `src/roles/default-role.ts`. Die vollständige Referenz finden Sie unter [Rollen & Berechtigungen](/l/de/developers/extend/apps/config/roles).
## Marktplatz-Metadaten
Wenn Sie planen, [Ihre App zu veröffentlichen](/l/de/developers/extend/apps/operations/publishing), steuern diese optionalen Felder, wie Ihre App im Marktplatz erscheint:
| Feld | Beschreibung |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Name des Autors oder des Unternehmens |
| `category` | App-Kategorie für die Filterung im Marktplatz |
| `logo` | Pfad zu deinem App-Logo in `public/` gebündelt (z.B. `public/logo.png`) |
| `galleryImages` | Array der Galerie-Bild-Pfade in `public/` gebündelt (z.B. `public/screenshot-1.png`) |
| `aboutDescription` | Längere Markdown-Beschreibung für den Tab "Info". Wenn weggelassen, verwendet der Marktplatz die `README.md` des Pakets von npm |
| `websiteUrl` | Link zu Ihrer Website |
| `termsUrl` | Link zu den Nutzungsbedingungen |
| `emailSupport` | Support-E-Mail-Adresse |
| `issueReportUrl` | Link zum Issue-Tracker |
<Note>
`logoUrl` und `screenshots` sind veraltete Aliase von `logo` und `galleryImages`. Externe absolute URLs (`http://` oder `https://`) werden für diese Felder nicht unterstützt: Sie werden beim Build mit einer Warnung gelöscht. Bundle stattdessen die Bilder im `public/` Ordner deiner App.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Installations-Hooks
description: Führen Sie Logik während des Installations-, Upgrade- oder Deinstallations-Lebenszyklus aus – initialisieren Sie Daten, sichern Sie Datensätze, validieren Sie das Upgrade, bereinigen Sie externe Ressourcen.
icon: wrench
---
Installations-Hooks sind spezielle Logikfunktionen, die während des Installations-, Upgrade- oder Deinstallations-Lebenszyklus ausgeführt werden. Sie verwenden dieselbe Handler-Laufzeit wie reguläre [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions), werden jedoch mit eigenen Define-Funktionen deklariert und sind vom normalen Trigger-Modell (HTTP, Cron, Datenbankereignisse) getrennt. Installations-Hooks erhalten ein `InstallPayload` (`{ previousVersion?: string; newVersion: string }` – `previousVersion` ist bei einer Neuinstallation `undefined`); der Deinstallations-Hook erhält ein `UninstallPayload` (`{ version?: string }` – die entfernte Version).
Jede App darf **höchstens einen** Hook jeder Art definieren (Pre-Install, Post-Install, Uninstall). Der Manifest-Build schlägt fehl, wenn mehr als ein Hook eines Typs erkannt wird.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## Auf einen Blick
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Zeitpunkt der Ausführung | Vor der Metadatenmigration — das **bisherige** Schema und die Daten sind noch intakt | Nach der Migration und SDK-Generierung — das **neue** Schema ist aktiv |
| Ausführung | Immer synchron; blockiert die Installation | Standardmäßig asynchron (warteschlangengesteuert, 3 Wiederholungsversuche); synchrones Opt-in über `shouldRunSynchronously: true` |
| Im Fehlerfall | Die Installation wird **abgebrochen**, bevor eine Schemaänderung erfolgt | Asynchron: bis zu 3 Mal erneut ausgeführt. Synchron: Der Aufrufer erhält `POST_INSTALL_ERROR` (Schemaänderungen werden **nicht** zurückgerollt) |
| Typische Verwendung | Daten sichern oder korrigieren, die eine Migration verlieren würde; ein riskantes Upgrade ablehnen, indem ein Fehler geworfen wird | Standarddaten befüllen, den Arbeitsbereich konfigurieren, externe Ressourcen registrieren |
**Faustregel:** Standardmäßig Post-Install verwenden. Greifen Sie nur zu Pre-Install, wenn die Migration selbst destruktiv ist und Sie den vorherigen Zustand abfangen müssen, bevor er verloren geht.
| Sie möchten ... | Verwenden |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Daten befüllen, den Arbeitsbereich konfigurieren, externe Ressourcen registrieren | `post-install` |
| Lange laufende Aufgaben, die die Installationsantwort nicht blockieren sollten | `post-install` (standardmäßig asynchroner Modus, mit Worker-Wiederholungsversuchen) |
| Schnelle Einrichtung, auf die sich der Aufrufer unmittelbar nach der Rückkehr der Installationsantwort verlässt | `post-install` mit `shouldRunSynchronously: true` |
| Daten lesen oder sichern, die bei der bevorstehenden Migration verloren gingen | `pre-install` |
| Ein Upgrade ablehnen, das vorhandene Daten beschädigen würde | `pre-install` (`throw` im Handler) |
| Abgleich bei jedem Upgrade | Einer der Hooks mit `shouldRunOnVersionUpgrade: true` |
## Verhalten, das von beiden Hooks geteilt wird
* Die Konfiguration ist eine `defineLogicFunction`-Konfiguration ohne die Trigger-Einstellungen, aber mit `shouldRunOnVersionUpgrade`.
* **Wann sie ausgeführt werden**: standardmäßig nur bei Neuinstallationen. Setze `shouldRunOnVersionUpgrade: true`, um sie auch bei Upgrades auszuführen. Verwende `previousVersion` / `newVersion`, um je nach Upgrade-Pfad unterschiedlich zu verzweigen.
* **Idempotenz ist wichtig**: Asynchrones Post-Install kann erneut ausgeführt werden, und beide Hooks werden bei Upgrades erneut ausgeführt, wenn `shouldRunOnVersionUpgrade` aktiviert ist.
* Die übliche Logikfunktions-Umgebung (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) wird injiziert, sodass du die Twenty-API mit dem Token deiner App aufrufen kannst.
* Der Hook wird zur Build-Zeit automatisch an das Anwendungsmanifest angehängt (`preInstallLogicFunction` / `postInstallLogicFunction`) — in [`defineApplication()`](/l/de/developers/extend/apps/config/application) muss nichts referenziert werden.
* Der Standardwert für `timeoutSeconds` ist 300, um längere Einrichtungsaufgaben wie Daten-Seeding zu ermöglichen.
* **Wird im Dev-Modus nicht ausgeführt**: `yarn twenty dev` überspringt den Installations-Flow und synchronisiert Dateien direkt, sodass Hooks dort nie ausgeführt werden. Führe sie stattdessen manuell aus:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="Wird ausgeführt, nachdem die Metadatenmigration des Arbeitsbereichs angewendet wurde">
Wird ausgeführt, nachdem deine App die Installation abgeschlossen hat: Metadaten synchronisiert, SDK-Client generiert, neues Schema abfragbar. Beispiel — bei Neuinstallationen einen Standarddatensatz anlegen:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Das Flag `shouldRunSynchronously` steuert das Ausführungsmodell:
* `false` *(Standard)* — in die Nachrichtenwarteschlange eingereiht (`retryLimit: 3`) und von einem Worker ausgeführt. Die Installationsantwort wird zurückgegeben, sobald der Job in die Warteschlange eingereiht wurde. **Für lange laufende Aufgaben verwenden** — das Befüllen großer Datensätze, langsame Drittanbieter-APIs.
* `true` — wird inline während des Installations-Flows ausgeführt. Die Installationsanforderung blockiert, bis der Handler fertig ist; ein geworfener Fehler erscheint als `POST_INSTALL_ERROR` beim Aufrufer (keine Wiederholungsversuche). **Für schnelle Aufgaben verwenden, die unbedingt vor der Antwort abgeschlossen sein müssen.** Die Migration wurde zu diesem Zeitpunkt bereits angewendet, daher werden Schemaänderungen bei einem Fehler nicht zurückgerollt — es wird nur der Fehler nach außen gegeben.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Wird ausgeführt, bevor die Metadatenmigration des Arbeitsbereichs angewendet wird">
Wird vor der Metadatenmigration gegen das **bisherige** Schema ausgeführt — die richtige Stelle, um Daten zu sichern, die eine Migration verlieren würde, oder um ein riskantes Upgrade abzulehnen. Vor der Ausführung führt der Server einen rein additiven „pared-down sync“ durch, der nur die Pre-Install-Funktion der neuen Version registriert; alles andere — Objekte, Felder und Daten der vorherigen Version — bleibt unangetastet, wenn dein Handler ausgeführt wird.
Pre-Install ist immer **synchron** und blockiert die Installation. Wenn der Handler einen Fehler wirft, wird die Installation abgebrochen, bevor eine Schemaänderung erfolgt — der Arbeitsbereich verbleibt in der vorherigen Version in einem konsistenten Zustand. Das ist beabsichtigt: Pre-Install ist Ihre letzte Chance, ein riskantes Upgrade abzulehnen.
Beispiel — die Werte eines Legacy-Feldes kopieren, bevor die Migration es entfernt:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## Deinstallations-Hook
`defineUninstallLogicFunction` deklariert einen Hook, der ausgeführt wird, wenn ein Benutzer Ihre App deinstalliert. Er wird **ausgeführt, bevor** die Metadaten, Daten und der Code der App entfernt werden – sobald die Löschmigration ausgeführt wurde, bleibt nichts mehr zum Ausführen übrig – sodass Ihr Handler weiterhin die Objekte und Datensätze der App abfragen kann. Verwenden Sie ihn zur Bereinigung externer Ressourcen: Stellen Sie API-Ressourcen außer Betrieb, löschen Sie verbleibende Bots, widerrufen Sie Webhooks.
Notizen:
* Der Hook ist Best-Effort: Er wird synchron ausgeführt, aber ein Fehler wird protokolliert und **blockiert die Deinstallation niemals** – die Bereinigung darf es nicht unmöglich machen, eine App zu entfernen.
* Er erhält `UninstallPayload` (`{ version?: string }` – die entfernte Version).
* Er wird **nicht** ausgeführt, wenn eine fehlgeschlagene Neuinstallation zurückgerollt wird – die App wurde nie vollständig installiert.
* Der Hook kann nicht ausgeführt werden, nachdem die App entfernt wurde, daher gehört externe Bereinigung, die von App-Daten abhängt (z. B. in Datensätzen gespeicherte Bot-IDs), hierher und nicht in einen externen geplanten Job.
* Wie die Installations-Hooks wird er **nicht im Dev-Modus ausgeführt** – lösen Sie ihn stattdessen manuell aus:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Übersicht
description: Konfigurieren Sie die App selbst – ihre Identität, Standardberechtigungen und das, was zur Installationszeit ausgeführt wird.
icon: screwdriver-wrench
---
Die **Konfigurationsebene** einer Twenty-App beschreibt die App *für die Plattform* – ihre Identität, die Berechtigungen, die sie hält, und den Code, der während der Installation oder Aktualisierung ausgeführt wird. Diese Deklarationen fügen keine neuen Datentypen oder Laufzeitverhalten hinzu; sie teilen Twenty mit, *wer die App ist* und *wie sie eingerichtet werden soll*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="App-Konfiguration" icon="rocket" href="/l/de/developers/extend/apps/config/application">
`defineApplication` – Identität, Standardrolle, Variablen, Marketplace-Metadaten.
</Card>
<Card title="Rollen & Berechtigungen" icon="shield-halved" href="/l/de/developers/extend/apps/config/roles">
`defineRole` – deklariert, was die Logikfunktionen Ihrer App lesen und schreiben können.
</Card>
<Card title="Installations-Hooks" icon="wrench" href="/l/de/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` und `defineUninstallLogicFunction` – Daten sichern, Standardwerte befüllen, Aktualisierungen validieren, beim Entfernen aufräumen.
</Card>
</CardGroup>
## Wie die Bausteine zusammenhängen
* **Application** ist der Einstiegspunkt. Jede App hat genau einen `defineApplication()`-Aufruf, und dieser verweist auf eine **Rolle** als Standard.
* Die **Rolle** steuert, was die Logikfunktionen und Frontend-Komponenten der App lesen und schreiben können. Folgen Sie dem Prinzip der geringsten Privilegien: Gewähren Sie nur die Berechtigungen, die Ihr Code tatsächlich benötigt.
* **Install Hooks** laufen während der Installation oder Aktualisierung – Pre-Install vor der Metadatenmigration (so kann ein riskantes Upgrade abgelehnt werden), Post-Install nach der Migration (so können Standarddaten gegen das neue Schema befüllt werden). Der Uninstall-Hook wird direkt ausgeführt, bevor die App entfernt wird, sodass er externe Ressourcen bereinigen kann, während die Daten der App noch lesbar sind.
<Note>
Installations-Hooks nutzen die Laufzeit der [Logikfunktion](/l/de/developers/extend/apps/logic/logic-functions) – gleiche Handler-Signatur, gleiche Umgebungsvariablen, gleicher typisierter API-Client –, werden aber mit ihren eigenen Define-Funktionen deklariert und leben außerhalb des regulären Trigger-Modells (HTTP, Cron, Datenbankereignisse).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Öffentliche Assets
description: Liefern Sie statische Dateien — Bilder, Icons, Schriftarten — zusammen mit Ihrer App über den Ordner public/ aus.
icon: folder-open
---
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 \*\* — die Felder `logo` und `galleryImages` in `defineApplication()` Referenzdateien aus diesem Ordner (z. B. `public/logo.png`). Diese werden im Marketplace angezeigt, wenn Ihre App veröffentlicht wird. Externe absolute URLs werden für diese Felder ignoriert — bündeln Sie stattdessen die Bilder in `public/`. Jedes Bild darf 10 MB nicht überschreiten.
* **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 enthalten** — `yarn twenty dev: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:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
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:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
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.

View File

@@ -0,0 +1,206 @@
---
title: Rollen & Berechtigungen
description: Legen Sie fest, welche Objekte und Felder die Logikfunktionen und Frontend-Komponenten Ihrer App lesen und schreiben können.
icon: shield-halved
---
Eine **Rolle** ist ein Berechtigungssatz: welche Objekte eine App lesen oder schreiben kann, welche Felder sie sehen kann und welche plattformbezogenen Funktionen sie nutzen kann. Alle Logikfunktionen und Frontend-Komponenten einer App erben die Berechtigungen der Rolle, die mit `defineApplicationRole()` markiert ist (siehe [Die Standardfunktionsrolle](#the-default-function-role) unten).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
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,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Zeilenbasierte Sicherheit
Objekt- und Feldberechtigungen legen fest, *auf welche Objekte und Felder* eine Rolle zugreifen kann. **Zeilenbasierte
Berechtigungsprädikate** gehen noch weiter und legen fest, *welche Datensätze* eine Rolle sehen und bearbeiten kann — zum
Beispiel eine Self-Service-Rolle, bei der jeder externe Benutzer nur seine eigenen Datensätze sieht.
Deklarieren Sie Prädikate mit `rowLevelPermissionPredicates` für die Rolle. Wie beim Rest des Manifests hat
jedes Prädikat einen eigenen `universalIdentifier` und referenziert ein Objekt und ein Feld über deren
`universalIdentifier`; außerdem besitzt es einen `operand` und (optional) ein workspaceMember-Feld, dessen Wert zur Abfragezeit injiziert wird — sodass Sie ausdrücken können: "Die Eigentümerbeziehung des Datensatzes **ist** das aktuelle workspaceMember":
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Da Prädikate im Manifest ausgeliefert werden, werden sie zusammen mit der Rolle bei jeder Installation und jedem Upgrade erstellt, aktualisiert und entfernt – es ist kein separater Post-Installationsschritt erforderlich, um sie zu synchronisieren.
### Kombinieren von Prädikaten mit Gruppen
Standardmäßig werden die Prädikate einer Rolle mit `AND` verknüpft. Um einige von ihnen mit `OR` zu verknüpfen (oder um Logik zu verschachteln), deklarieren Sie einen Eintrag `rowLevelPermissionPredicateGroups` und verweisen Sie von jedem Prädikat aus über `predicateGroupUniversalIdentifier` darauf. Diese Rolle ermöglicht es einem Partner, eine Opportunity zu sehen, deren er entweder **Eigentümer**
oder der **Ansprechpartner** ist:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Notizen:
* Geben Sie jedem Prädikat und jeder Gruppe einen stabilen `universalIdentifier` (beliebige uuid) – er identifiziert die Entität über Upgrades hinweg eindeutig, und Prädikate verweisen über ihn auf Gruppen.
* Prädikate können auf Objekte und Felder verweisen, die Ihrer App gehören, oder auf Standardobjekte von Twenty.
* Zeilenbasierte Sicherheit wird für Workspaces in Tarifen durchgesetzt, die sie enthalten; die Prädikate werden auch in anderen Tarifen weiterhin synchronisiert, sie werden dort einfach nicht durchgesetzt.
## Die Standard-Funktionsrolle
Wenn Sie eine neue App erzeugen, erstellt die CLI eine Datei für die Standardrolle, die mit `defineApplicationRole()` deklariert ist:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
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: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` ist ein dünner Wrapper um `defineRole()`, der **die** Rolle kennzeichnet, die zum Installationszeitpunkt als Standardrolle Ihrer Anwendung verwendet wird. Die Validierung ist identisch zu `defineRole`, aber die Build-Pipeline verdrahtet deren `universalIdentifier` automatisch in das `defaultRoleUniversalIdentifier` des Anwendungsmanifests – sodass Sie es nicht selbst aus [`defineApplication`](/l/de/developers/extend/apps/config/application) referenzieren müssen.
Notizen:
* Genau **eine** `defineApplicationRole(...)` ist pro App zulässig – der Manifest-Build schlägt fehl, wenn mehr als eine gefunden wird.
* Verwenden Sie `defineRole()` (nicht `defineApplicationRole()`) für alle **zusätzlichen** Rollen, die Ihre App mitliefert.
* Das explizite Setzen von `defaultRoleUniversalIdentifier` in `defineApplication()` wird für die Abwärtskompatibilität weiterhin unterstützt, ist aber zugunsten von `defineApplicationRole()` veraltet.
## Beste Praktiken
* Beginnen Sie mit der vorgegebenen Rolle und schränken Sie sie dann schrittweise ein – standardmäßig wird umfangreicher Lesezugriff gewährt, was selten das ist, was Sie in Produktionsumgebungen möchten.
* Ersetzen Sie `objectPermissions` und `fieldPermissions` durch die genauen Objekte und Felder, die Ihre Funktionen tatsächlich benötigen.
* `permissionFlagUniversalIdentifiers` steuern den Zugriff auf Funktionen auf Plattformebene. Halten Sie sie minimal.
* Ein funktionierendes Beispiel finden Sie unter: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
title: Objekte erweitern
description: Fügen Sie Standard-Twenty-Objekten Felder hinzu (Person, Company, …) oder zu Objekten aus anderen Apps mit `defineField`.
icon: wand-magic-sparkles
---
Verwenden Sie `defineField()`, um einem Objekt, das Ihnen nicht gehört, ein Feld hinzuzufügen – ein Standard-Twenty-Objekt wie Person oder Company oder ein Objekt, das von einer anderen installierten App bereitgestellt wird. Im Gegensatz zu Inline-Feldern, die innerhalb von [`defineObject`](/l/de/developers/extend/apps/data/objects) deklariert werden, benötigen eigenständige Felder einen `objectUniversalIdentifier`, um anzugeben, welches Objekt sie erweitern.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
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 Standard-Twenty-Objekte importieren Sie die Konstante aus `twenty-sdk`:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Wenn Sie Felder **inline innerhalb von `defineObject()`** definieren, benötigen Sie `objectUniversalIdentifier` **nicht** – es wird vom übergeordneten Objekt geerbt.
* `defineField()` ist die einzige Möglichkeit, Felder zu Objekten hinzuzufügen, die Sie nicht mit `defineObject()` erstellt haben.
* Der Speicherort der Datei liegt bei Ihnen. Die Konvention ist `src/fields/\<name>.field.ts`, aber das SDK erkennt Felder überall in `src/`.
* Die `writability` kann auch pro Feld festgelegt werden: `MetadataWritability.APPLICATION` beschränkt Schreibvorgänge auf die eigenen Logikfunktionen deiner App, `MetadataWritability.SYSTEM` ist für plattformverwaltete Daten reserviert. Die Schreibbarkeit eines Felds kann nur strenger sein als die Ebene seines Objekts, und Lesevorgänge werden nicht beeinträchtigt. Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die vollständige Semantik.
* Um eine Registerkarte zu einem Standard-Seitenlayout hinzuzufügen (z. B. der Aufgaben- oder Unternehmensdetailseite), verwenden Sie [`definePageLayoutTab`](/l/de/developers/extend/apps/layout/page-layouts#definepagelayouttab) mit `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` aus `twenty-sdk/define`.
## Hinzufügen einer Relation zu einem bestehenden Objekt
Um ein Relationsfeld hinzuzufügen (z. B. zur Verknüpfung Ihres benutzerdefinierten Objekts mit einer Standard-`Person`), verwenden Sie `defineField()` mit `FieldType.RELATION`. Das Muster ist dasselbe wie bei Inline-Relationen, jedoch mit explizit gesetztem `objectUniversalIdentifier`. Siehe [Relations](/l/de/developers/extend/apps/data/relations) für das bidirektionale Muster.

View File

@@ -0,0 +1,143 @@
---
title: Objekte
description: Deklarieren Sie neue Datensatztypen — benutzerdefinierte Tabellen mit eigenen Feldern — mit defineObject.
icon: table
---
Benutzerdefinierte **Objekte** sind neue Datensatztypen, die Ihre App zu einem Arbeitsbereich hinzufügt – Postkarte, Rechnung, Abonnement, alles, was spezifisch für Ihre Domäne ist. Jedes Objekt deklariert sein Schema (Felder, Relationen, Standardwerte) und einen stabilen universellen Bezeichner, der über Synchronisierungen und Deployments hinweg bestehen bleibt.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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
* 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.
* `openRecordIn` legt fest, wo Datensätze dieses Objekts beim Klicken geöffnet werden: `ObjectOpenRecordIn.USER_CHOICE` (die Standardeinstellung, gemäß der individuellen Präferenz jedes Workspace-Mitglieds in Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` oder `ObjectOpenRecordIn.RECORD_PAGE`. Heften Sie es an `RECORD_PAGE` für Datensätze, die für die Nutzung eine ganze Seite benötigen – wie Workflows und Dashboards –, oder an `SIDE_PANEL` für Datensätze, die nur als schnelles Panel sinnvoll sind, wie Kalendereinträge.
* `writability` steuert, wer überhaupt Datensätze des Objekts schreiben darf, bevor Rollenberechtigungen greifen: `MetadataWritability.OPEN` (der Standard – Workspace-Rollen entscheiden), `MetadataWritability.APPLICATION` (nur die eigenen Logikfunktionen deiner App können Datensätze erstellen, aktualisieren oder löschen; verwende dies für konfigurationsähnliche Objekte, deren Datensätze Verhalten festlegen, damit Workspace-Mitglieder mit weitreichendem Zugriff auf Datensätze sie nicht über die API bearbeiten können) oder `MetadataWritability.SYSTEM` (reserviert für plattformverwaltete Daten). Lesezugriffe sind nicht betroffen – dies wird serverseitig erzwungen, im Gegensatz zu `isUIEditable`, das nur UI-Bedienelemente ausblendet. Es existiert auch auf Feldebene, wo es nur strenger als auf Objektebene sein kann.
* Inline definierte Felder benötigen **kein** `objectUniversalIdentifier` – er wird vom übergeordneten Objekt geerbt. Verwenden Sie [`defineField()`](/l/de/developers/extend/apps/data/extending-objects), um Objekten Felder hinzuzufügen, die Ihnen nicht gehören.
* Sie können mit `yarn twenty dev:add object` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen. Siehe [Architektur → Gerüste für Entitäten](/l/de/developers/extend/apps/getting-started/scaffolding).
<Note>
**Basisfelder werden automatisch hinzugefügt.** Wenn Sie ein benutzerdefiniertes Objekt definieren, erstellt Twenty Standardfelder wie `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` und `deletedAt` für Sie. Sie müssen diese nicht in Ihrem `fields`-Array deklarieren – nur Ihre benutzerdefinierten Felder. Sie können ein Standardfeld überschreiben, indem Sie eines mit demselben Namen deklarieren, aber das ist nur selten eine gute Idee.
</Note>
## Feldtypen
Die vollständige Menge von `FieldType`-Werten, exportiert aus `twenty-sdk/define`:
| Kategorie | Typen |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Text | `TEXT`, `RICH_TEXT`, `ARRAY` (von Zeichenketten), `RAW_JSON` |
| Numerisch | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (beliebige Genauigkeit), `RATING`, `POSITION` |
| Datumsangaben | `DATE`, `DATE_TIME` |
| Auswahl | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Zusammengesetzt | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Bezeichner & Relationen | `UUID`, `RELATION`, `MORPH_RELATION` (siehe [Relationen](/l/de/developers/extend/apps/data/relations)) |
| System | `TS_VECTOR` (Volltext-Suchvektor, vom Server verwaltet) |
Zusammengesetzte Typen speichern mehrere Unterfelder (z. B. `FULL_NAME` = Vorname + Nachname; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` und `MULTI_SELECT` erfordern ein `options`-Array wie im obigen Beispiel.
## Standardwerte
Wörtliche Zeichenfolgen-Standardwerte müssen in einfache Anführungszeichen **innerhalb** der Zeichenfolge eingeschlossen werden — `defaultValue: "'Draft'"`, nicht `defaultValue: "Draft"`. Deshalb verwendet das `status`-Feld oben `` `'${PostCardStatus.DRAFT}'` ``.
Nicht in Anführungszeichen gesetzte Zeichenfolgen sind für berechnete Standardwerte reserviert, die ausgewertet werden, wenn ein Datensatz erstellt wird:
* `'uuid'` — generiert eine UUID (für `UUID`-Felder)
* `'now'` — der aktuelle Zeitstempel (für `DATE_TIME`-Felder)
Dieselbe Konvention gilt für Zeichenketten-Unterfelder zusammengesetzter Standardwerte (z. B. `{ source: "'MANUAL'" }` in einem `ACTOR`-Feld) und für `SELECT`-/`MULTI_SELECT`-Werte. Ein wörtlicher Zeichenfolgen-Standardwert, der nicht in Anführungszeichen gesetzt ist, löst beim Build deiner App eine Warnung aus.
## Nullbarkeit
`isNullable` steuert, ob ein Feld `NULL` akzeptiert. Standardmäßig ist es `true` – lasse es für optionale Felder weg. Setze `isNullable: false`, um ein Feld auf Datenbankebene erforderlich zu machen.
Änderungen an `isNullable` werden bei jedem Sync angewendet, einschließlich Syncs, die ein vorhandenes Feld aktualisieren – so kannst du die Nullbarkeit eines Feldes ändern, indem du das Manifest bearbeitest und erneut synchronisierst.
<Note>
**Das Ändern eines vorhandenen Feldes in nicht nullbar erfordert einen Standardwert.** Wenn du ein Feld auf `isNullable: false` änderst, musst du auch einen `defaultValue` angeben, der nicht `NULL` ist. Der Standardwert füllt alle vorhandenen `NULL`-Zeilen auf, bevor die `NOT NULL`-Einschränkung angewendet wird; ohne ihn schlägt die Synchronisierung mit `Default value cannot be null for non-nullable fields` fehl. Relationsfelder und `TS_VECTOR`-Felder sind immer nullbar, daher hat `isNullable` keine Auswirkung auf sie.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Was kommt als Nächstes
* **Verbinden Sie dieses Objekt mit anderen** – siehe [Relationen](/l/de/developers/extend/apps/data/relations) für das bidirektionale Relationsmuster.
* **Fügen Sie Objekten aus anderen Apps Felder hinzu** – siehe [Objekte erweitern](/l/de/developers/extend/apps/data/extending-objects) für `defineField()`.
* **Dieses Objekt in der UI anzeigen** — siehe [Navigationsmenüeinträge](/l/de/developers/extend/apps/layout/navigation-menu-items), um einen Eintrag in der Seitenleiste hinzuzufügen; siehe [Ansichten](/l/de/developers/extend/apps/layout/views), um benutzerdefinierte Listenkonfigurationen hinzuzufügen.

View File

@@ -0,0 +1,101 @@
---
title: Übersicht
description: Gestalten Sie die Daten, die Ihre App zu einem Workspace hinzufügt – Objekte, Felder und Beziehungen.
icon: database
---
Die **Datenebene** einer Twenty-App umfasst die Daten, die Ihre App zu einem Workspace *hinzufügt* – die neuen Datensatztypen, die sie deklariert, die Spalten, die sie zu bestehenden Objekten hinzufügt, und wie diese Datensätze miteinander verknüpft sind.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="Objekte" icon="table" href="/l/de/developers/extend/apps/data/objects">
`defineObject` – deklarieren Sie neue Datensatztypen mit eigenen Feldern.
</Card>
<Card title="Objekte erweitern" icon="wand-magic-sparkles" href="/l/de/developers/extend/apps/data/extending-objects">
`defineField` – fügen Sie Standardobjekten oder Objekten anderer Apps Felder hinzu.
</Card>
<Card title="Beziehungen" icon="diagram-project" href="/l/de/developers/extend/apps/data/relations">
Bidirektionale `MANY_TO_ONE`- / `ONE_TO_MANY`-Verbindungen zwischen Objekten.
</Card>
<Card title="Zeitachsen-Aktivitätstypen" icon="clock-rotate-left" href="/l/de/developers/extend/apps/data/timeline-activity-types">
Definieren Sie automatische Audit-Ereignisse und explizite App-Ereignisse für Datensatz-Zeitachsen.
</Card>
</CardGroup>
## Entitäten im Überblick
| Entität | Zweck | Definiert mit |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Objekt** | Ein neuer benutzerdefinierter Datensatztyp (z. B. PostCard, Invoice) mit eigenen Feldern | `defineObject()` |
| **Feld** | Eine Spalte in einem Objekt. Eigenständige Felder können Objekte erweitern, die Sie nicht erstellt haben (z. B. `loyaltyTier` zu Company hinzufügen) | `defineField()` |
| **Beziehung** | Eine bidirektionale Verknüpfung zwischen zwei Objekten – beide Seiten werden als Felder deklariert | `defineField()` mit `FieldType.RELATION` |
| **Indizes** | Ein Datenbankindex, um eine wiederkehrende Abfrage für eines Ihrer Objekte zu beschleunigen | `defineIndex()` |
| **Zeitachsen-Aktivitätstyp** | Ein automatisches Audit-Ereignis oder explizites Domänenereignis, das in Datensatz-Zeitleisten angezeigt wird. | `defineTimelineActivityType()` |
Das SDK erkennt diese zur Build-Zeit über eine AST-Analyse, sodass die Dateiorganisation Ihnen überlassen ist – die Konvention ist `src/objects/`, `src/fields/`, `src/indexes/` und `src/timeline-activity-types/`. Stabile `universalIdentifier`-UUIDs verknüpfen alles über Deploys hinweg.
## Indizes (optional)
Apps können Indizes gemeinsam mit ihren Objekten ausliefern, um wiederkehrende Abfragen schnell zu halten. Der häufigste Fall ist eine Status- oder Fremdschlüsselspalte, die Sie häufig lesen.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Eindeutige Indizes
`defineIndex` akzeptiert `isUnique: true` sowohl für Einspalten- als auch Mehrspalteneindeutigkeit. Dies ist das empfohlene Primitive – `defineField({ isUnique: true })` ist veraltet und wird in einer zukünftigen Version entfernt.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Andere Einschränkungen
* Partielle `WHERE`-Klauseln bleiben unter Kontrolle der Administratoren – Apps können sie nicht deklarieren.
* Jedes Objekt ist auf 10 benutzerdefinierte Indizes begrenzt (die Indizes des Frameworks selbst werden nicht mitgezählt).
Ordnen Sie das `fields`-Array so an, wie Postgres es verwenden soll – die ganz linke Spalte zuerst, wie in einem Telefonbuch. Indizes sind nicht kostenlos: Jeder Schreibvorgang in die Tabelle aktualisiert sie. Fügen Sie einen nur dann hinzu, wenn Sie eine Abfrage haben, die ihn benötigt.
<Note>
Suchen Sie nach **Application Config** oder **Roles & Permissions**? Diese beschreiben die App selbst und nicht die Daten, die sie hinzufügt – sie befinden sich unter [Config](/l/de/developers/extend/apps/config/overview). Suchen Sie nach **Connections** (Linear, GitHub, Slack OAuth)? Diese existieren, um *von* Logikfunktionen aufgerufen zu werden, und befinden sich unter [Logic](/l/de/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Beziehungen
description: Objekte mit bidirektionalen MANY_TO_ONE-/ONE_TO_MANY-Relationen verbinden.
icon: diagram-project
---
Relationen verbinden zwei Objekte miteinander. In Twenty sind Relationen stets **bidirektional** — jede Relation hat zwei Seiten, und jede Seite wird als Feld deklariert, das auf die andere verweist.
| Beziehungstyp | Beschreibung | Fremdschlüssel vorhanden? |
| ------------- | ----------------------------------------------------------------------- | ------------------------- |
| `MANY_TO_ONE` | Viele Datensätze dieses Objekts verweisen auf einen Datensatz des Ziels | Ja (`joinColumnName`) |
| `ONE_TO_MANY` | Ein Datensatz dieses Objekts hat viele Datensätze des Ziels | Nein (die 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
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):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
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):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
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',
},
});
```
<Note>
**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. Das Build-System löst dies zur Kompilierzeit auf.
</Note>
## Verknüpfungsbeziehungen (Junction)
Ein Verknüpfungsobjekt (Junction) verbindet einen Quelldatensatz mit einem oder mehreren Zieldatensätzen. Beispielsweise kann `PostCardRecipient` sowohl eine `postCard`-Beziehung als auch eine `recipient`-Beziehung enthalten. Legen Sie im `ONE_TO_MANY`-Feld des Quellobjekts `junctionTargetFieldUniversalIdentifier` auf das Beziehungsfeld des Verknüpfungsobjekts fest, das auf das Ziel verweist:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` muss eine `MANY_TO_ONE`- oder morphische Beziehung auf dem Verknüpfungsobjekt identifizieren. Damit wird Twenty mitgeteilt, welcher verknüpfte Datensatz das Ziel ist, wenn eine Funktion wie die [Auffächerung von Timeline-Aktivitäten](/l/de/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) die Verknüpfung durchläuft.
## Relationen zu Standardobjekten
Um eine Relation mit einem integrierten Twenty-Objekt (Person, Company usw.) zu erstellen, verwenden Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
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
| Eigenschaft | Erforderlich | Beschreibung |
| ---------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `type` | Ja | Muss `FieldType.RELATION` sein |
| `relationTargetObjectMetadataUniversalIdentifier` | Ja | Der `universalIdentifier` des Zielobjekts |
| `relationTargetFieldMetadataUniversalIdentifier` | Ja | Der `universalIdentifier` des entsprechenden Felds auf dem Zielobjekt |
| `universalSettings.relationType` | Ja | `RelationType.MANY_TO_ONE` oder `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Nur für MANY_TO_ONE | Was passiert, wenn der referenzierte Datensatz gelöscht wird: `CASCADE`, `SET_NULL`, `RESTRICT` oder `NO_ACTION` |
| `universalSettings.joinColumnName` | Nur für MANY_TO_ONE | Datenbankspaltenname für den Fremdschlüssel (z. B. `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Nur Junction-`ONE_TO_MANY` | Beziehungsfeld auf dem Verknüpfungsobjekt, das auf das Auffächerungsziel verweist |
## Inline-Relationsfelder
Sie können eine Relation auch direkt innerhalb von [`defineObject`](/l/de/developers/extend/apps/data/objects) deklarieren. Wenn inline, lassen Sie `objectUniversalIdentifier` weg — er wird vom übergeordneten Objekt geerbt:
```ts
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
],
});
```

View File

@@ -0,0 +1,205 @@
---
title: Systemmetadaten ansprechen
description: Lösen Sie die deterministischen universellen Bezeichner der Metadaten auf, die Twenty für jedes Objekt automatisch bereitstellt, damit Ihre App ohne Hardcoding darauf verweisen kann.
icon: gears
---
Jedes Objekt in Twenty verfügt über **Systemmetadaten**, die Sie selbst nie deklarieren, wie etwa eine Reihe von Feldern und eine Hauptlistenansicht mit ihren Spalten. Der Server erstellt all dies, wenn das Objekt bereitgestellt wird, und der Umfang wächst mit Twenty.
Da Sie sie nicht deklarieren, gibt es keine `universalIdentifier`-Konstante, die Sie importieren könnten. Stattdessen **leitet** der Server jeden Bezeichner deterministisch ab, und `twenty-sdk` stellt dieselbe Ableitung zur Verfügung, sodass Ihr Manifest den genauen Wert auflösen kann, den der Server verwendet.
## Systemfelder
Die skalaren Felder, die bei jedem Objekt vorhanden sind und die Sie nicht mit [`defineField()`](/l/de/developers/extend/apps/data/extending-objects) deklarieren:
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Wie referenzieren Sie also `createdAt` als Spalte in einer [Ansicht](/l/de/developers/extend/apps/layout/views)?
### Das Problem
Seit Twenty 2.19 wird die universelle Kennung eines Systemfelds **deterministisch abgeleitet** vom Server aus drei Eingaben: der universellen Kennung der Applikation, der universellen Kennung des Objekts und dem Feldnamen. Sich eine ID auszudenken und sie hart zu codieren, funktioniert nicht: Sie stimmt mit nichts auf dem Server überein, und die Synchronisierung lehnt die lose Referenz ab:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### Die Lösung
<Note>
`getFieldUniversalIdentifier` ist ab `twenty-sdk` 2.21 verfügbar.
</Note>
Verwenden Sie `getFieldUniversalIdentifier`, um genau denselben Wert aufzulösen, den der Server verwendet. Die Funktion nimmt die drei Eingaben entgegen und gibt die universelle Kennung des Feldes zurück:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` ist die Kennung Ihrer App, diejenige, die Sie an [`defineApplication()`](/l/de/developers/extend/apps/config/application) übergeben.
* `objectUniversalIdentifier` ist die Kennung des Objekts, zu dem das Feld gehört.
* `name` ist der Name des Systemfelds, einer der oben aufgeführten Werte.
### Beispiel: eine createdAt-Spalte in einer Ansicht
Der typische Anwendungsfall ist das Hinzufügen einer `createdAt`-Spalte zu einer Ansicht eines Ihrer benutzerdefinierten Objekte. Lösen Sie die Feld-ID auf und referenzieren Sie sie wie jeden anderen `fieldMetadataUniversalIdentifier`:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Dieselbe aufgelöste ID funktioniert überall dort, wo ein `fieldMetadataUniversalIdentifier` erwartet wird: Ansichtsfelder, Filter, Sortierungen, Gruppierungen und Page-Layout-Widgets.
<Note>
Lösen Sie die ID auf, statt sie hart zu codieren. Da der Server den Wert aus der Applikations-ID, der Objekt-ID und dem Feldnamen ableitet, hält ein Aufruf von
`getFieldUniversalIdentifier` Ihre Referenz korrekt, selbst wenn sich diese
Eingaben ändern, und verhindert Abweichungen, falls sich die Ableitung jemals weiterentwickelt.
</Note>
### System-Relationsfelder
<Note>
`getSystemRelationFieldUniversalIdentifier` ist ab Version 2.23 von `twenty-sdk` verfügbar und erfordert einen Twenty-Server in Version 2.23 oder höher.
</Note>
Zusätzlich zu den oben genannten skalaren Systemfeldern stellt der Server für jedes Objekt vier **System-Relationsfelder** bereit: `timelineActivities`, `attachments`, `noteTargets` und `taskTargets`, die jeweils auf das passende Standard-Relationsobjekt verweisen.
Diese Felder werden nicht mit `getFieldUniversalIdentifier` aufgelöst: Ihre Kennung wird **namenunabhängig** abgeleitet, aus dem Objekt, das das Feld enthält, und dem Objekt, auf das das Feld verweist. Auf diese Weise ändert das Umbenennen eines Objekts niemals die Kennungen seiner Relationsfelder.
Verwende `getSystemRelationFieldUniversalIdentifier`, um sie aufzulösen:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` ist das Objekt, das das Feld **enthält**.
* `relationTargetObjectUniversalIdentifier` ist das Objekt, auf das das Feld **zeigt**.
Die Richtung wird durch die Reihenfolge der Argumente codiert. Um die umgekehrte Seite aufzulösen (z. B. `attachment.targetRocket`, das Morph-Feld, das der Server auf dem Standard-Relationsobjekt erstellt), vertausche die beiden:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Wie bei skalaren Systemfeldern funktioniert die aufgelöste ID überall dort, wo ein `fieldMetadataUniversalIdentifier` erwartet wird.
## Systemansichten
<Note>
`getSystemViewUniversalIdentifier` und `getSystemViewFieldUniversalIdentifier` sind ab Version 2.26 von `twenty-sdk` verfügbar und erfordern einen Twenty-Server in Version 2.26 oder höher.
</Note>
Der Server stellt außerdem für jedes Objekt eine **Systemansicht** bereit: die Hauptlistenansicht (`All {objectLabelPlural}`, mit dem Schlüssel `ViewKey.INDEX` (abgeleitet mit `SYSTEM_VIEW_KEYS.INDEX`)), mit einer Spalte pro anzeigbarem Feld. Wie bei Systembeziehungsfeldern werden ihre Bezeichner **namensfrei** abgeleitet, sodass das Umbenennen eines Objekts oder eines Feldes sie nie ändert.
Verwenden Sie `getSystemViewUniversalIdentifier`, um die Ansicht aufzulösen:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` ist die Anwendung, der das **Objekt** gehört; sie bestimmt den Namensraum der Ansicht.
* `objectUniversalIdentifier` ist das Objekt, das die Ansicht auflistet.
* `viewKey` ist der Unterscheidungsschlüssel für Systemansichten: `SYSTEM_VIEW_KEYS.INDEX` für die Hauptlistenansicht, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` für die Felder-Widget-Ansicht auf der Datensatzseite. Er dient als Schlüssel für die Ableitung; nur `INDEX` wird auch in der Ansichtszeile gespeichert.
Die aufgelöste ID funktioniert überall dort, wo ein `viewUniversalIdentifier` erwartet wird, etwa bei einem [`NavigationMenuItemType.VIEW`](/l/de/developers/extend/apps/layout/navigation-menu-items)-Eintrag in der Seitenleiste. Um einfach die Hauptliste eines Objekts zu öffnen, sollten Sie `NavigationMenuItemType.OBJECT` mit `targetObjectUniversalIdentifier` bevorzugen: Es benötigt keine Ableitung.
`getSystemViewFieldUniversalIdentifier` löst eine einzelne **Spalte** in einer Systemansicht auf, basierend auf der Ansicht und dem Feld, das sie anzeigt:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Beachten Sie das erste Argument: Eine Spalte ist dem Namensraum der Anwendung zugeordnet, die das **Feld, das sie anzeigt**, besitzt – nicht der Anwendung, der die Ansicht gehört. Ein Feld, das Ihre App zu einem Standardobjekt hinzufügt, erhält seine Spalte im Namensraum Ihrer Anwendung – auf einer Ansicht, die Twenty gehört.
<Warning>
Systemansichten und ihre Spalten sind im Besitz des Servers: Lösen Sie ihre Bezeichner auf, um auf sie zu verweisen, niemals, um sie zu deklarieren. `key` in [`defineView()`](/l/de/developers/extend/apps/layout/views) ist veraltet und wird ignoriert, sodass eine Manifestansicht niemals den `INDEX`-Schlüssel beanspruchen kann, und der Server stellt bereits eine Spalte für jedes Feld bereit, das Sie hinzufügen, sodass das Deklarieren eines eigenen `defineViewField()` für dasselbe Feld in einer Systemansicht damit in Konflikt steht.
</Warning>
## Standardobjekte von Twenty
Für ein **Standard**-Twenty-Objekt (Person, Company, Opportunity, …) müssen Sie nichts ableiten: Die Bezeichner für Felder und Ansichten sind vorab berechnete Konstanten, die Sie direkt importieren können.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Greifen Sie auf die obigen Helfer zurück, wenn es sich um ein Objekt handelt, das **Ihre App** mit [`defineObject()`](/l/de/developers/extend/apps/data/objects) definiert und für das es keine solche Konstante gibt.
<Note>
`name` ist ein **Standard**-Feld, kein Systemfeld. Es behält seine eigene hart codierte
universelle Kennung und wird nicht über
`getFieldUniversalIdentifier` aufgelöst. Für Objekte, die Sie definieren, referenzieren Sie das
`name`-Feld über die Kennung, die Sie ihm in `defineObject()` gegeben haben.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Aktivitätstypen der Zeitachse
description: Definieren Sie automatische Audit-Ereignisse und explizite App-Ereignisse, die in Datensatz-Zeitachsen dargestellt werden.
icon: clock-rotate-left
---
Ein Zeitachsen-Aktivitätstyp definiert das stabile Vokabular für ein Ereignis, das in der Zeitachse eines Datensatzes angezeigt wird. Standardobjekte und App-Objekte verwenden denselben Vertrag: Ein Typ hat eine Bezeichnung und ein Symbol, kann optional deklarieren, wann er ausgegeben wird, und kann optional über eine der [Front-Komponenten](/l/de/developers/extend/apps/layout/front-components) Ihrer App gerendert werden.
<Note>
Zeitachsen-Aktivitätstypen befinden sich in der Beta-Phase und werden bald in Twenty 2.34 verfügbar sein. Die
API kann sich weiterentwickeln, während wir aus den Anwendungsfällen von App-Entwicklern lernen.
</Note>
Erstellen Sie einen mit dem Scaffolder:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Oder definieren Sie ihn direkt:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Automatische und explizite Ereignisse
Fügen Sie `emit` hinzu, wenn Twenty diesen Typ automatisch erstellen soll. `emit.on` unterstützt `created`, `updated`, `deleted`, `restored`, `linked` und `unlinked`, während `emit.objectUniversalIdentifier` das Quellobjekt identifiziert. In einem Workspace kann nur ein effektiver Typ denselben Emit-Schlüssel verarbeiten — `on`, Objekt und optionale Through-Beziehung.
Ohne `emit.through` wird das Ereignis auf der eigenen Zeitachse des Quelldatensatzes geschrieben. Um es auf verwandte Datensätze zu fächern, setzen Sie `emit.through. elationFieldUniversalIdentifier` zu einer direkten Beziehung oder einer [one-to-many junction relation](/l/de/developers/extend/apps/data/relations#junction-relations) zum Quellobjekt. Morphe Relations, die an jedes Mitglied ihrer Morph-Gruppe angehängt werden, so dass eine einzige Deklaration mehrere Objekttypen ansprechen kann.
Für eine direkte Beziehung erzeugt das Erstellen oder Wiederherstellen des Quelldatensatzes `linked`, indem es `unlinked` erzeugt, und zeigt die Beziehung erneut an, erzeugt `unlinked` auf das vorherige Ziel plus `linked` auf das neue Ziel. Andere Quell-Aktualisierungen erzeugen keine Link-Ereignisse. Dies ist der Vertrag, der von Anhängen verwendet wird, deren Ziel eine direkte Verwandlung ist.
Für eine Kreuzungsbeziehung identifiziert `universalSettings.junctionTargetFieldUniversalIdentifier` die Beziehung vom Kreuzungsobjekt zum Ziel. Erstellen oder Löschen einer Verbindungszeile erzeugt `linked` oder `unlinked`. Wenn Sie beide Seiten einer Verbindungszeile neu anzeigen, erzeugt dies auch ein Link-Ereignis. Aktualisierungen von nicht zusammenhängenden Knotenfeldern nicht.
`linked` und `unlinked` Ereignisse erfordern `emit.through`, da ihr Trigger eine Änderung der konfigurierten direkten oder Kreuzungsbeziehung ist.
Dieses Beispiel verwendet denselben generischen Vertrag wie Notizen, Aufgaben, Nachrichten und Kalenderereignisse:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Standardmäßig wird für jede Aktualisierung der Quelle ein `updated`-Through-Ereignis ausgegeben. Setzen Sie `emit.through.triggerFieldUniversalIdentifiers`, wenn nur Änderungen an ausgewählten Quellfeldern auf den Zielzeitachsen erscheinen sollen.
Lassen Sie `emit` für ein explizites Domänenereignis weg, das Ihre Logikfunktion selbst erstellt. Dadurch wird vermieden, dass sowohl eine automatische Audit-Zeile als auch eine explizite Zeile für denselben Vorgang erstellt wird.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Erstellen Sie explizite Ereignisse mit [`createTimelineActivity()`](/l/de/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). App-Code verwendet stabile universelle Bezeichner; Twenty löst die installationsspezifischen Metadaten-IDs auf.
## Benutzerdefiniertes Rendering
Ohne eine Front-Komponente rendert Twenty eine native generische Zeile anhand der Typbezeichnung, des Symbols und der Metadaten verknüpfter Objekte. Dies funktioniert für Standard- und benutzerdefinierte Objekte ohne objektspezifischen Renderer.
Für benutzerdefinierte Details setzen Sie `frontComponentUniversalIdentifier` auf eine Front-Komponente, die derselben App gehört:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Die native Zeile bleibt die eingeklappte Darstellung. Twenty bindet die Front-Komponente erst ein, nachdem der Benutzer diese Zeile aufklappt, und vermeidet so eine Sandbox und einen Worker für jedes sichtbare Ereignis. Rufen Sie innerhalb der Komponente `useTimelineActivityId()` aus `twenty-sdk/front-component` auf, um die Zeilen-ID zu lesen und alle Daten abzurufen, die Ihre Darstellung benötigt. Sie gibt `null` zurück, wenn die Komponente außerhalb einer Zeitachsen-Zeile gerendert wird.
## Darstellung einer anderen Anwendung überschreiben
Eine App besitzt automatische Zeitachsen-Verträge für ihre eigenen Objekte. Um ein Ereignis für ein Objekt anzupassen, das einer anderen App gehört, deklarieren Sie den vorhandenen Typ explizit mit `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Eine Überschreibung muss `emit` enthalten, da sie einen vorhandenen automatischen Emit-Slot und keinen rein expliziten Typ ersetzt. Der referenzierte Typ muss zur Anwendung des Zielobjekts gehören und dieselbe Aktion und Route beschreiben. Das Entfernen der überschreibenden App stellt den Basistyp wieder her; das Entfernen des Basistyps deaktiviert die Überschreibung.
## Workspace-Überschreibungen und Stummschaltung
Workspace-Administratoren können die Darstellung eines Typs ändern oder seine automatischen und expliziten Ereignisse stummschalten, ohne die Anwendung abzuspalten. Verwenden Sie die Mutation `updateTimelineActivityType` der Metadaten-API mit der installationsspezifischen Typ-ID:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` und `icon` werden als Workspace-Überschreibungen gespeichert, sodass spätere Anwendungsupdates die Auswahl des Administrators nicht überschreiben. Setzen Sie `isActive` auf `false`, um die automatische Ausgabe zu stoppen und neue explizite Ereignisse dieses Typs abzulehnen. Vorhandene Zeilen bleiben sichtbar.
Stellen Sie die Standardwerte der Anwendung, einschließlich des aktiven Zustands, wieder her mit:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| ------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile UUID für den Typ über Installationen und Upgrades hinweg |
| `name` | Ja | App-lokaler programmatischer Name; muss innerhalb der App eindeutig sein |
| `label` | Ja | Benutzerseitiger Aktionstext, der vom nativen Renderer verwendet wird |
| `icon` | Nein | Neben dem Ereignis angezeigter Twenty-Symbolname |
| `emit` | Nein | Deklaration der automatischen Ausgabe; für rein explizite Typen weglassen |
| `emit.on` | Mit emit | Audit-Aktion, die bewirkt, dass dieser Typ geschrieben wird |
| `emit.objectUniversalIdentifier` | Mit emit | Objekt, dessen Datensätze diesen Typ ausgeben |
| `emit.through.relationFieldUniversalIdentifier` | Nein | Direkte oder Anschlussbeziehung, die zum Fächer verwendet wird, um zugehörige Timelines zu nutzen |
| `emit.through.triggerFieldUniversalIdentifiers` | Nein | Quellfelder, die ein `updated`-Through-Ereignis auslösen können |
| `frontComponentUniversalIdentifier` | Nein | App-eigene Frontkomponente eingehängt, wenn die Zeile erweitert wird |
| `replacesTimelineActivityTypeUniversalIdentifier` | Nein | Existierter Emit-Typ ersetzt; erfordert `emit` |
Zwanzig Schnappschüsse semantische Identität und dauerhafte Präsentation fallen zurück, wenn ein Ereignis erstellt wird. Historische Zeilen behalten ihre ursprüngliche Aktion und Objektbedeutung. Während der Installation des Typs wird die aktuelle übersetzte Bezeichnung, das Symbol und die vordere Komponente live dargestellt; nach der Deinstallation hält der Snapshot-Fallback den Datensatz lesbar. Die Auflösung verwendet den universellen Identifikator, so dass die Präsentation auch die Neuinstallation der App überlebt.

View File

@@ -0,0 +1,102 @@
---
title: Konzepte
description: Wie Twenty-Apps funktionieren – Entitätenmodell, Sandboxing und Installationslebenszyklus.
icon: sitemap
---
Twenty-Apps sind TypeScript-Pakete, die Ihren Arbeitsbereich mit benutzerdefinierten Objekten, Logik, UI-Komponenten und KI-Funktionen erweitern. Sie laufen auf der Twenty-Plattform mit vollständigem Sandboxing und Berechtigungsverwaltung.
## Wie Apps funktionieren
Eine App ist eine Sammlung von **Entitäten**, die mithilfe von `defineEntity()`-Funktionen aus dem Paket `twenty-sdk` deklariert werden. Das SDK erkennt diese Deklarationen zur Build-Zeit per AST-Analyse und erzeugt ein **Manifest** — eine vollständige Beschreibung dessen, was Ihre App zu einem Arbeitsbereich hinzufügt. Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**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. Die obige Ordnerstruktur ist eine Konvention, keine Anforderung.
</Note>
## Entitätstypen
| Entität | Zweck | Dokumentation |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Anwendung** | App-Identität, Standardrolle, Variablen | [Anwendungskonfiguration](/l/de/developers/extend/apps/config/application) |
| **Rolle** | Berechtigungssätze für Objekte und Felder | [Rollen & Berechtigungen](/l/de/developers/extend/apps/config/roles) |
| **Objekt** | Benutzerdefinierte Datensatztypen mit Feldern | [Objekte](/l/de/developers/extend/apps/data/objects) |
| **Feld** | Felder zu Objekten aus anderen Apps hinzufügen | [Objekte erweitern](/l/de/developers/extend/apps/data/extending-objects) |
| **Beziehung** | Bidirektionale Verknüpfungen zwischen Objekten | [Beziehungen](/l/de/developers/extend/apps/data/relations) |
| **Logikfunktion** | Serverseitiges TypeScript mit Triggern | [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) |
| **Skill** | Wiederverwendbare Anweisungen für KI-Agenten | [Skills & Agenten](/l/de/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | KI-Assistenten mit benutzerdefinierten Prompts | [Skills & Agenten](/l/de/developers/extend/apps/logic/skills-and-agents) |
| **Verbindungsanbieter** | OAuth-Zugangsdaten für Drittanbieter-APIs | [Verbindungen](/l/de/developers/extend/apps/logic/connections) |
| **Ansicht** | Vorkonfigurierte Listenansichten für Datensätze | [Ansichten](/l/de/developers/extend/apps/layout/views) |
| **Navigationsmenüeintrag** | Benutzerdefinierte Seitenleisten-Einträge | [Navigationsmenüeinträge](/l/de/developers/extend/apps/layout/navigation-menu-items) |
| **Seitenlayout** | Tabs und Widgets auf der Detailseite eines Datensatzes | [Seiten-Layouts](/l/de/developers/extend/apps/layout/page-layouts) |
| **Frontend-Komponente** | Isolierte React-UI innerhalb von Twenty | [Frontend-Komponenten](/l/de/developers/extend/apps/layout/front-components) |
| **Gemeinsame Abhängigkeiten** | Von den Frontend-Komponenten der App gemeinsam genutzte Abhängigkeiten, die in package.json deklariert sind | [Gemeinsame Abhängigkeiten](/l/de/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **Befehlsmenü-Eintrag** | Schnellaktionen und Cmd+K-Einträge | [Befehlsmenü-Einträge](/l/de/developers/extend/apps/layout/command-menu-items) |
## Sandboxing
* **Logikfunktionen** laufen in isolierten Node.js-Prozessen auf dem Server. Sie greifen nur über den typisierten API-Client auf Daten zu, der entweder als die Person fungiert, die die Ausführung ausgelöst hat, oder als die App selbst. Siehe [Wessen Zugriff ein Aufruf verwendet](/l/de/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **Frontend-Komponenten** laufen in Web Workers mit Remote DOM — von der Hauptseite isoliert, rendern aber native DOM-Elemente (keine iframes). Sie kommunizieren über eine Message-Passing-Host-API mit Twenty.
* **Berechtigungen** werden auf API-Ebene durchgesetzt. Zwei Laufzeit-Token werden eingefügt, die beide von der mit `defineApplicationRole()` deklarierten Rolle abgeleitet sind; das Standard-Token wird weiter auf das eingeschränkt, was die auslösende Person tun kann, wenn jemand hinter der Ausführung steht.
## App-Lebenszyklus
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — überwacht Ihre Quelldateien und synchronisiert Änderungen in Echtzeit mit einem verbundenen Twenty-Server. Der typisierte API-Client wird automatisch neu erzeugt, wenn sich das Schema ändert.
* **`yarn twenty dev:build`** — kompiliert TypeScript, bündelt Logikfunktionen und Frontend-Komponenten mit esbuild und erzeugt ein Manifest.
* **Pre/Post-Install-Hooks und Uninstall-Hooks** — optionale Funktionen, die während der Installation oder direkt vor der Deinstallation ausgeführt werden. Details finden Sie unter [Install Hooks](/l/de/developers/extend/apps/config/install-hooks).
## Nächste Schritte
<CardGroup cols={2}>
<Card title="Konfiguration" icon="screwdriver-wrench" href="/l/de/developers/extend/apps/config/overview">
App-Identität, Standardrolle sowie Install- und Uninstall-Hooks.
</Card>
<Card title="Daten" icon="database" href="/l/de/developers/extend/apps/data/overview">
Objekte, Felder und bidirektionale Relationen.
</Card>
<Card title="Logik" icon="bolt" href="/l/de/developers/extend/apps/logic/overview">
Logikfunktionen, Skills, Agenten und OAuth-Verbindungen.
</Card>
<Card title="Layout" icon="table-columns" href="/l/de/developers/extend/apps/layout/overview">
Ansichten, Navigation, Seiten-Layouts, Frontend-Komponenten.
</Card>
<Card title="Operationen" icon="rocket" href="/l/de/developers/extend/apps/operations/overview">
CLI, Tests, Remotes, CI und das Veröffentlichen Ihrer App.
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: Lokaler Server
description: Den lokalen Twenty Docker-Server verwalten – starten, stoppen, aktualisieren, parallele Testinstanz und manuelle SDK-Einrichtung.
icon: server
---
## Lokalen Server verwalten
Verwenden Sie `yarn twenty docker:*`, um den lokalen Twenty-Container zu steuern:
| Befehl | Was es tut |
| -------------------------------------- | --------------------------------------------------- |
| `yarn twenty docker:start` | Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty docker:start 2.2.0` | Eine bestimmte Serverversion starten |
| `yarn twenty docker:start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty docker:stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty docker:status` | URL, Version und Anmeldedaten anzeigen |
| `yarn twenty docker:logs` | Serverprotokolle streamen |
| `yarn twenty docker:reset` | Alle Daten löschen und neu starten |
| `yarn twenty docker:upgrade` | Das neueste `twenty-app-dev`-Image herunterladen |
| `yarn twenty docker:upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen.
## Serverversion fixieren
Wenn keine Version übergeben wird, ermittelt `docker:start` die Version aus dem `engines.twenty`-Bereich deiner App in der `package.json` – derselbe Bereich, gegen den der Server beim Installieren deiner App validiert. Es startet das neueste veröffentlichte `twenty-app-dev`-Image, das den Bereich erfüllt, und greift auf `latest` zurück, wenn das Feld fehlt oder keine veröffentlichte Version übereinstimmt:
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Gib eine Version explizit an, um den Bereich für einen einzelnen Lauf zu überschreiben: `yarn twenty docker:start 2.3.0`. Wenn bereits ein Container mit einer anderen Version existiert, aktualisiert `docker:start` ihn direkt (der Container wird neu erstellt, während deine Daten-Volumes erhalten bleiben).
## Aktualisieren des Server-Images
`yarn twenty docker:upgrade` lädt das neueste Image herunter, vergleicht die Digests und erstellt den Container nur neu, wenn sich tatsächlich etwas geändert hat. Die Volumes bleiben erhalten — nur der Container wird ersetzt. Wenn ein neues Image heruntergeladen wurde und der Container lief, startet das Upgrade automatisch einen neuen Container; führen Sie anschließend `yarn twenty docker:start` aus, um zu warten, bis er betriebsbereit ist.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Überprüfen Sie die laufende Version mit `yarn twenty docker:status` (dies zeigt die im Container enthaltene `APP_VERSION` an).
## Eine parallele Testinstanz ausführen
Übergeben Sie `--test` an jeden `docker:*`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich für Integrationstests oder Experimente, ohne Ihre Hauptentwicklungsdaten anzutasten:
| Befehl | Was es tut |
| ----------------------------------- | ------------------------------------------------- |
| `yarn twenty docker:start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty docker:stop --test` | Anhalten |
| `yarn twenty docker:status --test` | Status anzeigen |
| `yarn twenty docker:logs --test` | Protokolle streamen |
| `yarn twenty docker:reset --test` | Daten löschen |
| `yarn twenty docker:upgrade --test` | Image aktualisieren |
Die Testinstanz hat ihren eigenen Container (`twenty-app-dev-test`), eigene Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eine eigene Konfiguration — sie läuft parallel zu Ihrer Hauptinstanz ohne Konflikte. Kombinieren Sie `--test` mit `--port`, um den Port 2021 zu überschreiben.
## Manuelle Einrichtung (ohne Scaffolding-Tool)
Überspringen Sie das Scaffolding-Tool, wenn Sie das SDK zu einem bestehenden Projekt hinzufügen:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Fügen Sie der `package.json` das Skript hinzu:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Sie können jetzt `yarn twenty dev`, `yarn twenty docker:start` und den Rest ausführen.
<Note>
Installieren Sie `twenty-sdk` nicht global — fixieren Sie es pro Projekt, damit jede App ihre eigene Version verwendet.
</Note>

View File

@@ -0,0 +1,79 @@
---
title: Projektstruktur
description: Was in einer generierten Twenty-App enthalten ist – Dateien, Ordner und was jede einzelne davon macht.
icon: folder-tree
---
Eine neue App, die mit `npx create-twenty-app` generiert wurde, sieht so aus:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## Wichtige Dateien
| Datei / Ordner | Zweck |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und Metadaten (Anzeigename, Beschreibung). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Eine Willkommens-Startseite: eine Front-Komponente, die von einem eigenständigen Seitenlayout gerendert wird und über die Seitenleiste erreichbar ist. |
| `src/__tests__/` | Ein Unit-Test plus ein Integrationstest (mit seinem globalen Setup), der die App mit einem echten Server synchronisiert. |
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
| `AGENTS.md` / `CLAUDE.md` | Leitfaden für KI-Coding-Agenten, die an der App arbeiten. |
| `CHANGELOG.md` / `SETUP.md` | Changelog mit wichtigen Änderungen und Einrichtungsanweisungen für die lokale Entwicklung. |
<Note>
**Die Dateiorganisation liegt bei Ihnen.** Die oben genannten Ordner sind Konventionen – das SDK erkennt Entitäten über eine AST-Analyse von `export default defineEntity(...)`-Aufrufen, unabhängig davon, wo sich die Datei befindet.
</Note>
## Abhängigkeiten
Beide Twenty-SDK-Pakete gehören unter `devDependencies`, nicht unter `dependencies`:
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
Das Scaffolding-Tool fixiert `twenty-sdk` und `twenty-client-sdk` auf seine eigene Version – halte beide beim Aktualisieren synchron.
* **`twenty-sdk`** stellt die `twenty`-CLI sowie die Build-/Scaffolding-Tools bereit. Es läuft nur während der Entwicklung und beim Build und wird zur Laufzeit der veröffentlichten App niemals importiert.
* **`twenty-client-sdk`** *wird* hingegen von deinem App-Code importiert (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), aber Twenty stellt es zur Laufzeit bereit – Logikfunktionen beziehen es aus einer generierten SDK-Schicht, und Frontend-Komponenten lösen es aus serverseitig ausgelieferten Modulen auf. Deine installierte Kopie wird nur für die Typprüfung und den Build zum Zeitpunkt des Deployments verwendet, daher muss sie niemals im ausgelieferten Bundle enthalten sein.
Wenn eines der Pakete unter `dependencies` bleibt, wird es in das Runtime-Bundle der installierten App gezogen, wo es nur Ballast ist. `twenty dev:build` gibt eine Warnung aus, wenn eines von beiden weiterhin unter `dependencies` aufgeführt ist.
Füge die eigenen Runtime-Abhängigkeiten deiner App (Bibliotheken, die deine Logikfunktionen zur Laufzeit tatsächlich importieren) wie gewohnt unter `dependencies` hinzu. Halte den Abhängigkeitsbaum schlank: `dependencies` werden in eine Laufzeitschicht installiert, die (entpackt) auf 250 MB begrenzt ist, und eine Synchronisierung oder eine Installation schlägt bei Überschreiten dieser Grenze mit einem Fehler zur Größe der Abhängigkeiten fehl — UI-Bibliotheken und Entwicklungstools gehören in `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Schnellstart
icon: rocket
description: Erstellen Sie in wenigen Minuten Ihre erste Twenty-App.
---
## Voraussetzungen
* **Node.js 24.5+** — [Hier herunterladen](https://nodejs.org/)
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es: `corepack enable`
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um einen lokalen Twenty-Server auszuführen. Überspringen Sie dies, wenn Twenty bereits anderswo läuft.
Das Erstellen einer Twenty-App umfasst drei Phasen. Das Scaffolding-Tool fasst sie zu einem einzigen Happy-Path-Befehl zusammen, aber jede Phase ist ein eigenes Konzept — wenn etwas fehlschlägt, hilft Ihnen das Wissen, in welcher Phase Sie sich befinden, zu erkennen, was zu beheben ist.
| Phase | Was Sie tun | Tool | Ergebnis |
| ----------------------- | ------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------- |
| **1. Gerüst erstellen** | Den Quellcode der App erzeugen | `npx create-twenty-app` | Ein TypeScript-Projekt auf der Festplatte |
| **2. Server starten** | Einen Twenty-Server starten, in den synchronisiert wird | Docker + `yarn twenty docker:start` | Eine laufende Twenty-Instanz |
| **3. Synchronisieren** | Ihren Code live mit dem Server synchronisieren | `yarn twenty dev` | Ihre Änderungen erscheinen in der Benutzeroberfläche |
---
## Phase 1 — Projektgerüst erstellen
Erstellen Sie eine neue App aus der Vorlage:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Das Scaffolding-Tool ist nicht interaktiv: Der Verzeichnisname wird zum App-Namen. Übergeben Sie `--display-name` und `--description`, um die erzeugten Metadaten anzupassen (Sie können sie später auch in `src/constants/universal-identifiers.ts` bearbeiten). Dadurch wird ein TypeScript-Projekt in `my-twenty-app/` erzeugt, mit einer Startdatei `application-config.ts`, einer Standardrolle, CI/CD-Workflows und einem Integrationstest.
**Nach dieser Phase:** Sie haben den Quellcode einer App auf Ihrem Rechner. Es läuft noch nicht — das ist Phase 2.
---
## Phase 2 — Einen lokalen Twenty-Server starten
Ihre App benötigt einen Twenty-Server, in den sie synchronisieren kann. Der Server ist eine vollständige Twenty-Instanz — UI, GraphQL-API, PostgreSQL — die lokal in Docker läuft. Ihr lokaler Code lädt seine Definitionen auf diesen Server hoch, wodurch sie in der Benutzeroberfläche erscheinen.
Der Scaffolder startet eine Instanz für Sie: Bei laufendem Docker zieht er das `twentycrm/twenty-app-dev`-Image, startet es auf Port `2020` und authentifiziert die CLI für den vorbefüllten Demo-Workspace (`tim@apple.dev`) – keine Anmeldung erforderlich.
Um stattdessen eine Verbindung zu einem bestehenden Twenty-Server herzustellen, übergeben Sie `--url \<your-server-url>`. Remote-Server authentifizieren sich mit OAuth: Ein Browser öffnet sich, damit Sie sich anmelden und auf **Authorize** klicken können, wodurch die CLI Zugriff auf Ihren Workspace erhält. (Sie können lokal auch OAuth aktivieren mit `--authentication-method oauth` – melden Sie sich mit `tim@apple.dev` / `tim@apple.dev` an.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty-Anmeldebildschirm" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty-CLI-Autorisierungsbildschirm" />
</div>
Ihr Terminal bestätigt, dass alles eingerichtet ist.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App-Gerüst erfolgreich erstellt" />
</div>
**Nach dieser Phase:** Sie haben einen laufenden Twenty-Server unter [http://localhost:2020](http://localhost:2020), und Ihre CLI ist autorisiert, mit ihm zu synchronisieren.
<Note>
Wenn Docker nicht installiert ist oder nicht läuft, zeigt das Scaffolding-Tool den richtigen Startbefehl für Ihr Betriebssystem an. Sobald Docker läuft, können Sie mit `yarn twenty docker:start` fortfahren — ein erneutes Scaffolding ist nicht nötig.
</Note>
---
## Phase 3 — Ihre Änderungen synchronisieren
Das ist die innere Schleife, in der Sie die meiste Zeit verbringen werden.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Dies überwacht `src/`, baut bei jeder Änderung neu und synchronisiert das Ergebnis mit dem Server. Bearbeiten Sie eine Datei, speichern Sie, und innerhalb weniger Sekunden spiegelt der Server die Änderung wider. Sie sehen eine Live-Statusanzeige in Ihrem Terminal.
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) fügen Sie `--verbose` hinzu.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Terminalausgabe im Dev-Modus" />
</div>
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Unter **Your Apps** sollte Ihre App angezeigt werden.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Liste &#x22;Your Apps&#x22;, die &#x22;My twenty app&#x22; anzeigt" />
</div>
Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** anzuzeigen — ein serverseitiger Datensatz, der Ihre App beschreibt (Name, Bezeichner, OAuth-Anmeldedaten, Quelle). Eine Registrierung kann in mehreren Arbeitsbereichen auf demselben Server installiert werden.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Details der Anwendungsregistrierung" />
</div>
Klicken Sie auf **View installed app**, um die Installation im Arbeitsbereich anzuzeigen. Die Registerkarte **About** zeigt die Version und Verwaltungsoptionen.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
</div>
**Nach dieser Phase:** Sie haben eine Live-Entwicklungsschleife. Bearbeiten Sie eine beliebige Datei in `src/`, und sie erscheint in der Benutzeroberfläche.
### Einmalige Synchronisierung für CI und Skripte
Verwenden Sie `plan` und `apply`, um dieselbe Pipeline einmalig ohne Watcher auszuführen:
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| Befehl | Verhalten | Wann verwenden |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `yarn twenty dev` | Überwacht und synchronisiert bei jeder Änderung erneut. Läuft, bis Sie es stoppen. | Interaktive lokale Entwicklung. |
| `yarn twenty apply` | Einmaliger Build + Sync, beendet sich mit `0` bei Erfolg, mit `1` bei Fehler. Fragt bei destruktiven Änderungen nach einer Bestätigung (übergeben Sie `--force`, um dies zu überspringen). | CI, Pre-Commit-Hooks, KI-Agenten, skriptgesteuerte Workflows. |
| `yarn twenty plan` | Erstellt und gibt die Metadatenänderungen aus **ohne sie anzuwenden**. | Prüfen Sie, was eine Synchronisierung ändern würde, bevor Sie sie ausführen. |
Alle Modi benötigen ein authentifiziertes Remote-Repository. Weitere Informationen zu `plan` finden Sie unter [Synchronisierung & Wiederherstellung](/l/de/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan).
<Note>
`yarn twenty dev --once` und `yarn twenty dev --once --dry-run` sind veraltete Aliasse für `yarn twenty apply` und `yarn twenty plan`.
</Note>
### Dev-Modus-Optionen
| Flag | Beschreibung |
| ------------------------------------- | ----------------------------------------------------------------------------------- |
| `--force` | Wendet destruktive Änderungen (Löschungen) ohne Bestätigung an. |
| `--debounceMs \<ms>` | Legt die Entprellzeit für Dateiänderungen in Millisekunden fest (Standard: `1000`). |
| `--verbose` / `--debug` | Zeigt ausführliche Build-Protokolle, Sync-Anfragen und Fehler-Traces an. |
## Was Sie erstellen können
Apps bestehen aus **Entitäten** — jede ist als TypeScript-Datei mit einem einzigen `export default` definiert:
| Entität | Was es tut |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| **Objekte & Felder** | Benutzerdefinierte Datenmodelle (Postkarte, Rechnung usw.) mit typisierten Feldern |
| **Logikfunktionen** | Serverseitiges TypeScript, ausgelöst durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse |
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge |
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
Vollständige Referenz: [Konzepte](/l/de/developers/extend/apps/getting-started/concepts).
## Nächste Schritte
<CardGroup cols={2}>
<Card title="Konfiguration" icon="screwdriver-wrench" href="/l/de/developers/extend/apps/config/overview">
Anwendungsidentität, Standardrolle, Install- und Deinstallations-Hooks, öffentliche Assets.
</Card>
<Card title="Daten" icon="database" href="/l/de/developers/extend/apps/data/overview">
Objekte, Felder und bidirektionale Relationen.
</Card>
<Card title="Logik" icon="bolt" href="/l/de/developers/extend/apps/logic/overview">
Logikfunktionen, Skills, Agents und OAuth-Verbindungen.
</Card>
<Card title="Layout" icon="table-columns" href="/l/de/developers/extend/apps/layout/overview">
Ansichten, Navigation, Seiten-Layouts, Front-Komponenten.
</Card>
<Card title="Operationen" icon="rocket" href="/l/de/developers/extend/apps/operations/overview">
CLI, Tests, Remotes, CI und die Veröffentlichung Ihrer App.
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: Scaffolding
description: Generieren Sie Entitätsdateien interaktiv mit yarn twenty dev:add — Objekte, Felder, Ansichten, Logikfunktionen und mehr.
icon: wand-magic-sparkles
---
Anstatt Entitätsdateien manuell zu erstellen, können Sie den interaktiven Scaffolder verwenden:
```bash filename="Terminal"
yarn twenty dev:add
```
Er fordert Sie auf, einen Entitätstyp auszuwählen, führt Sie durch die erforderlichen Felder und schreibt anschließend 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:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Verfügbare Entitätstypen
| Entitätstyp | Befehl | Generierte Datei |
| -------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| Objekt | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Feld | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Logikfunktion | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Frontend-Komponente | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Rolle | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Ansicht | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Navigationsmenüeintrag | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Seitenlayout | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| Seitenlayout-Registerkarte | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Befehlsmenü-Eintrag | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Ansichtsfeld | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Verbindungsanbieter | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Zeitachsen-Aktivitätstyp | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## Was der Scaffolder generiert
Jeder Entitätstyp hat seine eigene Vorlage. Zum Beispiel fragt `yarn twenty dev: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:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: Fehlerbehebung
description: Häufige Probleme beim ersten Start — Docker, Node-Version, Yarn, Abhängigkeiten.
icon: wrench
---
* **Docker-Fehler** — Stellen Sie sicher, dass Docker Desktop (oder der Daemon) läuft, bevor Sie `yarn twenty docker:start` ausführen. Die Fehlermeldung zeigt den richtigen Startbefehl für Ihr Betriebssystem an.
* **Falsche Node-Version** — Es wird 24.5+ benötigt (`engines.node: ^24.5.0`). Prüfen Sie mit `node -v`.
* **Yarn 4 fehlt** — Führen Sie `corepack enable` aus.
* **Abhängigkeiten defekt** — `rm -rf node_modules && yarn install`.
* **`twenty-sdk`-Fehler nach dem Upgrade auf v2.8.0** — es wurde in v2.8.0 von `dependencies` zu `devDependencies` verschoben. Siehe [Projektstruktur → Abhängigkeiten](/l/de/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty dev:build` warnt vor `twenty-client-sdk` unter `dependencies`** — Es wird zur Laufzeit von Twenty bereitgestellt, daher sollte es in `devDependencies` neben `twenty-sdk` verschoben werden. Siehe [Projektstruktur → Abhängigkeiten](/l/de/developers/extend/apps/getting-started/project-structure#dependencies).
Hängen Sie fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe.

View File

@@ -0,0 +1,118 @@
---
title: Befehlsmenü-Einträge
description: Stellen Sie Front-Komponenten als Schnellaktionen und Einträge im Befehlsmenü (Cmd+K) mit defineCommandMenuItem bereit.
icon: terminal
---
Ein **Befehlsmenü-Eintrag** ist die Brücke zwischen dem Benutzer und einer [Front-Komponente](/l/de/developers/extend/apps/layout/front-components). Er registriert die Komponente im Twenty-Befehlsmenü (Cmd+K) und zeigt sie optional als angeheftete Schnellaktionsschaltfläche oben rechts auf der Seite an.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für den Befehl |
| `label` | Ja | Vollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird |
| `frontComponentUniversalIdentifier` | Ja | Der `universalIdentifier` der Front-Komponente, die dieser Befehl öffnet |
| `shortLabel` | Nein | Kürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird |
| `icon` | Nein | **Veraltet** — wird zugunsten des Anwendungssymbols ignoriert; der Build gibt eine Warnung aus, wenn gesetzt |
| `isPinned` | Nein | Bei `true` wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt |
| `availabilityType` | Nein | Steuert, wo der Befehl erscheint: `'GLOBAL'` (immer verfügbar), `'GLOBAL_OBJECT_CONTEXT'` (nur auf Seiten mit einem Objektkontext – Index- und Datensatzseiten), `'RECORD_SELECTION'` (nur wenn Datensätze ausgewählt sind) oder `'FALLBACK'` (wird angezeigt, wenn keine anderen Befehle passen) |
| `availabilityObjectUniversalIdentifier` | Nein | Beschränken Sie den Befehl auf Seiten eines bestimmten Objekttyps (z. B. nur bei Company-Datensätzen) |
| `conditionalAvailabilityExpression` | Nein | Ein boolescher Ausdruck, der die Sichtbarkeit dynamisch steuert (siehe unten) |
## Headless-Befehle
Ein Befehlsmenü-Eintrag, der mit einer [Headless-Front-Komponente](/l/de/developers/extend/apps/layout/front-components#headless-vs-non-headless) gekoppelt ist, ist die idiomatische Art, eine One-Click-Aktion bereitzustellen – Code ausführen, navigieren oder bestätigen und ausführen. Die Seite „Front Components“ behandelt die [SDK Command-Komponenten](/l/de/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`), die das Action-and-Unmount-Muster handhaben.
Ein typischer Ablauf: Eine Headless-Komponente rendert `<Command execute={...} />` (siehe das [vollständige Beispiel](/l/de/developers/extend/apps/layout/front-components#sdk-command-components)), und der Befehlsmenüeintrag verweist darauf:
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## 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:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` impliziert bereits eine nichtleere Auswahl — verwende `numberOfSelectedRecords` nur für bestimmte Zählwerte (z. B. `>= 2`).
</Note>
### Kontextvariablen
Diese repräsentieren den aktuellen Zustand der Seite:
| Variable | Typ | Beschreibung |
| ------------------------------ | --------- | --------------------------------------------------------------- |
| `pageType` | `string` | Aktueller Seitentyp (z. B. 'RecordIndexPage', 'RecordShowPage') |
| `isInSidePanel` | `boolean` | Ob die Komponente in einem Seitenpanel gerendert wird |
| `numberOfSelectedRecords` | `number` | Anzahl der aktuell ausgewählten Datensätze |
| `isSelectAll` | `boolean` | Ob „Alle auswählen“ aktiv ist |
| `selectedRecords` | `array` | Die ausgewählten Datensatzobjekte |
| `favoriteRecordIds` | `array` | IDs der favorisierten Datensätze |
| `objectPermissions` | `object` | Berechtigungen für den aktuellen Objekttyp |
| `targetObjectReadPermissions` | `object` | Leseberechtigungen für das Zielobjekt |
| `targetObjectWritePermissions` | `object` | Schreibberechtigungen für das Zielobjekt |
| `featureFlags` | `object` | Aktive Feature-Flags |
| `objectMetadataItem` | `object` | Metadaten des aktuellen Objekttyps |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Ob die aktuelle Ansicht einen Soft-Delete-Filter hat |
### Operatoren
Kombinieren Sie Variablen zu booleschen Ausdrücken:
| Operator | Beschreibung |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| `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 |

View File

@@ -0,0 +1,849 @@
---
title: Frontend-Komponenten
description: Erstellen Sie React-Komponenten, die innerhalb der Twenty-UI gerendert werden und durch eine Sandbox isoliert sind.
icon: window-maximize
---
Front-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 einem sandboxed iframe mit opaker Origin ausgeführt, wobei die UI dennoch nativ auf der Seite gerendert wird und nicht auf dieses iframe beschränkt ist.
<Warning>
Front-Komponenten befinden sich noch in aktiver Entwicklung. Ihr Code wird gegen ein teilweises DOM und nicht gegen eine echte Browserseite ausgeführt, sodass fortgeschrittene Anwendungsfälle fehlschlagen können – oft ohne sichtbare Fehlermeldung. Siehe [Aktuelle Einschränkungen](#current-limitations).
</Warning>
## Wo Front-Komponenten verwendet werden können
Front-Komponenten können an drei 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](/l/de/developers/extend/apps/layout/page-layouts) eingebettet werden. Beim Konfigurieren eines Dashboards oder eines Datensatzseiten-Layouts können Benutzer ein Front-Komponenten-Widget hinzufügen.
* **App settings** — Definiert mit [`defineSettingsFrontComponent()`](#custom-settings-component), wird die Front-Komponente als Abschnitt im **Settings**-Tab der App gerendert und ersetzt dabei die standardmäßige Variablenkonfigurationsoberfläche.
Eine Front-Komponente allein ist über die Benutzeroberfläche nicht erreichbar – Sie müssen sie *sichtbar machen*. Die drei Möglichkeiten dafür sind:
* **Mit einem [Befehlsmenüeintrag](/l/de/developers/extend/apps/layout/command-menu-items) verknüpfen** — registriert sie im Befehlsmenü (Cmd+K) und optional als angeheftete Schnellaktion.
* **Als Widget in ein [Seitenlayout](/l/de/developers/extend/apps/layout/page-layouts) einbetten** — platziert es auf der Detailseite eines Datensatzes oder in einem Dashboard.
* **Definiere sie mit [`defineSettingsFrontComponent()`](#custom-settings-component)** — rendert sie als Abschnitt im **Settings**-Tab der App und ersetzt dabei die standardmäßige Variablenkonfigurationsoberfläche.
## Einfaches Beispiel
Die schnellste Möglichkeit, eine Front-Komponente in Aktion zu sehen, besteht darin, sie mit einem [`defineCommandMenuItem`](/l/de/developers/extend/apps/layout/command-menu-items) zu verknüpfen, sodass sie als Schnellaktionsschaltfläche in der oberen rechten Ecke der Seite erscheint:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
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,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Nach dem Synchronisieren mit `yarn twenty dev` (oder durch einmaliges Ausführen von `yarn twenty apply`) erscheint die Schnellaktion oben rechts auf der Seite:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Schnellaktionsschaltfläche oben rechts" />
</div>
Klicken Sie darauf, um die Komponente inline zu rendern.
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| --------------------- | ------------ | --------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
| `component` | Ja | Eine React-Komponentenfunktion |
| `name` | Nein | Anzeigename |
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
## Eine Front-Komponente auf einer Seite platzieren
Über Befehle hinaus können Sie eine Front-Komponente direkt in eine Datensatzseite einbetten, indem Sie sie als Widget in einem **Seitenlayout** hinzufügen. Details finden Sie unter [Seitenlayouts](/l/de/developers/extend/apps/layout/page-layouts).
## Benutzerdefinierte Einstellungen-Komponente
Um die automatisch generierte Variablenkonfigurationsoberfläche im **Settings**-Tab deiner App durch deine eigene Komponente zu ersetzen, definiere sie mit `defineSettingsFrontComponent` statt mit `defineFrontComponent`. Es verwendet dieselben [Konfigurationsfelder](#configuration-fields) (mit Ausnahme von `isHeadless`, das nicht akzeptiert wird, da eine Einstellungskomponente immer eine sichtbare Benutzeroberfläche rendert) und kennzeichnet die Komponente zusätzlich als Einstellungen-UI der App.
Die Komponente wird als Abschnitt innerhalb des **Settings**-Tabs gerendert, nicht als Ersatz für den gesamten Tab. Die von Twenty verwalteten Systembereiche – Auto-Upgrade, App-URL und Verbindungen – werden immer darüber gerendert und können von der App nicht überschrieben werden.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
Pro App ist nur eine Einstellungs-Frontkomponente zulässig; wenn mehr als eine deklariert wird, schlägt der Build fehl. Wenn vorhanden, rendert der **Settings**-Tab der App diese Komponente anstelle der standardmäßigen Konfigurationsoberfläche für Variablen.
## 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.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
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/front-component`:
* **`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 Seite im Seitenpanel. Props hängen von `page` ab — z. B. verwendet `ViewRecord` `recordId` + `objectNameSingular` (plus eine optionale `tab`-ID, um den Datensatz auf einem bestimmten Tab zu öffnen), andere Seiten verwenden `pageTitle` + `pageIcon`.
Hier ist ein vollständiges Beispiel einer Headless-Front-Komponente, die `Command` verwendet, um eine Aktion aus dem Befehlsmenü auszuführen:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
Und ein Beispiel, das `CommandModal` verwendet, um vor der Ausführung um Bestätigung zu bitten:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/front-component';
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,
});
```
Und ein Beispiel mit `CommandOpenSidePanelPage`, um den aktuellen Datensatz im Seitenbereich auf einem bestimmten Tab zu öffnen. `tab` ist eine Seitenlayout-Tab-ID (Standardlayouts verwenden IDs wie `company-tab-emails` oder `company-tab-timeline`; benutzerdefinierte Layouts verwenden die eigene ID des Tabs). Wenn die ID im Layout des Datensatzes nicht vorhanden ist, wird stattdessen der Standard-Tab geöffnet:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Aufrufen einer Logikfunktion
Front-Komponenten laufen browserseitig in einem Web-Worker, der in einem sandboxed iframe mit opaker Origin ausgeführt wird, während [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) serverseitig laufen. Es gibt keinen direkten In-Process-Aufruf zwischen beiden – stattdessen ruft eine Front-Komponente eine Logikfunktion über HTTP auf.
Eine mit `httpRouteTriggerSettings` deklarierte Logikfunktion ist über HTTP unter ihrem Routenpfad erreichbar. `RestApiClient` behandelt Pfade, die mit `/s/` beginnen, als App-Routen, löst sie zur URL auf, unter der deine Funktionen bereitgestellt werden, und authentifiziert sie mit `TWENTY_APP_ACCESS_TOKEN`.
> **In Twenty Cloud werden HTTP-ausgelöste Logikfunktionen auf einer eigenen, arbeitsbereichsspezifischen Domain bereitgestellt** unter `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Für externe Aufrufer kopiere die exakte URL aus den **HTTP trigger**-Einstellungen der Funktion oder aus dem **Settings**-Tab der Anwendung.
Eine headless Front-Komponente kann den Aufruf beim Mounten über die `Command`-Komponente ausführen und sich anschließend automatisch unmounten:
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
Der an `RestApiClient` übergebene Pfad ist der `httpRouteTriggerSettings.path` der Logikfunktion, der mit `/s` präfixiert ist. Belasse `isAuthRequired: true`; das `TWENTY_APP_ACCESS_TOKEN`, das Twenty für deine Komponente ausstellt, authentifiziert die Anfrage:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` wird automatisch injiziert – siehe [Anwendungsvariablen](#application-variables). Da geheime Anwendungsvariablen niemals in Front-Komponenten offengelegt werden, sollten API-Schlüssel und andere sensible Logik in der Logikfunktion verbleiben und nicht in der Front-Komponente.
</Note>
### Aufrufen der Twenty-REST-API
Um App-HTTP-Routen aufzurufen oder Twenty-Datensätze aus einer Front-Komponente zu lesen und zu schreiben, verwende `RestApiClient` aus `twenty-client-sdk/rest`. Es sendet `/s/...`-Pfade an die Funktions-Basis-URL deines Arbeitsbereichs und alle anderen Pfade, einschließlich `/rest/...`, an `TWENTY_API_URL`.
Es handelt immer als die Person, die die Seite betrachtet. `runAs: 'application'` ist nur eine Option für Logikfunktionen: Eine Komponente erhält niemals das eigene Token Ihrer Anwendung. Wenn Sie es hier anfordern, wird ein Fehler ausgelöst. Lagern Sie Arbeiten, die den eigenen Zugriff der Anwendung benötigen, hinter eine Logikfunktion aus und rufen Sie stattdessen diese auf.
| Methode | Beschreibung |
| --------------------------------- | ---------------------------------------------------------------------------------------- |
| `get(path, options?)` | Sendet eine `GET`-Anfrage |
| `post(path, body?, options?)` | Sendet eine `POST`-Anfrage |
| `put(path, body?, options?)` | Sendet eine `PUT`-Anfrage |
| `patch(path, body?, options?)` | Sendet eine `PATCH`-Anfrage |
| `delete(path, options?)` | Sendet eine `DELETE`-Anfrage |
| `request(method, path, options?)` | Generische Anfrage mit einer beliebigen HTTP-Methode |
| `resolveUrl(path, options?)` | Löst einen Pfad zu seiner vollständigen URL auf, ohne eine Anfrage zu senden (für Links) |
`options` akzeptiert `headers`, `query` (ein Record von Query-String-Parametern; null- bzw. undefined-Werte werden übersprungen) sowie ein `AbortSignal` über `signal`. Ein `body`-Objekt, das kein `FormData` ist, wird automatisch als JSON serialisiert. Bei einem `401` aktualisiert der Client das Access-Token einmal über den Host und versucht die Anfrage erneut.
Die Basis-URL und das Token werden standardmäßig aus der Umgebung ermittelt. Gib bei Bedarf – zum Beispiel in Tests – Überschreibungen an den Konstruktor weiter:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Fehlgeschlagene Anfragen lösen einen `RestApiClientError` aus, der `status`, `statusText`, `url` und den geparsten `body` bereitstellt:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## Zugriff auf den Laufzeitkontext
Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
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:
| Hook | Gibt zurück | Beschreibung |
| --------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------- |
| `useUserId()` | `string` oder `null` | Die ID des aktuellen Benutzers |
| `useSelectedRecordIds()` | `string[]` | Alle ausgewählten Datensatz-IDs (leeres Array, wenn keine ausgewählt sind) |
| `useRecordId()` | `string` oder `null` | **Veraltet.** Verwenden Sie stattdessen `useSelectedRecordIds()` |
| `useFrontComponentId()` | `string` | Die ID dieser Komponenteninstanz |
| `useTimelineActivityId()` | `string` oder `null` | Die aktuelle Timeline-Aktivitäts-ID beim Rendern einer benutzerdefinierten Timeline-Zeile |
| `useColorScheme()` | `'light'` oder `'dark'` | Das aktive Farbschema der Host-UI (`System` ist bereits aufgelöst) |
| `useFrontComponentExecutionContext(selector)` | variiert | Zugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion |
## Anwendungsvariablen
In [`defineApplication()`](/l/de/developers/extend/apps/config/application) mit `isSecret: false` definierte Anwendungsvariablen sind in Front-Komponenten über das Hilfsprogramm `getApplicationVariable` verfügbar:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Geheime Variablen (`isSecret: true`) werden **nicht** in Front-Komponenten offengelegt. Sie sind nur in [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) verfügbar, die serverseitig ausgeführt werden. Dadurch wird verhindert, dass sensible Werte wie API-Schlüssel an den Browser gesendet werden.
</Warning>
`getApplicationVariable` gibt immer einen **String** (oder `undefined`) zurück, unabhängig vom deklarierten `type` der Variable. Der String wird je nach Typ konsistent serialisiert (boolesche Werte als `"true"` / `"false"`, Zahlen als Dezimalstrings, Arrays / Objekte als JSON), im selben Format, das für die Logikfunktion `process.env` verwendet wird — parsen Sie ihn selbst (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Siehe [Variablentypen](/l/de/developers/extend/apps/config/application#variable-types).
Die folgenden Systemvariablen sind immer über `process.env` verfügbar:
| Variable | Beschreibung |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | Basis-URL der Twenty-Core-API |
| `TWENTY_APP_ACCESS_TOKEN` | Kurzlebiges Token, das auf die Schnittmenge der Rolle der angemeldeten Person und der Rolle Ihrer Anwendung beschränkt ist, sodass eine Komponente niemals mehr tun kann als die Person, die sie betrachtet |
### `TWENTY_FUNCTIONS_URL`
Twenty injiziert außerdem `TWENTY_FUNCTIONS_URL` in Front-Komponenten und Logikfunktionen: die Basis-URL, unter der die HTTP-ausgelösten Logikfunktionen deiner App bereitgestellt werden.
Sie existiert, weil diese URL nicht immer der Twenty-Server selbst ist. In Twenty Cloud werden App-Routen auf einer eigenen, arbeitsbereichsspezifischen Domain bereitgestellt (`https://\<your-workspace-subdomain>.withtwenty.com` oder die primäre öffentliche Domain der Anwendung, falls eine konfiguriert ist), damit von der App erzeugte Antworten in einem isolierten Origin und nicht im Origin der Twenty-App ausgeführt werden. Self-Hosted- und lokale Instanzen stellen App-Routen unter dem Präfix `/s` auf dem Server selbst bereit und setzen die Variable eventuell gar nicht. Da die Basis-URL je nach Arbeitsbereich und Instanz variiert, kann dein Code sie nicht hardcoden – der Server injiziert zur Laufzeit den richtigen Wert.
Du musst sie nur selten direkt lesen. Rufe deine Routen über `RestApiClient` mit einem mit `/s/` präfixierten Pfad auf, und der Client löst die URL für dich auf: Er entfernt das Präfix `/s` und verwendet `TWENTY_FUNCTIONS_URL` als Ziel, mit Fallback auf `\<TWENTY_API_URL>/s`, wenn die Variable nicht gesetzt ist. Verwende `resolveUrl('/s/\<path>')`, um die absolute URL ohne das Senden einer Anfrage zu erhalten, z. B. für einen Link. Lies die Variable nur direkt aus, wenn du eine URL manuell zusammenbaust:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## Host-Kommunikations-API
Front-Komponenten können Navigation, Modals und Benachrichtigungen mittels Funktionen aus `twenty-sdk` auslösen:
| Funktion | Beschreibung |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Lädt ein `Blob` in ein FILES-Feld hoch; gibt entweder `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` oder `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` zurück |
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
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,
});
```
### Speicher
`localStorage` und `sessionStorage` funktionieren wie auf einer normalen Seite mit der standardmäßigen synchronen API. Deine Schlüssel sind auf die Installation deiner App und die angemeldete Benutzerin bzw. den angemeldeten Benutzer beschränkt: Keine andere App kann sie lesen, und wenn sich eine andere Benutzerin oder ein anderer Benutzer im selben Browser anmeldet, beginnt diese Person mit einem leeren Speicher. In `localStorage` geschriebene Werte bleiben geräteweit über Neuladungen hinweg erhalten; `sessionStorage` besteht für die Dauer der Browsersitzung.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty speichert die Werte im Namen deiner App, sodass ein Schreibvorgang lokal sofort angewendet und im Hintergrund gespeichert wird. Lesevorgänge warten niemals auf den Host. Es wird nichts synchronisiert: Die Werte folgen der Benutzerin oder dem Benutzer nicht in einen anderen Browser oder auf eine andere Maschine. Verwende daher den [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store) einer Logic Function für alles, was einen Gerätewechsel überstehen muss.
Schreibvorgänge sind begrenzt, und alle Limits zählen Zeichen statt Bytes: Schlüssel sind höchstens 512 Zeichen lang, ein einzelner Wert höchstens 262.144 Zeichen, und jeder Speicher höchstens 1.048.576 Zeichen pro App und Benutzerin oder Benutzer. Ein Schreibvorgang, der ein Limit überschreitet, löst einen `QuotaExceededError` aus, wie bei der Browser-API.
### Mit mehreren Datensätzen arbeiten
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
Machen Sie es über einen [Befehlsmenüeintrag](/l/de/developers/extend/apps/layout/command-menu-items) sichtbar, der auf die Auswahl von Datensätzen beschränkt ist:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Öffentliche Assets
Front-Komponenten können mit `getPublicAssetUrl` auf Dateien aus dem `public/`-Verzeichnis der App zugreifen:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
Details finden Sie im Abschnitt [Öffentliche Assets](/l/de/developers/extend/apps/config/public-assets).
## Freigabe von Abhängigkeiten über die Front-Komponenten
Standardmäßig bündelt jede Front-Komponente ihre eigene Kopie der von ihr importierten Bibliotheken, so dass eine App mit fünf Komponenten fünf Mal React verschickt. Erklären Sie geteilte Abhängigkeiten im Paket Ihrer App. son\`, um diese Bibliotheken einmal zu erstellen und jede Komponente der App aus einer einzigen zwischengespeicherten Datei laden zu lassen:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Jede Komponente importiert dann ihre Abhängigkeiten genau wie zuvor — nichts ändert sich im Komponentencode:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Ein paar Dinge, die Sie wissen sollten:
* **Ein freigegebenes Abhängigkeitsbündel pro App.** Das Paket wird aus den eigenen Abhängigkeiten deiner App aufgebaut, so dass du die volle Kontrolle über die von dir ausgelieferten Versionen behalten kannst.
* **Listet die exakten Angaben, die du importierst, auf.** `twenty-ui/input` und `twenty-ui/display` sind zwei Einträge; ein Paketname allein deckt seine Unterpfade nicht ab. Das Auflisten von `react` deckt automatisch `react/jsx-runtime` ab.
* **Teile `react-dom/client` neben `react`.** Jede Komponente wird durch `createRoot` erzeugt, so dass es herauskommt, dass jede Komponente noch React DOM-Pakete enthält.
* \*\*Das Bundle wird zwischengespeichert. \* Es wird unter einer Content-Hash-URL mit einem langlebigen unveränderlichen Cache versorgt so dass es einmal heruntergeladen und über alle Komponenten der App wiederverwendet wird, bis sich eine seiner Abhängigkeiten ändert.
* **Komponenten, die keines der gemeinsam genutzten Pakete importieren, laden sie nie herunter.**
## Styling
Front-Komponenten unterstützen mehrere Styling-Ansätze. Sie können verwenden:
* **Inline-Styles** — `style={{ color: 'red' }}`
* **Twenty UI-Komponenten** — Twentys eigene Komponentenbibliothek; siehe [Verwenden von Twenty UI-Komponenten](#using-twenty-ui-components) unten
* **Emotion** — CSS-in-JS mit `@emotion/react`
* **Styled-components** — `styled.div`-Muster
* **Tailwind CSS** — Utility-Klassen
* **Beliebige CSS-in-JS-Bibliothek**, die mit React kompatibel ist
## Verwenden von Twenty UI-Komponenten
Twenty liefert seine Komponentenbibliothek als [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)-Paket aus. Frontend-Komponenten können sie für Schaltflächen, Tags, Status-Pills, Chips, Avatare, Icons, Typografie und Theme-Tokens verwenden, die automatisch zum hellen und dunklen Theme des Arbeitsbereichs passen.
### Installation
Fügen Sie das Paket zu Ihrer App hinzu und fixieren Sie es auf die Version, mit der Ihre Twenty-Instanz ausgeliefert wird:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` wird zur Build-Zeit in Ihre Frontend-Komponente gebündelt, sodass es nur eine Abhängigkeit Ihrer App sein muss – zur Laufzeit muss nichts konfiguriert werden.
### Komponenten importieren
Importieren Sie aus dem passenden Subpfad statt aus dem Paket-Root, damit nur die Komponenten, die Sie verwenden, in Ihrem Bundle landen:
| Subpfad | Was exportiert wird |
| --------------------------- | ------------------------------------------------- |
| `twenty-ui/input` | `Button`- und Formulareingaben |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` und mehr |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` und mehr |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` und mehr |
| `twenty-ui/icon` | `Icon*`-Komponenten (z. B. `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### Icons
Importieren Sie einzelne Icons aus `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Jedes benannte Icon wird per Tree Shaking berücksichtigt, sodass der Import einiger weniger Ihr Bundle nur gering vergrößert. Vermeiden Sie `IconsProvider`, `useIcons` und `iconsState` – sie ziehen das vollständige Tabler-Icon-Set (mehrere MB) mit hinein.
### Theming und Theme-Tokens
Twenty UI-Komponenten passen sich automatisch dem hellen und dunklen Theme des Arbeitsbereichs an – der Renderer wendet das aktive Farbschema auf dem Host an und die Komponenten leiten ihre Farben daraus ab.
Um dieselben Design-Tokens in Ihren eigenen Inline-Styles zu verwenden, rufen Sie den Hook `useTheme()` auf. Er gibt die Theme-Tokens von Twenty (Abstände, Farben, Radien, Schriftarten) zurück, die mit dem aktiven Theme verknüpft sind – ohne dass ein `ThemeProvider` in Ihrer Komponente eingerichtet werden muss:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Da `useTheme()` ein Hook ist, lesen Sie Tokens im Komponenten-Body aus, sodass die Werte immer das aktuelle Theme widerspiegeln. Dieselbe Token-Map wird auch als Konstante `themeCssVariables` exportiert, aber bevorzugen Sie `useTheme()` in Frontend-Komponenten – eine modulweite Konstante, die `themeCssVariables` dereferenziert, kann undefiniert sein, während das App-Manifest extrahiert wird.
Um explizit nach dem aktiven Schema zu verzweigen, lesen Sie es mit `useColorScheme()` aus `twenty-sdk/front-component` aus; der Hook gibt 'light' oder 'dark' zurück.
## Aktuelle Einschränkungen
Front-Komponenten befinden sich in aktiver Entwicklung. Rendern, Styling, Ereignisbehandlung, Messen von Elementen und Browser-Speicher funktionieren gut. Alles, was *über* diese Aspekte hinausgeht (Aufrufen einer DOM-Methode auf einem Ref, Beobachten von Größenänderungen eines Elements, Portaling außerhalb deines Baums), fehlt heute noch oder ist unvollständig, und das meiste davon schlägt stillschweigend fehl: keine Exception und auch kein TypeScript-Fehler, da das Gerüst gegen das vollständige Browser-DOM typisiert ist.
Wenn dich eines dieser Probleme blockiert, [eröffne ein Issue](https://github.com/twentyhq/twenty/issues/new/choose), damit es priorisiert wird.
### Layout und Messung
Elemente können sich selbst messen: Der Host spiegelt die Geometrie in die Sandbox, sodass Lesezugriffe lokal beantwortet werden, aber bis zu einem Frame veraltet sein können, und der erste Lesezugriff auf ein noch nie gemessenes Element gibt Nullen zurück. Nach dem Schreiben erneut in einem `requestAnimationFrame`-Callback oder in einem Effekt lesen.
| API | Was passiert |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funktionieren über den Spiegel; das Zuweisen von `scrollTop` / `scrollLeft` ist ein No-Op |
| `getClientRects()`, `window.matchMedia()` | Löst eine Exception aus |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funktionieren und melden den **Browser-Viewport**; die eigene Größe deines Widgets ist `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Gibt nur die Inline-Styles des Elements zurück, niemals die vom Host berechnete Kaskade |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof`-Guards funktionieren) |
| `MutationObserver` | Funktioniert, einschließlich `subtree`, `attributeFilter`, alter Werte und `takeRecords()` |
Positionierung über `getBoundingClientRect` funktioniert jetzt, aber alles, was Größenänderungen über `ResizeObserver` beobachtet (Recharts `ResponsiveContainer`, Floating UIs `autoUpdate`), funktioniert weiterhin nicht. Verwende ohnehin CSS für das Layout: dein Stylesheet erreicht die echte Seite, daher verhalten sich Flexbox, Grid, `aspect-ratio`, `clamp()` und `@container` normal, ohne Frame-Lag.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` und `queueMicrotask` funktionieren ohne das Präfix `window.`. Nur `window.requestAnimationFrame(...)` und ähnliche Aufrufe lösen eine Exception aus.
</Note>
### DOM-Zugriff
Ein `ref` gibt dir ein Sandbox-Element, kein `HTMLElement`.
| Was du schreibst | Was passiert | Stattdessen verwenden |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Löst eine Exception aus | Gesteuerte Komponenten; Werte aus `event.target` auslesen |
| `element.classList.add(...)` | Löst eine Exception aus (`classList` ist `undefined`) | Erstelle den `className`-String selbst |
| `document.createTreeWalker()` | Löst eine Exception aus | `querySelector()` / `querySelectorAll()` und `getElementById()` funktionieren; `getElementsByClassName()` funktioniert ebenfalls, gibt aber eine statische Collection zurück, keine Live-`HTMLCollection` |
| `document.activeElement` | Immer `undefined` | Fokus mit `onFocus` / `onBlur` verfolgen |
| `canvas` | Rendert nichts, keine Fehlermeldung | SVG oder außerhalb des Bildschirms zeichnen und das Ergebnis in einem `img`-Element anzeigen |
| `createPortal(node, document.body)` | Rendert nichts, während `isConnected` Erfolg meldet | Overlays inline mit `position: absolute` oder der Bibliothek dein eigenes Container-Element übergeben |
Diese Portal-Lücke ist der Grund, warum Radix-, Headless-UI-, MUI- und react-select-Popover standardmäßig nichts rendern. Die meisten akzeptieren eine Container-Prop; verweise sie auf ein Element, das du gerendert hast.
### Ereignisse
Maus, Zeiger, Touch, Drag, Tastatur, Fokus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` und `animationend`/`transitionend` werden an den Host durchgereicht, plus ein paar pro Element: `load`/`error` auf `img`, Zwischenablage und Komposition auf `input`/`textarea`, Medien auf `video`/`audio`, `toggle` auf `details`/`dialog`. Alles andere (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, Pointer-Capture, `onLoad` von `img`) wird ohne Warnung verworfen.
`document.addEventListener()` und `window.addEventListener()` werden ohne Fehler registriert und feuern nie, weshalb ein Drag aufhört, sobald der Zeiger das Element verlässt, auf dem er begonnen hat. `event.preventDefault()` wird ebenfalls nicht durchgereicht; Formularübermittlung, `dragover`/`drop` und Link-Klicks sind bereits für dich abgesichert.
### Attribute und Styling
Jedes Element leitet seine eigenen Eigenschaften an den Host-DOM weiter (`href` auf `a`, `src`/`alt` auf `img`, `value`/`placeholder`/`disabled` auf `input` usw.), plus einen gemeinsamen Satz auf jedem Element: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` und jedes `aria-*`-/`data-*`-Attribut (mit Bindestrich, daher wird `ariaLabel` verworfen). Alles außerhalb davon wird stillschweigend verworfen, daher solltest du benutzerdefinierte Zustände als `data-*` ausdrücken.
Component-CSS, egal ob aus `import './styles.css'`, CSS-in-JS oder einem `style`-Element, wird **unscoped** in den `head` der Host-Seite injiziert. Dadurch kollidieren Klassennamen mit denen von Twenty (präfixe sie und schreibe niemals nacktes `div { ... }`-Selektoren), und `@media` bezieht sich auf das Browserfenster statt auf dein Widget (verwende `@container` mit deinem eigenen `container-type`). Inline-`style`-Props sind nicht betroffen.
### Speicher und Netzwerk
`localStorage` und `sessionStorage` werden von Twenty statt vom Browser bereitgestellt: Die Komponente läuft in einem Worker mit einem intransparenten Ursprung, daher speichert der Host die Werte im Namen deiner App. Siehe [Speicher](#storage) für ihren Geltungsbereich und ihre Limits. IndexedDB, Cookies, die Cache API und `BroadcastChannel` sind weiterhin nicht verfügbar. Um Zustand geräteübergreifend zu persistieren, rufe eine [Logic Function](/l/de/developers/extend/apps/logic/logic-functions) auf und verwende deren [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store).
`fetch` funktioniert, mit Einschränkungen:
* Aufrufe an die Twenty-API und die Routen deiner App werden vom Host als Proxy weitergeleitet, daher solltest du [`RestApiClient`](#calling-the-twenty-rest-api) bevorzugen. Bei proxied Aufrufen werden `AbortSignal` und die anderen `RequestInit`-Optionen verworfen, und es werden nur `string`- und `URLSearchParams`-Bodies unterstützt.
* Andere Origins verlassen die Sandbox mit `Origin: null`, sodass eine Drittanbieter-API nur antwortet, wenn sie `Access-Control-Allow-Origin: *` sendet. Rufe sie stattdessen aus einer Logic Function auf.
* `fetch('/rest/people')` wird niemals der Twenty-API zugeordnet, weil die Sandbox keine Seiten-URL hat, gegen die ein relativer Pfad aufgelöst werden könnte.
### Medienerfassung
`navigator.mediaDevices.getUserMedia()` und `MediaRecorder` funktionieren innerhalb von Front-Komponenten über Sandbox-Polyfills, sodass Standard-Aufnahmecode unverändert ausgeführt wird und `MediaRecorder.isTypeSupported` Auskunft darüber gibt, ob gängige Container-/Codec-Kombinationen unterstützt werden. Detaillierte `getUserMedia`-Constraint-Objekte werden akzeptiert, aber nicht weitergereicht – der Host erfasst mit seinen Standardwerten für die angeforderten Typen – und es kann immer nur eine Aufnahme gleichzeitig über Anwendungen hinweg aktiv sein. Speichere ein aufgezeichnetes `Blob` mit der Host-Funktion `uploadFile`.
### Weitere Lücken
* **Dateiinhalt.** Ein `input` des Typs `file` gibt deinem Handler nur Dateimetadaten, nicht die Bytes, daher ist `FileReader` nicht verfügbar. Um ein `Blob` hochzuladen, das dein Code bereits besitzt – z. B. eines, das von `MediaRecorder` erzeugt wurde – verwende die Host-Funktion `uploadFile`.
* **Drag-and-drop-Nutzlasten.** Drag-Events werden ausgelöst, aber `event.dataTransfer` ist `undefined`.
* **Node-Built-ins.** `fs`, `path` und `node:crypto` führen zu einem Build-Fehler, daher solltest du diese Arbeit in eine [Logic Function](/l/de/developers/extend/apps/logic/logic-functions) auslagern. Web Crypto, `fetch`, `TextEncoder` und `URL` sind verfügbar.
* **`iframe`** wird immer erneut ohne `allow-same-origin` in einer Sandbox ausgeführt, sodass eine Einbettung, die auf ihre eigene Sitzung angewiesen ist, als abgemeldet gerendert wird. Es hat auch kein `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Navigationsmenüeinträge
description: Fügen Sie der Arbeitsbereichsseitenleiste benutzerdefinierte Einträge hinzu – Links zu gespeicherten Ansichten oder externen URLs.
icon: bars
---
Ein **Navigationsmenüeintrag** ist ein Eintrag in der linken Seitenleiste. Verwenden Sie `defineNavigationMenuItem()`, um benutzerdefinierte Seitenleistenlinks bereitzustellen – typischerweise einen pro [Ansicht](/l/de/developers/extend/apps/layout/views), die Sie bereitstellen – oder um auf externe URLs zu verweisen.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
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` legt fest, worauf der Menüeintrag verweist. Jeder Typ ist einem bestimmten Bezeichnerfeld zugeordnet:
| Typ | Was es tut | Pflichtfeld |
| ------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Öffnet eine gespeicherte Ansicht | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Öffnet eine externe URL | `link` |
| `NavigationMenuItemType.FOLDER` | Gruppiert verschachtelte Einträge unter einer Bezeichnung | `name` (und untergeordnete Einträge verweisen über `folderUniversalIdentifier` auf den Ordner) |
| `NavigationMenuItemType.OBJECT` | Öffnet die Standardindexseite eines Objekts | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Öffnet ein eigenständiges Seitenlayout | `pageLayoutUniversalIdentifier` |
* `position` steuert die Reihenfolge in der Seitenleiste.
* Das Enum enthält außerdem `NavigationMenuItemType.RECORD`, das intern für vom Benutzer erstellte Datensatzfavoriten verwendet wird — es ist in einem App-Manifest nicht verwendbar (es gibt kein Feld, um auf einen Datensatz zu verweisen).
* `icon` und `color` sind optional und passen das Erscheinungsbild des Eintrags an.
* `folderUniversalIdentifier` ist ebenfalls bei jedem Eintrag verfügbar, um ihn innerhalb eines übergeordneten Elements vom Typ `FOLDER` zu verschachteln.
<Note>
**Häufige Falle:** Wenn Sie ein Objekt ohne zugehörigen Navigationsmenüeintrag erstellen, ist dieses Objekt von der Seitenleiste aus nicht erreichbar. Sofern es sich nicht um ein technisches/internes Objekt handelt, sollte jedes benutzerdefinierte Objekt einen entsprechenden Eintrag in der Seitenleiste haben, der darauf verweist.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Übersicht
description: Binden Sie Ihre App in das UI von Twenty ein – Seitenleisten-Einträge, gespeicherte Ansichten, Registerkarten auf Datensatzseiten und isolierte React-Komponenten.
icon: table-columns
---
Die **Layout-Ebene** einer Twenty-App umfasst alles, was der Benutzer sieht: wo die App in der Seitenleiste erscheint, welche Listenansichten sie bereitstellt, wie ihre Detailseiten für Datensätze angeordnet sind und welche benutzerdefinierten React-Komponenten innerhalb dieser Seiten gerendert werden.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="Ansichten" icon="list" href="/l/de/developers/extend/apps/layout/views">
`defineView` — gespeicherte Listen-Konfigurationen: sichtbare Spalten, Filter, Gruppen.
</Card>
<Card title="Navigationsmenüeinträge" icon="bars" href="/l/de/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — Seitenleisten-Einträge, die auf Ansichten oder externe URLs verweisen.
</Card>
<Card title="Seitenlayouts" icon="table-columns" href="/l/de/developers/extend/apps/layout/page-layouts">
`definePageLayout` und `definePageLayoutTab` — Registerkarten und Widgets auf der Detailseite eines Datensatzes.
</Card>
<Card title="Frontend-Komponenten" icon="window-maximize" href="/l/de/developers/extend/apps/layout/front-components">
`defineFrontComponent` — isolierte React-Komponenten, die innerhalb von Twenty gerendert werden.
</Card>
<Card title="Befehlsmenü-Einträge" icon="terminal" href="/l/de/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — Frontend-Komponenten als Cmd+K-Einträge und Schnellaktionen registrieren.
</Card>
</CardGroup>
## Wo die App erscheint
| Oberfläche | Was es steuert | Entität |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Seitenleiste** | Ein benutzerdefinierter Eintrag, der auf eine gespeicherte Ansicht oder eine externe URL verweist | `defineNavigationMenuItem` |
| **Datensatzliste** | Eine gespeicherte Konfiguration für ein Objekt – sichtbare Spalten, Reihenfolge, Filter, Gruppen | `defineView` |
| **Detailseite des Datensatzes** | Die Registerkarten und Widgets auf einer Datensatzseite (für Ihr eigenes Objekt oder ein Standardobjekt) | `definePageLayout`, `definePageLayoutTab` |
| **Innerhalb eines der oben genannten Bereiche** | Ein benutzerdefiniertes React-Widget – Schaltflächen, Formulare, Dashboards, Integrationen | `defineFrontComponent` |
| **App-Einstellungen** | Ein benutzerdefinierter Konfigurationsabschnitt im Tab "Einstellungen" der App, anstelle der standardmäßigen Variablen-Benutzeroberfläche | `defineSettingsFrontComponent` |
| **Befehlsmenü (Cmd+K)** | Eine angeheftete Schnellaktion oder ein versteckter Befehl | `defineCommandMenuItem` |
Frontend-Komponenten laufen in einem isolierten Web Worker unter Verwendung von Remote DOM – sie werden nativ auf der Seite gerendert (nicht in einem iframe), können aber die Hostseite oder das DOM nicht direkt erreichen. Die Kommunikation mit Twenty erfolgt über eine Message-Passing-Host-API.
Sie befinden sich noch in aktiver Entwicklung: Die Sandbox implementiert ein partielles DOM, daher können fortgeschrittene Anwendungsfälle fehlschlagen. Siehe [Aktuelle Einschränkungen](/l/de/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Seitenlayouts
description: Passen Sie Detailseiten von Datensätzen an – Tabs, Widgets und die Stellen, an denen Frontend-Komponenten gerendert werden – mithilfe von `definePageLayout` und `definePageLayoutTab`.
icon: table-columns
---
Ein **Seitenlayout** steuert, wie die Detailseite eines Datensatzes angeordnet ist: welche Tabs angezeigt werden und welche Widgets sie enthalten. Verwenden Sie `definePageLayout()`, um ein Layout für ein Objekt zu deklarieren, das Sie besitzen, oder `definePageLayoutTab()`, um einen einzelnen Tab zu einem Layout hinzuzufügen, das bereits existiert (Ihr eigenes oder ein standardmäßiges Twenty-Layout).
| Anwendungsfall | Entität |
| ----------------------------------------------------------------------------------------------- | --------------------- |
| Definieren Sie das gesamte Layout für die Datensatzseite eines Objekts, das Sie besitzen | `definePageLayout` |
| Fügen Sie einem vorhandenen Layout einen Tab hinzu (Ihr eigenes Objekt oder ein Standardlayout) | `definePageLayoutTab` |
## definePageLayout
Verwenden Sie dies, wenn Sie die gesamte Detailseite besitzen – typischerweise für ein benutzerdefiniertes Objekt, das Sie selbst definiert haben.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
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.VERTICAL_LIST,
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 eines von `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` oder `'STANDALONE_PAGE'`. Verwenden Sie `'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`: `VERTICAL_LIST` für Datensatzseiten und eigenständige Seiten, `GRID` für Dashboards und `CANVAS` für ein einzelnes Widget, das den Ansichtsbereich des Tabs ausfüllen soll. Ein `VERTICAL_LIST`-Tab ordnet Widgets vertikal an. Integrierte Widgets, die ihr eigenes Scrollen verwalten, wie etwa Timelines, Dateien, Notizen, Aufgaben und Workflows, füllen einen Ansichtsbereich aus; Felder, Front-Komponenten, Diagramme und andere Widgets mit `fit-content` werden mit ihrer Inhalts- oder konfigurierten Höhe dargestellt. Ein `GRID`-Tab ordnet seine Widgets immer als Karten in einem Raster mit 12 Spalten an. Ein `CANVAS`-Widget hat keine explizite Position; wenn ein Canvas-Tab mehrere Widgets enthält, werden sie in ihrer Inhaltshöhe gerendert, anstatt den Viewport auszufüllen.
* Setze `layoutMode` explizit. Wenn du es weglässt, erhältst du `VERTICAL_LIST` auf einer `STANDALONE_PAGE` und `GRID` überall sonst, was auf einer Datensatzseite selten gewünscht ist.
* Jedes `widget` innerhalb eines Tabs kann eine [Frontend-Komponente](/l/de/developers/extend/apps/layout/front-components), eine Relationenliste oder andere eingebaute Widget-Typen rendern.
* Ein `FRONT_COMPONENT`-Widget kann `headerCommandMenuItemUniversalIdentifiers` auf ein geordnetes Array universeller Kennungen von Befehlsmenüelementen aus derselben App festlegen. Diese Aktionen werden als Symbolschaltflächen im Kopf der Widget-Karte angezeigt und behalten ihre Verfügbarkeits- und Berechtigungsprüfungen auf Befehlsebene bei. Kennungen müssen eindeutig sein und bei installierter App aufgelöst werden können.
* `position` auf Tabs steuert deren Reihenfolge. Verwenden Sie höhere Werte (z. B. 50), um benutzerdefinierte Tabs hinter den integrierten zu platzieren.
### Feld-Widgets
Ein `FIELD`-Widget rendert ein Feld des Datensatzes. Bei Relationsfeldern kann es auch eine Liste verwandter Datensätze einbetten:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` nimmt den universellen Bezeichner eines Feldes am Objekt des Layouts.
* `fieldDisplayMode` ist eines von `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` oder `'TABLE'`. `TABLE` bettet eine Ansicht ein, die die Datensätze eines 1:n-Relationsfeldes auflistet.
* `nestedRelationFieldMetadataId` ist optional und nimmt den universellen Bezeichner eines Eins-zu-viele-Beziehungsfelds auf dem Beziehungszielobjekt entgegen, um Datensätze aufzulisten, die zwei Beziehungssprünge entfernt sind (z. B. eine Company-Seite, die die Opportunities der Personen des Unternehmens auflistet, oder eine Person-Seite, die die Opportunities des Unternehmens der Person auflistet). Der erste Sprung kann ein Eins-zu-viele- oder ein Viele-zu-eins-Beziehungsfeld sein, der zweite muss Eins-zu-viele sein (Junction-Beziehungen werden nicht unterstützt), und es erfordert `fieldDisplayMode: 'TABLE'` — die Kombination mit jedem anderen Anzeigemodus ist ein Validierungsfehler, da ein verschachteltes Widget immer als eingebettete Ansicht gerendert wird.
## definePageLayoutTab
Verwenden Sie dies, wenn Sie nur einen Tab zu einem vorhandenen Layout **hinzufügen** möchten – zum Beispiel einen Analytics-Tab auf der standardmäßigen Company-Seite oder einen KI-Zusammenfassungs-Tab, der an das Layout Ihres eigenen Objekts angehängt ist.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Hauptpunkte
* `pageLayoutUniversalIdentifier` ist **erforderlich** und muss auf ein Seitenlayout verweisen, das zum Installationszeitpunkt bereits existiert – entweder ein standardmäßiges Twenty-Layout oder eines, das von Ihrer eigenen App definiert wurde. App-übergreifende Verweise auf Layouts, die einer anderen installierten App gehören, werden derzeit nicht unterstützt. Wenn das übergeordnete Layout fehlt, schlägt die Installation mit einem eindeutigen Validierungsfehler fehl.
* Für Standard-Twenty-Layouts importieren Sie die Bezeichner aus `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Jeder Layout-Eintrag stellt außerdem seine `tabs` und deren `widgets` zur Verfügung, sodass Sie auf jede Ebene verweisen können:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Eine kurze Alias-Variable `STANDARD_PAGE_LAYOUT` ist ebenfalls verfügbar:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` sind ausschließlich auf diesen Tab beschränkt – sie verweisen auf [Frontend-Komponenten](/l/de/developers/extend/apps/layout/front-components), Ansichten usw., genau wie Widgets, die inline in `definePageLayout` definiert sind.
* `position` steuert die Reihenfolge im Zielseitenlayout relativ zu den vorhandenen Registerkarten. Wählen Sie einen Wert, der Ihre Registerkarte relativ zu integrierten Registerkarten an die gewünschte Position bringt.
* Verwenden Sie dies anstelle von `definePageLayout`, wenn Sie einem vorhandenen Layout nur etwas hinzufügen möchten. Verwenden Sie `definePageLayout`, wenn Sie das gesamte Layout besitzen.

View File

@@ -0,0 +1,113 @@
---
title: Ansichten
description: Stellen Sie vorkonfigurierte gespeicherte Ansichten bereit – Spaltenreihenfolge, Filter, Gruppen – für Objekte in Ihrer App.
icon: list
---
Eine **Ansicht** ist eine gespeicherte Konfiguration dafür, wie Datensätze eines Objekts angezeigt werden: welche Felder erscheinen, in welcher Reihenfolge, ob sie sichtbar sind und welche Filter oder Gruppen angewendet werden. Verwenden Sie `defineView()`, um vorkonfigurierte Ansichten mit Ihrer App auszuliefern.
Jedes Objekt erhält automatisch seine Hauptlistenansicht (die "Alle ..."-Tabelle, die ein `OBJECT`-Navigationselement öffnet), die vom Server bereitgestellt wird – einschließlich der Objekte, die Ihre App erstellt, mit einer Spalte pro Feld, das Ihre App definiert. Ansichten, die Sie mit `defineView()` definieren, sind stets **zusätzliche** Ansichten neben der Standardansicht.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
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',
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. Es kann sich um ein von Ihnen definiertes benutzerdefiniertes Objekt oder ein Standardobjekt von Twenty handeln.
* Die Hauptlistenansicht des Objekts gehört dem Server: `key` ist veraltet und wird ignoriert, daher kann eine Manifest-Ansicht sie niemals beanspruchen. Liefern Sie ein `VIEW`-Navigationselement aus, wenn Ihre Ansicht nur einen Klick entfernt in der Seitenleiste sein soll.
* `fields` steuert, welche Spalten erscheinen und in welcher Reihenfolge. Jedes Feld referenziert einen `fieldMetadataUniversalIdentifier`. Informationen zum Verweisen auf ein automatisch erstelltes Systemfeld wie `createdAt` finden Sie unter [Systemmetadaten ansteuern](/l/de/developers/extend/apps/data/system-fields).
* Für erweiterte Konfigurationen können Sie außerdem `filters`, `filterGroups`, `sorts`, `groups` und `fieldGroups` deklarieren.
* `position` steuert die Reihenfolge, wenn mehrere Ansichten für dasselbe Objekt existieren.
## Optionale Eigenschaften
| Eigenschaft | Werte | Beschreibung |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `ViewType.TABLE` (Standard), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Wie Datensätze angeordnet werden. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` und `CALENDAR_WIDGET` existieren ebenfalls, werden aber intern von Page-Layout-Widgets verwendet.) |
| `visibility` | `ViewVisibility.WORKSPACE` (Standard), `ViewVisibility.UNLISTED` | Ob die Ansicht für den gesamten Workspace aufgelistet oder in Auswahlelementen verborgen ist. |
| `openRecordIn` | veraltet | Nicht mehr ausgelesen: Wo Datensätze geöffnet werden, ist jetzt eine Eigenschaft des [Objekts](/l/de/developers/extend/apps/data/objects) (`openRecordIn` bei `defineObject()`) und fällt auf die jeweilige Präferenz des einzelnen Mitglieds zurück. |
| `sortierungen` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Standard-Sortierreihenfolge. |
| `isCompact` | `boolean` | Kompakte Zeilenanzeige. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Datensätze (z. B. Kanban-Spalten) nach einem Feld gruppieren. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Aggregationen und Größen von Kanban-Spalten. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Kalenderansichten: Layout, das Datumsfeld, das die Position der Datensätze bestimmt, und ein optionales Enddatumsfeld. |
Alle oben genannten Enums werden aus `twenty-sdk/define` exportiert.
## Filter
Eine Ansicht kann mit vorab angewendeten Filtern ausgeliefert werden. Jeder Filter hat drei Koordinaten: das **Feld**, das gefiltert wird, der **Operand** (wie verglichen wird) und der **Wert** (womit verglichen wird). Alle drei müssen übereinstimmen — die Verwendung eines Operanden, der nicht auf einen Feldtyp anwendbar ist, wird bei der Synchronisierung zurückgewiesen.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Unterstützte Operanden pro Feldtyp
| Feldtyp | Unterstützte Operanden |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden – `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
### Wertstruktur je Operand
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber die erwartete Struktur hängt vom Operanden ab:
| Operandenfamilie | Wertstruktur | Beispiel |
| -------------------------------------------------------- | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` bei `SELECT` | Array von Optionsschlüsseln (Strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei `MULTI_SELECT` | Array von Optionsschlüsseln (Strings) | `['TAG_A']` |
| `IS`, `IS_NOT` bei `RELATION` | Array von Datensatz-IDs (UUIDs) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei textähnlichen Feldern | Zeichenkette | `'acme'` |
| `IS`, `IS_NOT` bei `NUMBER` | String (der Wert) | `'5'` |
| `IS` bei `RATING` / `UUID` | String (der Wert) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (der Grenzwert) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` bei `DATE` / `DATE_TIME` | ISO-8601-String | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | leerer String | `''` |
| `IS` bei `BOOLEAN` | `'true'` oder `'false'` | `'true'` |
## Wie Ansichten in der UI angezeigt werden
Eine Ansicht für sich ist aus der Seitenleiste nicht erreichbar. Damit sie dort erscheint, verknüpfen Sie sie mit einem [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items) des Typs `VIEW`, der auf den `universalIdentifier` der Ansicht zeigt. Das ist das kanonische Muster: Für jedes benutzerdefinierte Objekt wird bei der Erstellung automatisch eine Standard-Indexansicht generiert, sodass du nur den Sidebar-Eintrag ausliefern musst, der sie öffnet.

View File

@@ -0,0 +1,197 @@
---
title: Hintergrundjobs
description: Übergib lang laufende oder rate-limitierte Arbeiten an die Twenty-Worker, indem du einen weiteren Lauf einer Logikfunktion in die Warteschlange stellst, statt alles inline auszuführen.
icon: layer-group
---
Ein Logikfunktionslauf ist durch seine `timeoutSeconds` begrenzt (maximal 900 Sekunden). Alles, was in diesem Zeitfenster nicht abgeschlossen werden kann – ein vollständiger Re-Sync, ein Fan-out pro Datensatz, eine Drittanbieter-API, die dich rate-limitiert – muss in kleinere Läufe aufgeteilt werden.
`enqueueJobs` macht genau das: Es weist die Twenty-Worker an, eine der Logikfunktionen deiner App später, einmal pro Payload, jeweils in einem eigenen Prozess mit eigenem Timeout-Budget auszuführen. Der Aufrufer kehrt sofort zurück.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Läufe in die Warteschlange stellen
Importiere `enqueueJobs` aus `twenty-sdk/logic-function`, gib den `universalIdentifier` der auszuführenden Logikfunktion an und übergib zum Einreihen eine Payload pro Lauf.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Jeder Lauf erhält seine Payload als Handler-Argument, genau wie jeder andere Trigger. Das Ziel muss zur **gleichen Applikation** gehören wie der Aufrufer — das Einreihen der Funktion einer anderen App wird mit `Logic function not found` abgelehnt und nichts wird eingereiht. Ein einzelner Aufruf akzeptiert bis zu `200` Payloads.
<Note>
`enqueueJobs` kehrt zurück, sobald die Jobs akzeptiert wurden, nicht erst, wenn sie ausgeführt wurden. Sie gibt nicht die Ergebnisse der Ziele zurück — lasse jedes Ziel das, was es erzeugt, in den [Key-Value-Speicher](/l/de/developers/extend/apps/logic/key-value-store) oder in einen Workspace-Datensatz schreiben, wenn du es wieder auslesen musst.
</Note>
<Note>
Der ältere Helper `enqueueJob`, der pro Aufruf einen einzelnen Job einreiht, ist veraltet. Verwende stattdessen `enqueueJobs` mit einer `payloads`-Liste mit einem Element.
</Note>
## Job-Optionen
Optionen gelten für jeden Lauf im Batch.
| Option | Standard | Bereich | Was es tut |
| ------------ | -------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Zusätzliche Warteschlangenversuche insgesamt. Von der Anwendung angeforderte Wiederholungsversuche sind auf `3` begrenzt. Erhöhe diesen Wert nur für Handler, die sicher zweimal ausgeführt werden können. |
| `delayMs` | `0` | `0`–`604800000` (7 Tage) | So lange warten, bevor die Läufe zur Ausführung anstehen. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**Priorität ist noch nicht konfigurierbar.** Eingereihte Jobs laufen immer mit der niedrigsten Priorität, sodass Plattformarbeit niemals hinter Applikationsjobs verzögert wird. Steuerung der Priorität kommt bald.
</Note>
Der eingereihte Lauf erbt den agierenden Benutzer der Funktion, die ihn eingereiht hat, sodass er mit denselben Berechtigungen agiert.
## Wiederholen eines vorübergehenden Fehlers
Twenty wiederholt nicht jede Ausnahme aus dem Anwendungscode. Ein gewöhnlicher ausgelöster Fehler wird als dauerhafter Fehler behandelt. Bei einem vorübergehenden Fehler kann eine eingereihte Logikfunktion durch Auslösen von `RetryableLogicFunctionError` bis zu drei Wiederholungen anfordern.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Löse `RetryableLogicFunctionError` nach Möglichkeit direkt aus. Wenn du sie erweiterst, ersetze ihren `name` nicht: Twenty erkennt den serialisierten Namen `RetryableLogicFunctionError` in allen Laufzeitumgebungen.
`retryCount` ist bei der initialen Ausführung `0` und erhöht sich nur, wenn der Anwendungscode eine Wiederholung anfordert. `maxRetries` beträgt höchstens `3` und kann geringer sein, wenn der Job in der Warteschlange ein niedrigeres Gesamtlimit für Wiederholungen hat. Plattformfehler erhöhen `retryCount` nicht, verbrauchen jedoch weiterhin das Gesamtsicherheitsbudget der Warteschlange.
Die Warteschlange verzögert Wiederholungsversuche mit exponentiellem Backoff und Jitter. Die genaue Verzögerung wird absichtlich nicht garantiert, daher sollte der Anwendungscode nicht davon abhängen, dass eine Wiederholung zu einem genauen Zeitpunkt erfolgt. Sobald `maxRetries` erreicht ist, wird ein weiterer `RetryableLogicFunctionError` ohne weitere Ausführung als endgültiger Anwendungsfehler erfasst.
<Warning>
Wiederholungen führen den gesamten Handler erneut aus und können erfolgen, nachdem einige Nebenwirkungen erfolgreich waren. Mache den Handler idempotent, bevor du Wiederholungen anforderst.
</Warning>
## Verwendung: eine lange Synchronisation seitenweise verarbeiten
Die klassische Form ist eine Funktion, die *sich selbst* mit dem nächsten Cursor in die Warteschlange stellt. Jeder Lauf erledigt eine Seite Arbeit deutlich innerhalb seines eigenen Timeouts, und die Kette endet, wenn nichts mehr übrig ist.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Aufteilung pro Datensatz
Wenn die Arbeit natürlich pro Element anfällt, reihe in einem einzigen Aufruf einen Job pro Element ein und lasse die Worker sie parallel verarbeiten, anstatt inline zu iterieren.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Bewährte Vorgehensweisen für langlaufende Arbeit
Zwei Regeln decken fast jeden langen Job ab: **rekursiv statt in einer Schleife arbeiten** und **pro Lauf einen begrenzten Chunk verarbeiten**.
Ein Lauf, der versucht, alles zu erledigen, ist der Fehlerfall — er erreicht das Timeout, und bei einem Retry startet er den gesamten Vorgang erneut von vorn. Bestimme stattdessen die Größe eines Chunks so, dass er bequem innerhalb von `timeoutSeconds` fertig wird, speichere deine Position und reihe den nächsten Lauf ein.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
Warum das funktioniert:
* **Lege die Chunk-Größe anhand des langsamsten Elements fest, nicht anhand des Durchschnitts.** `CHUNK_SIZE × Worst-Case-Elementzeit` muss mit ausreichendem Puffer in `timeoutSeconds` passen, sonst geht das Ende eines Chunks verloren, wenn der Lauf abgeschnitten wird.
* **Mache die Abbruchbedingung explizit.** Rekursiere nur, solange ein voller Chunk zurückgekommen ist. Eine Kette, die nur bei „keine Ergebnisse“ anhält, läuft für immer weiter, wenn die Quelle unterwegs einmal eine verkürzte Seite zurückgibt.
* **Persistiere den Fortschritt, bevor du den nächsten Lauf einreihst,** damit ein fehlgeschlagener Link beim letzten abgeschlossenen Chunk und nicht am Anfang neu startet.
* **Halte jeden Chunk idempotent.** Die erneute Verarbeitung eines Chunks nach einem Retry darf nicht zu doppelten Schreibvorgängen führen — führe Schlüsselschreibvorgänge auf dem Datensatz oder der externen ID aus, die du verarbeitest.
* **Bevorzuge eine verkettete Chunk-Verarbeitung gegenüber einem riesigen Fan-out**, wenn die Arbeit auf einen rate-limitierten Drittanbieter trifft: Eine Kette mit `delayMs` taktet sich selbst, wohingegen Tausende auf einmal eingereihte Jobs sofort zur Ausführung anstehen.

View File

@@ -0,0 +1,269 @@
---
title: Verbindungen
description: Ermöglichen Sie Ihrer App, im Namen eines Benutzers über OAuth in Diensten von Drittanbietern zu handeln.
icon: plug
---
Verbindungen sind Anmeldedaten, die ein Benutzer für einen externen Dienst besitzt (Linear, GitHub, Slack, ...). Ihre App legt fest, **wie** diese Anmeldedaten bezogen werden — ein **Verbindungsanbieter** — und verwendet sie zur Laufzeit, um authentifizierte Aufrufe an die Drittanbieter-API zu tätigen.
Derzeit wird nur OAuth 2.0 unterstützt. Zukünftige Anmeldedatentypen (Personal Access Tokens, API-Schlüssel, Basic Auth) werden in dieselbe Oberfläche integriert — Apps, die bereits `defineConnectionProvider({ type: 'oauth', ... })` müssen nicht migriert werden.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Legen Sie fest, wie die Verbindungen Ihrer App bezogen werden">
Ein Verbindungsanbieter beschreibt den OAuth-Handshake, den Ihre App benötigt. Der Benutzer klickt in den Einstellungen Ihrer App auf "Verbindung hinzufügen", schließt den Zustimmungsbildschirm des Anbieters ab, und in seinem Arbeitsbereich wird eine `ConnectedAccount`-Zeile erstellt.
Eine funktionierende Einrichtung benötigt **zwei Dateien** — den Verbindungsanbieter und eine passende `serverVariables`-Deklaration in `defineApplication`, die die OAuth-Client-Anmeldedaten enthält.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Hauptpunkte:
* `name` ist die eindeutige Bezeichner-Zeichenfolge, die in `listConnections({ providerName })` verwendet wird (kebab-case, muss `^[a-z][a-z0-9-]*$` entsprechen).
* `displayName` wird im Einstellungs-Tab der jeweiligen App und in der KI-Toolliste angezeigt.
* `clientIdVariable` / `clientSecretVariable` sind **Namen**, keine Werte — sie müssen den in `defineApplication.serverVariables` deklarierten Schlüsseln entsprechen. Die tatsächlichen `client_id` und `client_secret` werden vom Serveradministrator über die App-Registrierungsoberfläche eingegeben und niemals in Ihr Repository eingecheckt.
* Verwenden Sie `serverVariables` (nicht `applicationVariables`) — OAuth-Anmeldedaten gelten serverweit und es gibt eine OAuth-App pro Twenty-Server.
* Solange beide `serverVariables` nicht ausgefüllt sind, zeigt der Einstellungs-Tab pro App den Hinweis "Benötigt Server-Admin" an und der Button "Verbindung hinzufügen" ist deaktiviert.
* `type: 'oauth'` ist derzeit der einzige unterstützte Wert. Der Diskriminator ist vorwärtskompatibel: zukünftige Typen (`'pat'`, `'api-key'`, ...) werden neue Unterkonfigurationsblöcke neben `oauth` hinzufügen.
Die OAuth-Callback-URL, die Ihr Anbieter auf die Whitelist setzen muss, lautet:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Beim Verbindungsaufbau eine Logikfunktion ausführen" description="Reagiere in dem Moment, in dem eine Verbindung hergestellt wird">
Einige Anbieter liefern dir beim Verbindungsaufbau Daten, die du persistieren musst, bevor die Verbindung nutzbar ist – das klassische Beispiel ist Slack, bei dem die OAuth-Antwort die `team_id` des Workspaces angibt, anhand derer eingehende Ereignisse zugeordnet werden. Setze `onConnectLogicFunction` so, dass auf eine Logikfunktion in derselben App (über ihren `universalIdentifier`) verwiesen wird; sie wird direkt nach dem Erstellen des `ConnectedAccount` ausgeführt.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
Der Hook wird **asynchron im verbindenden Workspace** ausgeführt (er wird in eine Warteschlange gestellt, nicht abgewartet), sodass ein langsamer oder fehlerhafter Hook niemals den OAuth-Callback blockiert oder unterbricht – mache ihn idempotent und lass ihn seine eigenen Wiederholungen handhaben. Der Handler erhält:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Verwenden Sie von dort `getConnection(connectedAccountId)`, um das aktuelle Zugriffstoken auszulesen und die API des Anbieters aufzurufen (z. B. Slack `auth.test`) oder eine Zuordnung im [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store) zu persistieren.
</Accordion>
<Accordion title="Beim Trennen der Verbindung eine Logikfunktion ausführen" description="Bereinigen, wenn eine Verbindung entfernt wird">
Alles, was eine App zum Zeitpunkt des Verbindungsaufbaus beansprucht, muss freigegeben werden, wenn die Verbindung endet. Eine Slack-Integration, die zum Beispiel beim Verbindungsaufbau eine `team_id` für sich beansprucht, muss diesen Anspruch wieder freigeben, damit ein anderer Arbeitsbereich dasselbe Slack-Team verbinden kann. Setze `onDisconnectLogicFunction` so, dass auf eine Logikfunktion in derselben App verwiesen wird; sie wird direkt nach dem Löschen des `ConnectedAccount` ausgeführt.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
Wie der On-Connect-Hook läuft er **asynchron im Workspace, in dem die Trennung erfolgt**, und blockiert die Trennung nie. Der Handler erhält dieselbe Payload-Struktur:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Der `ConnectedAccount` ist bereits entfernt, wenn der Hook ausgeführt wird, daher wird `getConnection(connectedAccountId)` nicht mehr aufgelöst. Alles, was die Bereinigung benötigt (eine `team_id`, eine externe Abonnement-ID), muss zum Zeitpunkt des Verbindungsaufbaus im [Key-Value-Store](/l/de/developers/extend/apps/logic/key-value-store) gespeichert worden sein, indiziert nach `connectedAccountId`.
Der Hook wird ausgelöst, wenn eine Verbindung eigenständig entfernt wird. Die Deinstallation der App entfernt ihre Verbindungen hingegen über eine Datenbankkaskade, sodass der Hook dort nicht ausgeführt wird. Deklariere eine `uninstallLogicFunction` in `defineApplication` für diesen Pfad: Sie wird ausgeführt, bevor die Metadaten der App gelöscht werden, sodass sie weiterhin `listConnections` aufrufen und den verbleibenden Rest bereinigen kann.
</Accordion>
<Accordion title="listConnections / getConnection" description="Verbindungen aus einer Logikfunktion verwenden">
Innerhalb eines Logikfunktions-Handlers gibt `listConnections({ providerName })` die `ConnectedAccount`-Zeilen dieser App für den angegebenen Anbieter zurück, mit aktualisierten Zugriffstoken.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Jede Verbindung hat:
| Feld | Beschreibung |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Eindeutige Zeilen-ID; an `getConnection(id)` übergeben, um eine einzelne Verbindung erneut abzurufen |
| `visibility` | `'user'` (privat für ein Mitglied des Arbeitsbereichs) oder `'workspace'` (mit allen Mitgliedern geteilt) |
| `scopes` | Vom Upstream-Anbieter gewährte OAuth-Berechtigungen (unabhängig von `visibility` — diese sind nicht miteinander verknüpft) |
| `userWorkspaceId` | Die userWorkspace-ID des Eigentümers — nützlich, um "die Verbindung des anfragenden Benutzers" in HTTP-Routen-Triggern auszuwählen |
| `accessToken` | Frisches OAuth-Zugriffstoken (wird bei Ablauf automatisch erneuert) |
| `name` | Anzeigename der Verbindung (automatisch beim OAuth-Callback abgeleitet, vom Benutzer umbenennbar) |
| 'handle' | Die E-Mail des verbundenen Upstream-Accounts wurde bei jeder erneuten Verbindung aktualisiert; aufgelöst von der OIDC `id_token` gab beim Token-Austausch zurück, zurück auf die Verbindung von zwanzig Benutzer-E-Mail, wenn der Provider keine zurückgibt |
| `authFailedAt` | Gesetzt, wenn die jüngste Aktualisierung fehlgeschlagen ist; der Benutzer muss die Verbindung erneut herstellen |
Hauptpunkte:
* Übergeben Sie `{ providerName }`, um nach Anbieter zu filtern; lassen Sie es weg, um alle Verbindungen dieser App über alle Anbieter hinweg zu erhalten.
* Der Server aktualisiert das Zugriffstoken vor der Rückgabe transparent. Ihr Handler sieht stets ein verwendbares Token (oder `authFailedAt` ist gesetzt).
* `getConnection(id)` ist das Pendant für eine einzelne Zeile.
</Accordion>
<Accordion title="Sichtbarkeit: pro Benutzer vs. im Arbeitsbereich geteilt" description="Wie Benutzer zwischen privaten und geteilten Anmeldedaten wählen">
Wenn ein Benutzer auf "Verbindung hinzufügen" klickt, wird er aufgefordert, eine Sichtbarkeit auszuwählen:
* **Nur für mich** — die Anmeldedaten sind für den sich verbindenden Benutzer privat. Jede Logikfunktion, die in seinem/ihrem Auftrag aufgerufen wird (HTTP-Routen-Trigger mit `isAuthRequired: true`), sieht sie; Cron-Trigger und Datenbankereignisse nicht.
* **Im Arbeitsbereich geteilt** — jedes Arbeitsbereichsmitglied kann die Anmeldedaten verwenden. Cron-/Datenbank-Trigger sehen sie ebenfalls, da sie keinen anfragenden Benutzer haben.
Verwenden Sie für jeden Handler die richtige Option:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Mehrere Verbindungen pro (Benutzer, Anbieter) sind erlaubt, sodass derselbe Benutzer "Persönliches Linear" und "Arbeits-Linear" nebeneinander haben kann.
</Accordion>
<Accordion title="Einmalige Anbietereinrichtung" description="Registrieren Sie Ihre OAuth-App beim Drittanbieterdienst">
Für jeden Verbindungsanbieter muss der Serveradministrator zunächst eine OAuth-App beim Drittanbieter registrieren.
1. Gehen Sie zu den Entwickler-Einstellungen des Anbieters (z. B. https://linear.app/settings/api/applications/new).
2. Setzen Sie die **Redirect-URI** auf `\<SERVER_URL>/auth/apps/callback`.
3. Kopieren Sie die generierte **Client ID** und das **Client Secret**.
4. Öffnen Sie die installierte App in Twenty als Serveradministrator → setzen Sie die Werte in den entsprechenden `serverVariables`.
5. Mitglieder des Arbeitsbereichs können dann Verbindungen im **Verbindungen**-Abschnitt der jeweiligen App hinzufügen.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Key-Value-Store
description: Bewahren Sie Zwischenergebnisse auf, zwischenspeichern Sie Daten und teilen Sie den Zustand zwischen Ausführungen von Logikfunktionen mit dem integrierten Key-Value-Speicher der Anwendung.
icon: database
---
Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich **zwischen Durchläufen etwas merken müssen** (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es im integrierten Key-Value-Speicher.
Jede Anwendung erhält ihren eigenen isolierten Namespace: Einträge werden durch die authentifizierte App indiziert, sodass Ihre Schlüssel niemals mit denen einer anderen Anwendung kollidieren können – oder von ihr gelesen werden können.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Abrufen, Setzen, Löschen
Importieren Sie `kv` aus `twenty-sdk/logic-function`. Werte können beliebige JSON-serialisierbare Werte sein.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Geltungsbereiche
Jeder Eintrag hat einen Geltungsbereich, der bei jedem Aufruf als Option übergeben wird. Der Standardwert ist `WORKSPACE`.
* **`WORKSPACE`** (Standard) – der Eintrag ist nur für die aktuelle Workspace-Installation Ihrer App sichtbar. Jeder Workspace, der die App installiert, erhält seinen eigenen unabhängigen Satz von Schlüsseln. Das ist genau das, was Sie für Caches, Cursor und workspacespezifischen Zustand benötigen.
* **`SERVER`** – der Eintrag wird über **alle Installationen** Ihrer App auf dem Server hinweg geteilt. Server-Einträge verhalten sich wie **Claims**: Der gespeicherte Wert ist immer die workspaceId, die den Schlüssel beansprucht hat (lassen Sie `value` bei `set` weg, um den Schlüssel für den aktuellen Workspace zu beanspruchen), und nur dieser Workspace kann ihn überschreiben oder löschen. Jede Installation kann den Eintrag lesen.
Server-Claims existieren für Cross-Workspace-Routing. Ein [Server-Route-Resolver](/l/de/developers/extend/apps/logic/logic-functions#server-route-trigger) läuft im Workspace des Anwendungsregistrierungs-Inhabers, aber ein eingehender Webhook übermittelt in der Regel nur eine externe Konto-ID – nicht eine Twenty workspaceId. Lassen Sie jeden Workspace seine externe ID zum Verbindungszeitpunkt beanspruchen und lösen Sie sie dann in der Route auf:
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Da ein Server-Schlüssel nur für den eigenen Workspace des Aufrufers beansprucht und niemals von einem anderen überschrieben werden kann, kann ein Workspace kein Mapping kapern, das jemand anderem gehört. `kv.set` löst eine Exception aus, wenn der Schlüssel bereits von einem anderen Workspace beansprucht wurde.
## Verwenden Sie ihn: einen teuren Aufruf zwischenspeichern
Eine typische Verwendung ist das Zwischenspeichern einer langsamen oder ratelimitierten Antwort eines Drittanbieters, sodass wiederholte Durchläufe sie wiederverwenden, anstatt jedes Mal die Kosten zu tragen.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Muster & Tipps
* **Namespacing.** Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten – `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Ablauf (TTL).** Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel im Wert (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder löschen Sie veraltete Schlüssel aus einer [cron-getriggerten Funktion](/l/de/developers/extend/apps/logic/logic-functions).
* **Was gespeichert wird.** Jeder JSON-serialisierbare Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein `FILES`-Feld und [`uploadFile`](/l/de/developers/extend/apps/logic/logic-functions#uploading-files).
* **Sichtbarkeit.** Einträge liegen in der Instanzdatenbank, nicht als Workspace-Datensätze – sie erscheinen nie in der Workspace-UI, sind kein Teil des Datenmodells Ihrer App und benötigen keine Rollen- oder Objektberechtigungen.
## Alternative: ein abfragbares Store-Objekt
Der integrierte Store ist bewusst intransparent: Einträge sind keine Datensätze, daher können Sie sie nicht in der UI durchsuchen, nicht mit anderen Objekten verknüpfen oder mit Record-Abfragen filtern. Wenn Sie irgendetwas davon benötigen – etwa ein sichtbares Sync-Log oder pro-Datensatz-Zustand – definieren Sie stattdessen ein kleines **technisches Objekt** mit einem eindeutigen `key`-Feld und einem `RAW_JSON`-`value`-Feld und fragen Sie es über den [typisierten API-Client](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) ab. Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die Referenz zu `defineObject` und [Daten → Eindeutige Indizes](/l/de/developers/extend/apps/data/overview#unique-indexes) zur Durchsetzung der Schlüssel-Eindeutigkeit.
* **Geltungsbereich für einen Datensatz.** Fügen Sie vom Store-Objekt eine [Relation](/l/de/developers/extend/apps/data/relations) zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren.
* **Sichtbarkeit & Berechtigungen.** Zeilen befinden sich wie jeder andere Datensatz in der Workspace-Datenbank, sind also über die API abfragbar und respektieren die [Rolle](/l/de/developers/extend/apps/config/roles) Ihrer App. Um den Store aus dem Haupt-UI herauszuhalten, führen Sie ihn nicht in Ihrem [Navigationsmenü](/l/de/developers/extend/apps/layout/navigation-menu-items) auf.
<Note>
Im Gegensatz zum integrierten Store ist ein benutzerdefiniertes Objekt immer auf einen Workspace begrenzt – es kann keine Einträge über Installationen hinweg teilen, so wie es `SERVER`-Schlüssel tun.
</Note>

View File

@@ -0,0 +1,908 @@
---
title: Logikfunktionen
description: Definieren Sie serverseitige TypeScript-Funktionen mit HTTP-, cron- und Datenbankereignis-Triggern.
icon: bolt
---
Logikfunktionen sind serverseitige TypeScript-Funktionen, die auf der Twenty-Plattform ausgeführt werden. Sie können durch HTTP-Anfragen, cron-Zeitpläne oder Datenbankereignisse ausgelöst werden — und außerdem als Tools für KI-Agenten bereitgestellt werden.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Logikfunktionen und deren Trigger definieren">
Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Verfügbare Trigger-Typen:
* **httpRoute**: Stellt Ihre Funktion über einen HTTP-Pfad und eine Methode bereit. Im Anwendungscode den Routenpfad bei Verwendung von `RestApiClient` mit dem Präfix `/s/` versehen; die bereitgestellte URL verwendet als Basis die injizierte `TWENTY_FUNCTIONS_URL` (oder `\<server-url>/s`, wenn diese nicht gesetzt ist).
<Note>
Um eine routenausgelöste Logikfunktion von einer (headless) Front-Komponente aus aufzurufen, siehe [Aufrufen einer Logikfunktion](/l/de/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **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.*`
* **serverRoute**: Stellt eine einzelne, registrierungsbezogene HTTP-Route bereit. Eine **Resolver**-Funktion (deklariert mit `serverRouteTriggerSettings`) läuft im Owner-Workspace und gibt entweder eine synchrone `Response` zurück oder den Ziel-Workspace UND die Ziel-Logikfunktion, die in die Queue eingereiht werden soll; auf dem Enqueue-Pfad bestätigt die Plattform mit `202` und führt dieses **Ziel** in der Worker-Queue aus. Siehe [Server-Route-Trigger](#server-route-trigger).
<Note>
Sie können eine Funktion auch manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Sie können Protokolle mit folgendem Befehl ansehen:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Routen-Trigger-Payload
Wenn ein Route-Trigger Ihre Logikfunktion aufruft, erhält sie ein `RoutePayload`-Objekt, das dem [AWS-HTTP-API-v2-Format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) folgt.
Importieren Sie den Typ `RoutePayload` aus `twenty-sdk/logic-function`:
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Der Typ `RoutePayload` hat die folgende Struktur:
| Eigenschaft | Typ | Beschreibung | Beispiel |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) | siehe Abschnitt unten |
| `queryStringParameters` | `Record\<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' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Ursprünglicher UTF-8-Request-Body vor dem JSON-Parsing. Nützlich zur Verifizierung von Webhook-Signaturen im HMAC-Stil (z. B. GitHubs `X-Hub-Signature-256`, Stripe). `undefined`, wenn die Laufzeitumgebung es nicht beibehalten hat. | |
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Rohpfad 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:
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
Greifen Sie in Ihrem Handler wie folgt auf die weitergeleiteten Header zu:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Header-Namen werden in Kleinbuchstaben normalisiert. Greifen Sie mit Schlüsseln in Kleinbuchstaben darauf zu (z. B. `event.headers['content-type']`).
</Note>
#### Benutzerdefinierte HTTP-Antwort
Standardmäßig sendet das Zurückgeben eines einfachen Werts aus Ihrem Handler diesen als `200`-Antwort zurück (JSON für Objekte, `text/plain` für Zeichenketten). Um den Statuscode und die Antwort-Header zu steuern, geben Sie eine `Response` aus `twenty-sdk/logic-function` zurück:
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
Aus Sicherheitsgründen sind Antwort-Header auf eine Allowlist beschränkt. Jeder Header, der nicht auf der Liste steht (z. B. `Set-Cookie`, CORS-Header wie `Access-Control-Allow-Origin` oder benutzerdefinierte `X-*`-Header), wird stillschweigend verworfen, bevor die Antwort gesendet wird. Die erlaubten Antwort-Header sind:
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
Der Statuscode muss ein gültiger HTTP-Statuscode sein (zwischen 100 und 599). Antwort-Header-Namen werden ohne Beachtung der Groß-/Kleinschreibung verglichen.
</Note>
#### Plattform-Fehlerantworten
Zusätzlich zu den eigenen Antworten Ihres Handlers beantwortet die Plattform Routenaufrufe in einigen Situationen direkt: `404`, wenn die Route oder Funktion nicht existiert, `403`, wenn die Anwendung gestoppt ist, `429`, wenn das Ausführungslimit überschritten wird, und `422`, wenn die Produktions-`dependencies` der Anwendung zu groß sind, um installiert zu werden — siehe [Größenbeschränkungen für dependencies](/l/de/developers/extend/apps/getting-started/project-structure#dependencies).
#### Server-Route-Trigger
`httpRouteTriggerSettings` stellt eine Funktion unter `/s/` bereit und ermittelt den Workspace aus dem Host der Anfrage — was funktioniert, wenn jeder Workspace seine eigene Domain hat. Drittanbieter hingegen liefern die Ereignisse jedes Mandanten an **eine** URL. Für diesen Fall verwende `serverRouteTriggerSettings`.
Der Trigger besteht aus zwei Teilen:
1. Eine **Resolver**-Logikfunktion – deklariert mit `serverRouteTriggerSettings` – läuft in deinem **Owner-Workspace** (dem Workspace, dem die Anwendungsregistrierung gehört). Sie inspiziert die eingehende Anfrage und gibt entweder Folgendes zurück:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — die Plattform reiht dieses Ziel im aufgelösten Workspace in die Queue ein und bestätigt mit `202 { queued: true }`, oder
* eine `Response` von `twenty-sdk/logic-function` — die Plattform gibt diese HTTP-Response **synchron** zurück und reiht **kein** Ziel in die Queue ein (verwende dies für Challenge-Handshakes wie Slack `url_verification`).
Der Resolver ist die einzige Autorisierungsstelle – die URL enthält nur den Bezeichner des Resolvers. **Dies ist der bevorzugte Ort, um Anfragesignaturen zu verifizieren**: Der Resolver läuft vor jeglicher Nebenwirkung, hat Zugriff auf den ursprünglichen `rawBody` und weitergeleitete Header und kann ablehnen, ohne jemals das Ziel zu berühren.
2. Eine **Target**-Logikfunktion – eine reguläre, Workspace-spezifische Logikfunktion – läuft dann im aufgelösten Workspace mit dem Payload, der vom Resolver zurückgegeben wurde (oder dem ursprünglichen Anfrage-Payload, falls der Resolver ihn nicht transformiert hat). Sein Rückgabewert wird vom HTTP-Aufrufer **nicht** beobachtet, wenn der Resolver den Enqueue-Pfad gewählt hat.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
Der Endpoint ist erreichbar unter:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
Der Bezeichner ist der `universalIdentifier` des Resolvers aus deinem Manifest. Registriere diese URL beim Provider.
**Beantworten eines Verifizierungs-`GET`.** Einige Anbieter verifizieren einen Endpunkt, bevor sie an ihn liefern, indem sie einen `GET` mit einer Challenge an dieselbe URL senden, an die sie später Ereignisse per `POST` senden werden — die WhatsApp Cloud API von Meta ist einer davon. Eine Serverroute beantwortet nur `POST`, sofern du nichts anderes angibst; deklariere daher beide Methoden:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
Die Challenge trifft in `event.queryStringParameters` ein, und durch das Zurückgeben einer `Response` wird sie dem Anbieter bei derselben Anfrage zurückgesendet. Ein String-Body wird als `text/plain` gesendet, was diese Anbieter erwarten:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` ersetzt den Standard, anstatt ihn zu ergänzen, sodass `['GET']` allein dazu führt, dass die Route `POST` ablehnt. Es werden nur `GET` und `POST` unterstützt. Lassen Sie es nicht festgelegt, sofern der Anbieter nicht das zweite Verb benötigt: Bei einer Route, die `GET` deklariert, wird ihr Resolver von jedem nicht authentifizierten Aufrufer ausgeführt, einschließlich der Crawler und Link-Unfurler, die unaufgefordert `GET` senden. Für alles, wofür die Plattform keine Methode hat, wird `405` zurückgegeben, ohne dass der Resolver jemals ausgeführt wird.
<Note>
**Die Anwendung muss beansprucht und im Arbeitsbereich ihres Besitzers installiert werden.** Da der Resolver im **Owner-Arbeitsbereich** ausgeführt wird (dem Arbeitsbereich, der die Anwendungsregistrierung besitzt), funktioniert ein Server-Route-Trigger nur, wenn die Anwendung *beansprucht* wurde – d. h. sie einen Owner-Arbeitsbereich hat – **und** diese Anwendung **im Owner-Arbeitsbereich installiert ist**. Solange beides nicht zutrifft, hat der Resolver keinen Ausführungsort, sodass die Route nicht ausgeführt werden kann. Eine Anwendung, die eine `serverRouteTriggerSettings`-Logikfunktion bereitstellt, kann daher nicht im Marketplace aufgeführt werden, bevor sie beansprucht und im Owner-Arbeitsbereich installiert wurde.
</Note>
**Resolver-Vertrag.** Der `LogicFunctionConfig`-Typ des SDK erzwingt dies zur Compile-Zeit: Sobald du `serverRouteTriggerSettings` setzt, ist dein Handler darauf beschränkt, entweder eine `Response` oder `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` zurückzugeben (oder ein `Promise` von einem der beiden). Auf dem Dispatch-Pfad muss die `workspaceId` auf einen Workspace verweisen, in dem die Zielfunktion installiert ist, andernfalls wird die Anfrage mit `404` abgelehnt. Ein Ergebnis, das keiner der beiden Formen entspricht – einschließlich eines Ergebnisses, dessen Bezeichner keine UUIDs sind – wird mit `502` abgelehnt.
| Feld | Typ | Notizen |
| ---------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | UUID des Workspaces, in dem das Ziel ausgeführt wird. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` der Logikfunktion, die in diesem Workspace aufgerufen werden soll. |
| `payload` | `object` (optional) | Falls gesetzt, ersetzt er den Request-Body, der an das Target gesendet wird. |
<Warning>
**Die Signaturverifizierung liegt in deiner Verantwortung – verifiziere im Resolver.** Die Plattform verifiziert Anfragesignaturen nicht. Der Resolver ist der empfohlene Ort dafür: Er läuft zuerst, mit Zugriff auf `event.rawBody` und die Header, die du in `forwardedRequestHeaders` aufgeführt hast, und ein geworfener Fehler (oder eine nicht passende `workspaceId`) stoppt das Dispatching, bevor das Target aufgerufen wird. Wenn du die Verifizierung stattdessen in das Target verschiebst, muss das Target darauf achten, `rawBody` und Header nicht zu verlieren – d. h. der Resolver darf keinen `payload` zurückgeben. Überprüfe immer **vor** jeglicher Nebenwirkung und verwende einen Vergleich in konstanter Zeit.
</Warning>
Für Anfragesignaturen signieren die meisten Provider mit HMAC-SHA256; die Teile, die sich unterscheiden, sind der Header-Name, die Digest-Codierung und der signierte Payload-String. Einige Beispiele:
| Anbieter | Weiterzuleitende Header | Signierte Zeichenkette | Digest |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | --------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (Geheimnis ist base64 nach Entfernen von `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (mit Präfix `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (mit Präfix `v0=`) |
Das obige Resolver-Beispiel zeigt bereits den GitHub-HMAC-SHA256-Flow – passe den Header-Namen, die Digest-Codierung und den signierten Payload-String entsprechend dem Provider an, den du integrierst.
<Note>
Wenn der Resolver ein Dispatch-Objekt zurückgibt, antwortet die Route mit `202 { queued: true }` und das Ziel wird in der Worker-Queue ausgeführt – der Aufrufer bekommt weder die Latenz, noch das Ergebnis oder Fehler des Ziels mit (diese werden in den Ausführungsprotokollen aufgezeichnet). Dadurch wird verhindert, dass erneute Zustellungen des Senders Verarbeitungsverzögerungen verstärken – genau das ist bei der Erfassung von Webhooks erwünscht.
Wenn der Aufrufer den Response-Body in derselben Anfrage lesen muss (Challenge-Handshakes, interaktive Bestätigungen), gib stattdessen eine `Response` aus dem **Resolver** zurück. Die Plattform gibt sie synchron zurück und überspringt die Queue; ihre Header durchlaufen dieselbe Allow-Liste wie HTTP-Routen-Antworten. Halten Sie den Resolver schnell – einige Provider (z. B. Slack) laufen nach wenigen Sekunden in ein Timeout. Da der Resolver als öffentlicher Endpoint erreichbar ist, schütze ihn mit Rate-Limiting an deinem Edge.
</Note>
#### Datenbank-Event-Trigger-Payload
Wenn ein Datenbank-Event-Trigger Ihre Logic Function aufruft, erhält sie eine `DatabaseEventPayload` pro geändertem Datensatz. Die Payload kombiniert Metadaten über den Quell-Workspace und das Objekt mit dem Ereignis auf Datensatzebene.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
Die Nutzlast umfasst:
| Eigenschaft | Beschreibung |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `name` | Ereignisname, z. B. `person.updated`. |
| `workspaceId` | Arbeitsbereich, in dem das Ereignis stattgefunden hat. |
| `objectMetadata` | Metadaten für das Objekt, das geändert wurde. |
| `recordId` | ID des geänderten Datensatzes. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Akteurfelder, wenn das Ereignis von einem Benutzer des Arbeitsbereichs ausgelöst wurde. |
| `properties` | Datensatzdaten für das Ereignis mit `before`, `after`, `diff` und `updatedFields`, abhängig von der Operation. |
| Ereignis | Datensatzdaten |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Bei Soft Deletes folgt `.deleted` der Aktualisierungsstruktur, da sich das Feld `deletedAt` des Datensatzes ändert.
Für dauerhafte Löschvorgänge verwende `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtert, welche Aktualisierungsereignisse die Funktion auslösen.
`event.properties.updatedFields` gibt an, welche Felder beim aktuellen Ereignis tatsächlich geändert wurden.
</Note>
Beispiel für ein Created-Ereignis:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Beispiel für ein Updated-Ereignis:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
Nur bei E-Mail-Aktualisierungen auslösen:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Beispiel für ein Destroyed-Ereignis:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Ausführungskontext
Jeder Handler erhält ein zweites Argument, das die Ausführung selbst beschreibt, unabhängig davon, wodurch sie ausgelöst wurde. Während sich die Form des ersten Arguments je nach Auslöser ändert, bleibt dieses unverändert:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Feld | Typ | Beschreibung |
| ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | Der Arbeitsbereich, zu dem die Ausführung gehört. |
| `userWorkspaceId` | `string \| null` | Die Person, die die Ausführung ausgelöst hat, als Mitglied dieses Arbeitsbereichs. |
| `workspaceMemberId` | `string \| null` | Dieselbe Person als Arbeitsbereichsmitgliedsdatensatz, mit dem Sie Datensätze verknüpfen. |
| `retryCount` | `number` | `0` beim ersten Versuch. Siehe [Hintergrundaufgaben](/l/de/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Obergrenze für von der Anwendung angeforderte Wiederholungsversuche. |
`userWorkspaceId` und `workspaceMemberId` sind `null`, wenn niemand die Ausführung ausgelöst hat: Cron-Zeitpläne, Installations-Hooks und nicht authentifizierte Webhooks haben keine Person hinter sich. Sie sind auch `null`, wenn die Person keinen Arbeitsbereichsmitgliedsdatensatz hat oder ihr Datensatz gelöscht wurde.
<Note>
**Der Kontext teilt Ihnen mit, wer die Ausführung ausgelöst hat.** Was die Ausführung *tun* darf, ist eine separate Deklaration, siehe unten.
</Note>
#### Wessen Zugriff ein Aufruf verwendet
Jeder Client — `CoreApiClient`, `MetadataApiClient` und `RestApiClient` — agiert als die Person, die die Ausführung ausgelöst hat: ihre Rolle wird mit der Ihrer Anwendung geschnitten, sodass der Aufruf nie mehr tun kann als jede von Ihnen beiden. Das ist die Standardeinstellung und bedeutet, dass eine Person Ihre App niemals dazu verwenden kann, ihre eigenen Berechtigungen zu überschreiten.
Wenn niemand die Ausführung ausgelöst hat, gibt es keine Person, als die agiert werden kann; daher greift derselbe Client auf den eigenen Zugriff Ihrer Anwendung zurück: Cron-Zeitpläne, Installations-Hooks und nicht authentifizierte Webhooks benötigen keine besondere Behandlung.
Einige Aufrufe benötigen berechtigterweise den eigenen Zugriff der Anwendung, selbst wenn eine Person *hinter* der Ausführung steht — etwa zum Lesen der Konfigurationsdatensätze Ihrer App oder zum Ausführen von Aufgaben, die diese Person selbst nicht erledigen könnte. Erstellen Sie dafür einen zweiten Client:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Erstellen Sie beide einmal im Modulbereich, und jede Aufrufstelle gibt dann anhand des aufgerufenen Clients an, welchen Zugriff sie verwendet. `RestApiClient` akzeptiert dieselbe Option:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**Eine Ausführung, die niemand ausgelöst hat, agiert als Ihre Anwendung.** Cron-Zeitpläne, Installations-Hooks und nicht authentifizierte Webhooks haben keine Person hinter sich, daher greift der Standard-Client auf den eigenen Zugriff Ihrer Anwendung zurück und funktioniert weiterhin. `runAs: 'application'` wird nur benötigt, wenn Sie diesen Zugriff in einer Ausführung wünschen, die eine Person *doch* ausgelöst hat.
Prüfen Sie `context.workspaceMemberId`, wenn sich eine Funktion unterschiedlich verhält, je nachdem, ob jemand hinter ihr steht, beispielsweise um einen Datensatz zuzuordnen.
</Note>
Die SDK-Hilfsfunktionen, die auf die eigenen Ressourcen Ihrer Anwendung zugreifen, verwenden immer deren Zugriff und ignorieren `runAs`: der [Schlüssel-Wert-Speicher](/l/de/developers/extend/apps/logic/key-value-store), [Verbindungen](/l/de/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` und die Abrechnung von Credits.
#### Eine Funktion als KI-Tool oder Workflow-Aktion verfügbar machen
Logikfunktionen können auf zwei Oberflächen verfügbar gemacht werden, jeweils mit eigenem Trigger:
* **`toolTriggerSettings`** — macht die Funktion über die KI-Funktionen von Twenty (Chat, MCP, Funktionsaufrufe) auffindbar. Verwendet das standardmäßige JSON Schema, das Format, das LLMs nativ verstehen.
* **`workflowActionTriggerSettings`** — lässt die Funktion als Schritt im visuellen Workflow-Builder erscheinen. Verwendet das umfangreiche `InputSchema` von Twenty, sodass der Builder geeignete Feldeditoren, Variablenauswahlen und Beschriftungen rendern kann.
Eine Funktion kann sich für eine, die andere oder beide entscheiden. Sie stehen neben `cronTriggerSettings`, `databaseEventTriggerSettings` und `httpRouteTriggerSettings` — gleiches Muster, gleiche Struktur.
<Note>
**Beziehung zur Code-Aktion im Workflow.** Die integrierte **Code**-Aktion im Workflow-Builder ist selbst eine Logikfunktion – Twenty erstellt für jeden Code-Schritt eine Logikfunktion und stellt deren Editor inline bereit. `workflowActionTriggerSettings` ist die Art und Weise, wie Sie diesen einmaligen Inline-Code in eine **wiederverwendbare** Aktion verwandeln: Definieren Sie die Funktion einmal in Ihrer App, und sie wird in jedem Workflow auswählbar, anstatt in jeden Code-Schritt kopiert zu werden. Siehe die [Code-Aktion](/l/de/user-guide/workflows/capabilities/workflow-actions#code) im Benutzerhandbuch für die Endbenutzeransicht.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Hauptpunkte:
* Eine Funktion kann Oberflächen mischen — deklarieren Sie sowohl `toolTriggerSettings` als auch `workflowActionTriggerSettings`, um sie im Chat UND im Workflow-Builder bereitzustellen.
* `toolTriggerSettings.inputSchema` und `workflowActionTriggerSettings.inputSchema` sind beide optional. Wenn sie weggelassen werden, leitet der Manifest-Builder sie aus dem Handler-Quellcode ab (JSON Schema für das KI-Tool, das `InputSchema` von Twenty für die Workflow-Aktion). Geben Sie eines explizit an, wenn Sie eine reichere Typisierung wünschen — zum Beispiel mit `FieldMetadataType`-fähigen Feldern wie `CURRENCY` oder `RELATION` für den Workflow-Builder oder mit `description`-Feldern, die der KI-Agent lesen kann:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
Um Ihre Parameter **einmal** zu deklarieren und beide Oberflächen zu bedienen, definieren Sie ein einziges JSON Schema (`InputJsonSchema`) und konvertieren Sie es für die Workflow-Aktion mit `jsonSchemaToInputSchema` aus `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` nimmt das JSON Schema direkt, während `workflowActionTriggerSettings.inputSchema` das `InputSchema` von Twenty erwartet:
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Ein vollständiges Beispiel für eine Workflow-Aktion
`workflowActionTriggerSettings` akzeptiert vier Felder:
| Feld | Zweck |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Name, der für die Aktion in der Schritt-Auswahl des Workflow-Builders angezeigt wird. Standardmäßig der Funktions-`name`. |
| `icon` | Symbol, das neben der Aktion angezeigt wird (ein `tabler-icons`-Name, z. B. `IconBuilding`). |
| `inputSchema` | Twentys umfangreiches `InputSchema` – das, was der Builder als konfigurierbare Felder (mit Variablen-Auswählern) rendert. Optional; wird aus dem Handler abgeleitet, wenn es weggelassen wird. |
| `outputSchema` | Deklariert die Struktur, die der Handler zurückgibt, sodass **nachfolgende Schritte auf seine Ausgabefelder abbilden können**. Optional; ohne dieses Schema wird die Ausgabe als einzelner undurchsichtiger Wert bereitgestellt. |
Alles zusammen – eine Funktion, die als Workflow-Aktion bereitgestellt wird, mit einer deklarierten Ausgabe, damit spätere Schritte auf `taskId` verweisen können:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
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,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Sobald die App installiert ist, erscheint **Enrich Company** im Aktionswähler des Workflow-Builders. Der Builder rendert `companyName` und `domain` als Eingabefelder (die jeweils Werte aus vorherigen Schritten beziehen können), und nachgelagerte Schritte können auf die `taskId`- und `enriched`-Ausgaben des Schritts verweisen.
<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>
</Accordion>
</AccordionGroup>
<Note>
**Runtime-Hilfsfunktionen.** `twenty-sdk/utils` exportiert kleine Runtime-Hilfsfunktionen erneut, sodass Handler niemals direkt aus `twenty-shared` importieren. Zum Beispiel gibt `isDefined(value)` sowohl für `null` als auch für `undefined` den Wert `false` zurück – verwenden Sie es, um optionale Handler-Eingaben sicher einzugrenzen, die zur Laufzeit als `null` ankommen können, selbst wenn sie als `T | undefined` typisiert sind:
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Installations-Hooks** – Vorinstallations-, Nachinstallations- und Deinstallations-Handler – teilen sich diese Laufzeit, werden aber mit ihren eigenen define-Funktionen deklariert und verwenden keine Trigger-Einstellungen. Siehe [Installations-Hooks](/l/de/developers/extend/apps/config/install-hooks) für `definePreInstallLogicFunction`, `definePostInstallLogicFunction` und `defineUninstallLogicFunction`.
</Note>
## Eine Timeline-Aktivität erstellen
Verwenden Sie `createTimelineActivity()`, um ein explizites Domänenereignis aus einer Logikfunktion zu veröffentlichen. Definieren Sie das Ereignis zuerst als einen [Timeline-Aktivitätstyp](/l/de/developers/extend/apps/data/timeline-activity-types) und adressieren Sie dann den Typ und die Objekte über ihre stabilen universellen Identifikatoren:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty löst universelle Identifikatoren in installationsspezifische Metadaten-IDs auf, prüft, ob der Timeline-Aktivitätstyp zur aufrufenden App gehört, und erstellt einen Snapshot seiner Präsentationsmetadaten für die neue Aktivität.
Die erforderlichen Eingaben sind `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` und `targetRecordId`. Sie können auch `happensAt`, `properties` und `workspaceMemberId` angeben. `happensAt` steuert die Position und die angezeigte Zeit des Ereignisses in der Timeline; standardmäßig wird die Erstellungszeit verwendet.
Um einen weiteren Datensatz mit dem Ereignis zu verknüpfen, geben Sie `linkedRecordId` und `linkedObjectMetadataUniversalIdentifier` zusammen an. Sie können zusätzlich `linkedRecordCachedName` als historische Anzeige-Fallback-Option angeben:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Die Rolle der Logikfunktion benötigt Schreibberechtigung für das Standardobjekt `timelineActivity`. Lassen Sie explizite Ereignistypen von einer `action` ungebunden; ein an eine Aktion gebundener Typ erhält bereits automatische Audit-Ereignisse und würde andernfalls doppelte Zeilen erzeugen.
## 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.
| Client | Importieren | Endpunkt | Generiert? |
| ------------------- | ---------------------------- | --------------------------------------------------------- | ------------------------------------ |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — Arbeitsbereichsdaten (Datensätze, Objekte) | Ja, zur Entwicklungs-/Build-Zeit |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — Arbeitsbereichskonfiguration, Datei-Uploads | Nein, wird vorgefertigt ausgeliefert |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Arbeitsbereichsdaten (Datensätze, Objekte) abfragen und ändern">
Der `CoreApiClient` ist der Haupt-Client zum Abfragen und Ändern von Arbeitsbereichsdaten. Er wird während `yarn twenty dev` oder `yarn twenty dev:build` **aus Ihrem Arbeitsbereichsschema generiert** und ist daher vollständig typisiert, passend zu Ihren Objekten und Feldern.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
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.
<Note>
**Der CoreApiClient wird zur Entwicklungs-/Build-Zeit generiert.** Wenn Sie ihn verwenden, ohne zuvor `yarn twenty dev` oder `yarn twenty dev:build` ausgeführt zu haben, wird ein Fehler ausgelöst. Die Generierung erfolgt automatisch — die CLI inspiziert das GraphQL-Schema Ihres Arbeitsbereichs und erzeugt mit `@genql/cli` einen typisierten Client.
</Note>
#### Verwendung von CoreSchema für Typannotationen
`CoreSchema` stellt TypeScript-Typen bereit, die Ihren Arbeitsbereichsobjekten entsprechen — nützlich zum Typisieren von Komponentenzustand oder Funktionsparametern:
```ts
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);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Konfiguration des Arbeitsbereichs, Anwendungen und Dateiuploads">
`MetadataApiClient` ist im SDK bereits vorgefertigt enthalten (keine Generierung erforderlich). Er fragt den Endpunkt `/metadata` nach Arbeitsbereichskonfiguration, Anwendungen und Datei-Uploads ab. Er verwendet dieselbe Option `runAs` wie `CoreApiClient` — siehe [Wessen Zugriff ein Aufruf verwendet](#whose-access-a-call-uses).
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Dateien hochladen
Der `MetadataApiClient` enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei anzuhängen:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Parameter | Typ | Beschreibung |
| ---------------------------------- | -------------- | --------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Der Rohinhalt der Datei |
| `filename` | `Zeichenkette` | Der Name der Datei (wird für Speicherung und Anzeige verwendet) |
| `contentType` | `string` | MIME-Typ (standardmäßig `application/octet-stream`, wenn weggelassen) |
| `fieldMetadataUniversalIdentifier` | `string` | Der `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.
</Accordion>
</AccordionGroup>
<Note>
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 für den Standardzugriff: die Rolle einer Person wird mit der Ihrer Anwendung geschnitten, wenn jemand hinter der Ausführung steht; die eigene Rolle Ihrer Anwendung, wenn niemand dahintersteht. Die Person ist in einer Logikfunktion diejenige, die die Ausführung ausgelöst hat, und in einer Front-Komponente diejenige, die die Seite betrachtet.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Kurzlebiger Schlüssel, der ausschließlich auf die eigene Rolle Ihrer Anwendung begrenzt ist. Nur Logikfunktionen, dort immer eingefügt, und wird von `runAs: 'application'` verwendet.
Sie müssen diese **nicht** an die Clients übergeben — sie lesen sie automatisch aus `process.env`, und [Wessen Zugriff ein Aufruf verwendet](#whose-access-a-call-uses) behandelt die Auswahl zwischen ihnen. Die eigenen Berechtigungen Ihrer Anwendung werden durch die Rolle bestimmt, die mit `defineApplicationRole()` deklariert wird (oder über `defaultRoleUniversalIdentifier` in `application-config.ts` referenziert wird); eine Ausführung, die als Person agiert, kann weder diese Rolle noch die der Person überschreiten.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Übersicht
description: Serverseitiges TypeScript, das innerhalb von Twenty ausgeführt wird – ausgelöst durch HTTP-Routen, Cron-Zeitpläne, Datenbankereignisse, KI-Tools oder Workflow-Aktionen.
icon: bolt
---
Die **Logikschicht** einer Twenty-App ist der Code, der *ausgeführt wird* – serverseitige TypeScript-Handler, die auf HTTP-Anfragen, Cron-Zeitpläne und Datensatzänderungen reagieren; KI-Skills und -Agenten, die innerhalb des Workspaces leben; und OAuth-Verbindungen, die es Ihren Funktionen ermöglichen, im Namen eines Benutzers in Drittanbieterdiensten zu agieren.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="Logikfunktionen" icon="bolt" href="/l/de/developers/extend/apps/logic/logic-functions">
Der zentrale Baustein – Auslösertypen, Payloads und der typisierte API-Client.
</Card>
<Card title="Skills & Agenten" icon="robot" href="/l/de/developers/extend/apps/logic/skills-and-agents">
Wiederverwendbare Anweisungen für KI-Agenten und Assistenten mit benutzerdefinierten System-Prompts.
</Card>
<Card title="Verbindungen" icon="plug" href="/l/de/developers/extend/apps/logic/connections">
OAuth-Anmeldedaten, die Ihre App für Dienste von Drittanbietern verwaltet – Linear, GitHub, Slack und mehr.
</Card>
<Card title="Key-Value-Store" icon="database" href="/l/de/developers/extend/apps/logic/key-value-store">
Zustand zwischen Ausführungen von Logikfunktionen beibehalten — Caches, Cursor und arbeitsbereichsübergreifende Claims.
</Card>
<Card title="Hintergrundjobs" icon="layer-group" href="/l/de/developers/extend/apps/logic/background-jobs">
Reihen Sie die Ausführung einer Logikfunktion in die Warteschlange der Worker ein, um das Timeout je Ausführung zu umgehen.
</Card>
</CardGroup>
## Auslösertypen im Überblick
Eine Logikfunktion wählt einen oder mehrere Auslöser – jeder Eintrag unten ist ein eigenes Feld auf `defineLogicFunction()`:
| Auslöser | Wann sie ausgeführt wird | Einstellung |
| --------------------- | ------------------------------------------------------------------ | ------------------------------- |
| **HTTP-Route** | Eine Anfrage erreicht Ihren `/s/\<path>`-Endpunkt | `httpRouteTriggerSettings` |
| **Cron** | Ein CRON-Ausdruck trifft zu | `cronTriggerSettings` |
| **Datenbankereignis** | Ein Workspace-Datensatz wird erstellt, aktualisiert oder gelöscht | `databaseEventTriggerSettings` |
| **KI-Tool** | Eine Twenty-KI-Funktion entscheidet sich, Ihre Funktion aufzurufen | `toolTriggerSettings` |
| **Workflow-Aktion** | Ein Workflow-Schritt ruft Ihre Funktion auf | `workflowActionTriggerSettings` |
Funktionen werden in isolierten Node.js-Prozessen in einer Sandbox ausgeführt und greifen über einen typisierten API-Client, der auf die in [`defineApplication()`](/l/de/developers/extend/apps/config/application) deklarierte Rolle beschränkt ist, auf den Workspace zu.
<Note>
**Installations-Hooks** – Code, der vor oder nach der Installation ausgeführt wird – teilen sich diese Laufzeitumgebung, verwenden jedoch eigene Define-Funktionen und befinden sich unter [Config → Install Hooks](/l/de/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,187 @@
---
title: Skills & Agenten
description: Definieren Sie KI-Skills und Agenten für Ihre App.
icon: robot
---
<Warning>
Skills und Agenten befinden sich derzeit in der Alpha-Phase. Die Funktion ist funktionsfähig, entwickelt sich jedoch noch weiter.
</Warning>
Apps können KI-Funktionen definieren, die im Arbeitsbereich verfügbar sind — wiederverwendbare Skill-Anweisungen und Agenten mit benutzerdefinierten System-Prompts.
<AccordionGroup>
<Accordion title="defineSkill" description="Skills für KI-Agenten definieren">
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:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
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.
</Accordion>
<Accordion title="defineAgent" description="KI-Agenten mit benutzerdefinierten Prompts definieren">
Agenten sind KI-Assistenten, die innerhalb Ihres Arbeitsbereichs leben. Verwenden Sie `defineAgent()`, um Agenten mit einem benutzerdefinierten System-Prompt zu erstellen:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
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 eine eindeutige Kennung (als 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.
* `responseFormat` (optional) steuert die Form der Ausgabe des Agenten. Standardmäßig ist `{ type: 'text' }` für Freitext. Verwenden Sie `{ type: 'json', schema }`, um eine strukturierte JSON-Ausgabe zu erzwingen.
Standardmäßig gibt ein Agent freien Text zurück. Um eine strukturierte Ausgabe zu erhalten, setzen Sie `responseFormat` auf `{ type: 'json' }` und geben Sie ein `schema` an:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Schema-Hinweise:
* Das Schema ist ein flaches Objekt: Der `type` jeder Eigenschaft muss ein primitiver Typ sein (`string`, `number` oder `boolean`). Verschachtelte Objekte und Arrays werden nicht unterstützt.
* `description` (optional) an jeder Eigenschaft leitet das Modell an, was dort eingetragen werden soll.
* `required` (optional) listet die Eigenschaften auf, die das Modell immer zurückgeben muss.
* `additionalProperties: false` (optional) verbietet alle Eigenschaften, die nicht in `properties` deklariert sind.
</Accordion>
<Accordion title="runAgent" description="Einen Agenten aus einer Logikfunktion ausführen">
`runAgent()` ermöglicht es einer Logikfunktion, einen der Agenten Ihrer App auszuführen (mit seinen Fähigkeiten und Tools). Identifizieren Sie den Agenten über den `universalIdentifier`, den Sie an `defineAgent()` übergeben haben. Übergeben Sie entweder einen `prompt`-String oder einen `messages`-Konversationsverlauf – nicht beides:
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Für Multi-Turn-Bots (Slack, Discord, Teams, …) übergeben Sie den Thread-Verlauf als `messages` anstelle eines einzelnen `prompt`:
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Hauptpunkte:
* Geben Sie **genau eines** von `prompt` (string) oder `messages` (1 bis 100 Einträge
von `{ role: 'user' | 'assistant', content: string }`) an.
* Der Agent wird **synchron** ausgeführt und kann selbst Datensätze über seine eigenen Tools lesen/aktualisieren — `runAgent()` wird aufgelöst, sobald die Ausführung abgeschlossen ist.
* Eine App kann nur ihre eigenen Agenten ausführen.
* Die [Standardrolle](/l/de/developers/extend/apps/config/roles) der App muss das Berechtigungsflag `AI` gewähren — fügen Sie `SystemPermissionFlag.AI` zu ihren `permissionFlagUniversalIdentifiers` hinzu (oder setzen Sie `canAccessAllTools: true`).
Ohne dieses Flag schlägt `runAgent()` mit einem Berechtigungsfehler fehl.
* Setzen Sie ein großzügiges `timeoutSeconds` für die Logikfunktion — Agentenläufe können mehrere Sekunden dauern.
* `success` ist `true` und `result` ist nicht null, wenn der Lauf abgeschlossen ist; bei einem Fehler ist `success` `false`, `result` ist `null` und `error` enthält den Grund (zum Beispiel, wenn dem Workspace während des Laufs die AI-Credits ausgehen).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Vermeiden Sie Schleifen:** Wenn Sie `runAgent()` von einem `*.updated`-Datenbankereignis-Trigger aus aufrufen und der Agent denselben Datensatz aktualisiert, schränken Sie den Trigger mit `updatedFields` auf ein Feld ein, das der Agent niemals beschreibt (z. B. die Quell-URL), oder prüfen Sie, ob eines der Zielfelder noch leer ist, bevor Sie `runAgent()` aufrufen.
</Warning>
### Ausführung im Namen eines Arbeitsbereichsmitglieds
Übergeben Sie `runAsWorkspaceMemberId`, wenn die Ausführung von einer Person ausgelöst wird – zum Beispiel von einem Chatbot, der auf eine Nachricht antwortet –, damit der Agent als dieses Mitglied und nicht als die App agiert:
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
Der Durchlauf agiert dann mit der **eigenen Rolle des Mitglieds**: Er kann alles tun, was dieses Mitglied tun kann, und nichts darüber hinaus; von ihm erstellte Datensätze werden diesem Mitglied zugeschrieben, und die zeilenbasierten Berechtigungen des Mitglieds gelten. Die Rolle des Agenten nimmt daran nicht teil – sie ist der Standard der App für Durchläufe, hinter denen keine Person steht. Lassen Sie das Feld bei autonomen Ausführungen weg (geplante Jobs, Datenbankereignis-Trigger): Diese behalten die eigene Rolle des Agenten bei. Die ausführende Anwendung bleibt aus Gründen der Herkunftssicherung mit dem Kontext des Durchlaufs verknüpft, ohne die Berechtigungen des Mitglieds einzuengen.
Ihre App ist dafür verantwortlich, die Person, die die Ausführung ausgelöst hat, einem Arbeitsbereichsmitglied zuzuordnen. Das Benennen eines Mitglieds erfordert ein App-Zugriffstoken, und was ein Token benennen darf, hängt davon ab, ob ihm ein Benutzer zugeordnet ist:
* Ein Token **ohne angehängten Benutzer** kann jedes Mitglied benennen. Eine Logikfunktion läuft
mit einem solchen, und ebenso gilt dies für Token, die über `client_credentials` oder aus einem API-
Schlüssel ausgestellt werden.
* Ein Token, das **im Namen eines Benutzers** ausgestellt wird, wie es eine Frontkomponente erhält, darf
nur das eigene Mitglied dieses Benutzers benennen.
Alles andere — eine einfache Benutzersitzung, ein API-Schlüssel ohne App-Token — darf
überhaupt kein Mitglied benennen.
<Warning>
`runAgent()` löst eine Ausnahme aus, wenn das Arbeitsbereichsmitglied nicht aufgelöst werden kann — ein
unbekanntes oder entferntes Mitglied oder eines ohne Rolle. Es greift niemals auf die
eigene Rolle des Agenten zurück, da dies mehr Zugriff gewähren würde, als der Aufrufer angefordert
hat.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: yarn twenty Befehle zum Ausführen von Funktionen, Streamen von Logs, Verwalten von App-Installationen und Wechseln von Remotes.
icon: terminal
---
Die `yarn twenty` CLI ist Ihre Schnittstelle für alles, was mit der App zu tun hat. Vollständige Befehlsliste:
| Befehl | Was es tut | Dokumentiert in |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `dev` | Überwacht Ihre Quelldateien und synchronisiert Änderungen in Echtzeit | [Schnellstart](/l/de/developers/extend/apps/getting-started/quick-start) |
| `plan` | Metadatenänderungen anzeigen, ohne sie anzuwenden | [Synchronisierung & Wiederherstellung](/l/de/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `apply` | Metadatenänderungen anwenden, nachdem der Plan angezeigt wurde | [Synchronisierung & Wiederherstellung](/l/de/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Die App kompilieren und den API-Client generieren (`--tarball`, um ein `.tgz` zu packen) | [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | TypeScript-Typprüfung ausführen | [Tests](/l/de/developers/extend/apps/operations/testing) |
| `dev:add` | Ein Gerüst für eine neue Entität erstellen | [Gerüste](/l/de/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Den typisierten API-Client erneut generieren | diese Seite |
| `dev:function:exec` / `dev:function:logs` | Funktionen ausführen und ihre Protokolle streamen | diese Seite |
| `dev:translations-extract` | Übersetzbare Zeichenketten in `locales/`-Kataloge extrahieren | [Übersetzungen](/l/de/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Eine Synchronisierung des Marktplatzkatalogs auslösen | [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Release-Lebenszyklus | [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing) und diese Seite |
| `docker:*` | Den lokalen Twenty-Server-Container verwalten | [Lokaler Server](/l/de/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Serververbindungen verwalten | diese Seite |
Jeder Befehl akzeptiert `-r, --remote \<name>`, um ein bestimmtes Remote statt des Standard-Remotes anzusteuern.
## Funktionen ausführen (`yarn twenty dev:function:exec`)
Eine Logikfunktion manuell ausführen, ohne sie über HTTP, Cron oder ein Datenbankereignis auszulösen:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## Funktionsprotokolle ansehen (`yarn twenty dev:function:logs`)
Ausführungsprotokolle für die Logikfunktionen Ihrer App streamen:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Dies unterscheidet sich von `yarn twenty docker:logs`, das die Docker-Container-Logs anzeigt. `yarn twenty dev:function:logs` zeigt die Funktionsausführungsprotokolle Ihrer App vom Twenty-Server.
</Note>
## Generieren des typisierten Clients (`yarn twenty dev:generate-client`)
Generieren Sie den typisierten API-Client (`twenty-client-sdk`) erneut aus dem Schema des aktiven Remotes, ohne eine App zu erstellen oder zu synchronisieren. Verwenden Sie ihn, um in jedem Projekt einen typisierten Client zu erhalten – zum Beispiel für einen Backend-Dienst in einem separaten Repository –, der mit Ihrer Twenty-Instanz kommuniziert:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Importieren Sie dann den Client in Ihrem Code:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Führen Sie den Befehl erneut aus, wann immer sich Ihr Datenmodell ändert, um die generierten Typen zu aktualisieren.
<Note>
Der Client wird in `node_modules` generiert, daher wird er nicht zusammen mit Ihrem Code eingecheckt. Führen Sie `yarn twenty dev:generate-client` nach jeder Installation aus (zum Beispiel in einem `postinstall`-Skript oder in CI).
</Note>
## Eine App deinstallieren (`yarn twenty app:uninstall`)
Entfernen Sie Ihre App aus dem aktiven Arbeitsbereich:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app: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.
```bash filename="Terminal"
# 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 --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Ihre Anmeldedaten werden in `~/.twenty/config.json` gespeichert.

View File

@@ -0,0 +1,32 @@
---
title: Übersicht
description: Erstellen, testen und ausliefern Sie Ihre App – CLI-Befehle, Integrationstests, CI und das Veröffentlichen auf einem Server oder auf npm.
icon: rocket
---
Die **Operationsschicht** umfasst alles, was Sie *an* Ihrer App tun, statt *mit* ihr: das Ausführen von CLI-Befehlen, das Durchführen von Integrationstests gegen einen realen Twenty-Server, das Konfigurieren von CI und das Ausliefern von Releases – entweder als Tarball, der auf einem einzelnen Server bereitgestellt wird, oder als npm-Paket, das im Marketplace gelistet ist.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/de/developers/extend/apps/operations/cli">
`yarn twenty`-Referenz – exec, logs, uninstall, remotes.
</Card>
<Card title="Synchronisierung & Wiederherstellung" icon="compass" href="/l/de/developers/extend/apps/operations/sync-and-recovery">
Welcher Befehl wann, das Sync-Diff lesen sowie ein Stufenplan zur Wiederherstellung.
</Card>
<Card title="Tests" icon="flask" href="/l/de/developers/extend/apps/operations/testing">
Vitest-Setup, Integrationstests, Typprüfung, CI-Workflow.
</Card>
<Card title="Veröffentlichen" icon="upload" href="/l/de/developers/extend/apps/operations/publishing">
Build erstellen, Tarball bereitstellen, auf npm veröffentlichen, installieren.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: Veröffentlichen
icon: upload
description: Veröffentlichen Sie Ihre Twenty-App auf dem Twenty-Marktplatz oder stellen Sie sie intern bereit.
---
## Übersicht
Sobald Ihre App [lokal gebaut und getestet](/l/de/developers/extend/apps/getting-started/concepts) wurde, haben Sie zwei Möglichkeiten, sie zu verteilen:
* **Einen Tarball bereitstellen** — Laden Sie Ihre App direkt auf einen bestimmten Twenty-Server für die interne oder private Nutzung hoch.
* **Auf npm veröffentlichen** — führen Sie Ihre App im Twenty-Marktplatz auf, damit jeder Arbeitsbereich sie entdecken und installieren kann.
Beide Pfade beginnen mit demselben **Build**-Schritt.
## Erstellen Ihrer App
Führen Sie den Build-Befehl aus, um Ihre App zu kompilieren und eine distributionsfertige `manifest.json` zu erzeugen:
```bash filename="Terminal"
yarn twenty dev:build
```
Dabei werden TypeScript-Quelltexte kompiliert, Logikfunktionen und Frontend-Komponenten transpiliert und alles in `.twenty/output/` geschrieben. Fügen Sie `--tarball` hinzu, um zusätzlich ein `.tgz`-Paket für die manuelle Verteilung oder den Publish-Befehl zu erzeugen.
## Bereitstellung auf einem Server (Tarball)
Für Apps, die Sie nicht öffentlich verfügbar machen möchten — proprietäre Tools, ausschließlich für Unternehmen bestimmte Integrationen oder experimentelle Builds — können Sie einen Tarball direkt auf einem Twenty-Server bereitstellen.
### Voraussetzungen
Bevor Sie bereitstellen, benötigen Sie ein konfiguriertes Remote, das auf den Zielserver zeigt. Remotes speichern die Server-URL und Anmeldeinformationen lokal in `~/.twenty/config.json`.
Ein Remote hinzufügen:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Bereitstellen
Bauen und laden Sie Ihre App in einem Schritt auf den Server hoch:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Eine bereitgestellte App freigeben
Tarball-Apps werden nicht im öffentlichen Marktplatz gelistet, daher entdecken andere Arbeitsbereiche auf demselben Server sie nicht durch Stöbern. So geben Sie eine bereitgestellte App frei:
1. Gehen Sie zu **Einstellungen > Anwendungen > Registrierungen** und öffnen Sie Ihre App
2. Klicken Sie in der Registerkarte **Distribution** auf **Freigabelink kopieren**
3. Teilen Sie diesen Link mit Nutzern in anderen Arbeitsbereichen — er führt sie direkt zur Installationsseite der App
Der Freigabelink verwendet die Basis-URL des Servers (ohne Workspace-Subdomain), sodass er für jeden Arbeitsbereich auf dem Server funktioniert.
### Versionsverwaltung
Beim Aktualisieren einer bereits bereitgestellten Tarball-App verlangt der Server, dass die `version` in `package.json` **strikt höher** (gemäß der [semver](https://semver.org)-Reihenfolge) ist als die derzeit bereitgestellte Version. Das erneute Bereitstellen derselben Version oder das Pushen einer niedrigeren Version wird abgelehnt, bevor das Tarball gespeichert wird — in der CLI wird ein `VERSION_ALREADY_EXISTS`-Fehler angezeigt.
So veröffentlichen Sie ein Update:
1. Erhöhen Sie das Feld `version` in Ihrer `package.json` (z. B. `1.2.3` → `1.2.4`, `1.3.0` oder `2.0.0`).
2. Führen Sie `yarn twenty app:publish --private` aus (oder `yarn twenty app:publish --private --remote production`)
3. Arbeitsbereiche, in denen die App installiert ist und bei denen die automatische Aktualisierung (im Tab „Einstellungen“ der App) aktiviert wurde, werden im Hintergrund automatisch aktualisiert; bei den anderen wird die Aktualisierung in deren Einstellungen als verfügbar angezeigt
<Note>
Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `1.0.0-rc.2` ist zulässig, und eine finale Version wie `1.0.0` wird korrekt als höher als `1.0.0-rc.5` erkannt. Die Version in `package.json` muss selbst eine gültige semver-Zeichenfolge sein.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Kompatibilität der Serverversionen
Wenn Ihre App eine Funktion verwendet, die in einer bestimmten Twenty-Serverversion eingeführt wurde (z. B. OAuth-Anbieter, die in v2.3.0 hinzugefügt wurden), sollten Sie die minimale Serverversion, die Ihre App benötigt, mithilfe des Felds `engines.twenty` in `package.json` angeben:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Der Wert ist ein standardmäßiger [semver-Bereich](https://github.com/npm/node-semver#ranges). Häufige Muster:
| Bereich | Bedeutung |
| ---------------------------------- | ------------------------------------------------------ |
| `>=2.3.0` | Jeder Server ab 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 oder höher, aber unter der nächsten Hauptversion |
| `^2.3.0` | Entspricht `>=2.3.0 \<3.0.0` |
**Was bei Bereitstellung und Installation passiert:**
* Wenn `engines.twenty` gesetzt ist und die Version des Zielservers den Bereich nicht erfüllt, wird die Bereitstellung (Tarball-Upload) oder Installation mit dem Fehler `SERVER_VERSION_INCOMPATIBLE` abgelehnt, zusammen mit einer Meldung, die sowohl den erforderlichen Bereich als auch die tatsächliche Serverversion angibt.
* Wenn `engines.twenty` nicht gesetzt ist, wird die App auf jeder Serverversion akzeptiert (abwärtskompatibel mit bestehenden Apps).
* Wenn auf dem Server keine `APP_VERSION` konfiguriert ist, wird die Prüfung übersprungen.
<Note>
Der Server ist die maßgebliche Prüfinstanz — er validiert `engines.twenty` sowohl beim Tarball-Upload als auch bei der Workspace-Installation. Auch wenn Sie einen Tarball außerhalb des regulären Prozesses bereitstellen oder aus dem Marktplatz installieren, erzwingt der Server weiterhin die Kompatibilität.
</Note>
## Automatisiertes CI/CD (vorgefertigte Workflows)
Apps, die mit `create-twenty-app` erzeugt wurden, enthalten von Haus aus drei GitHub-Actions-Workflows unter `.github/workflows/`. CI läuft ohne Einrichtung, CD erfordert ein einziges Secret, und das Veröffentlichen auf npm erfordert eine einmalige Einrichtung als npm Trusted Publisher.
### CI — `ci.yml`
Führt Ihre Integrationstests bei jedem Push auf `main` und bei Pull Requests aus.
**Was sie macht:**
1. Checkt den Quellcode Ihrer App aus.
2. Startet eine isolierte Twenty-Testinstanz mithilfe der Composite-Action `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (das CI-Äquivalent zu `yarn twenty docker:start --test`).
3. Aktiviert Corepack, richtet Node.js anhand Ihrer `.nvmrc` ein und installiert Abhängigkeiten mit `yarn install --immutable`.
4. Führt `yarn test` aus und übergibt `TWENTY_API_URL` und `TWENTY_API_KEY` aus der gestarteten Instanz, damit Ihre Tests mit einem echten Server kommunizieren können.
**Konfigurationsoptionen:**
* `TWENTY_VERSION` (env, standardmäßig `latest`) — fixieren Sie die in CI verwendete Twenty-Server-Version, indem Sie dies in `ci.yml` anpassen.
* Die Parallelität wird nach `github.ref` gruppiert und bricht laufende Ausführungen bei neuen Pushes ab.
Es sind keine Secrets erforderlich — die Testinstanz ist flüchtig und existiert nur für die Dauer des Jobs.
### CD — `cd.yml`
Stellt Ihre App bei jedem Push auf `main` auf einem konfigurierten Twenty-Server bereit und optional aus einem Pull Request, wenn das Label `deploy` gesetzt ist.
**Was sie macht:**
1. Checkt den PR-Head (bei PRs mit Label) oder den gepushten Commit aus.
2. Führt `twentyhq/twenty/.github/actions/deploy-twenty-app@main` aus — das CI-Äquivalent zu `yarn twenty app:publish --private`.
3. Führt `twentyhq/twenty/.github/actions/install-twenty-app@main` aus, damit die neu bereitgestellte Version in den Ziel-Workspace installiert wird.
**Erforderliche Konfiguration:**
| Einstellung | Wo | Zweck |
| ----------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (standardmäßig `http://localhost:3000`) | Der Twenty-Server, auf den bereitgestellt werden soll. Ändern Sie dies vor der ersten Verwendung auf die echte Server-URL. |
| `TWENTY_DEPLOY_API_KEY` | GitHub-Repository **Settings → Secrets and variables → Actions** | API-Schlüssel mit Berechtigung zum Bereitstellen auf dem Zielserver. |
<Note>
Der Standardwert von `TWENTY_DEPLOY_URL` (`http://localhost:3000`) ist ein Platzhalter — von einem GitHub-gehosteten Runner ist er nicht erreichbar. Aktualisieren Sie sie auf die öffentliche URL Ihres Servers (oder verwenden Sie einen selbstgehosteten Runner mit Netzwerkzugriff), bevor Sie CD aktivieren.
</Note>
**Eine Vorschau-Bereitstellung aus einem PR auslösen:**
Fügen Sie einem Pull Request das Label `deploy` hinzu. Die `if:`-Bedingung in `cd.yml` führt den Job für diesen PR mit dem Head-Commit des PR aus, sodass Sie eine Änderung auf dem Zielserver vor dem Mergen validieren können.
### Veröffentlichen — `publish.yml`
Veröffentlicht Ihre App mit Herkunftsnachweis auf npm, wenn Sie ein Versions-Tag pushen (z. B. `v1.0.0`), oder wenn Sie den Workflow manuell über den Actions-Tab ausführen.
**Was sie macht:**
1. Checken Sie Ihre App aus, richten Sie Node.js ein und aktualisieren Sie npm (Trusted Publishing erfordert npm 11.5.1 oder neuer).
2. Führt `yarn twenty app:publish` aus, wodurch die App gebaut und `.twenty/output` auf npm veröffentlicht wird. In CI werden automatisch `--provenance` und `--access public` hinzugefügt, sodass im Workflow keine Flags benötigt werden.
**Einmalige Einrichtung:**
Öffnen Sie auf npmjs.com Ihr Paket > **Settings → Trusted Publisher** und registrieren Sie dieses Repository mit dem `publish.yml`-Workflow (siehe die [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers)). Das Veröffentlichen mit Provenance bestätigt, welches GitHub-Repository das Paket gebaut hat; zugleich beanspruchen Sie damit die Inhaberschaft Ihrer App in einem Twenty-Marktplatz.
<Note>
npm akzeptiert Herkunftsnachweise nur aus **öffentlichen** Quellcode-Repositories. Wenn du aus einem privaten Repository veröffentlichst, weist npm das OIDC-Provenance-Bundle mit einem `E422 Unsupported GitHub Actions source repository visibility: "private"`-Fehler. Um aus einem privaten Repository zu veröffentlichen, deaktiviere Herkunftsnachweise, indem du `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` im `env`-Block des Veröffentlichungs-Schritts setzt (ein auskommentierter Hinweis ist in der generierten `publish.yml` enthalten):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Fixieren der wiederverwendbaren Actions
Die Workflows `ci.yml` und `cd.yml` verweisen auf wiederverwendbare Actions mit `@main`, sodass Aktualisierungen der Actions im Repository `twentyhq/twenty` automatisch übernommen werden. Wenn Sie deterministische Builds möchten, ersetzen Sie `@main` in jeder `uses:`-Zeile durch eine Commit-SHA oder einen Release-Tag.
## Auf npm veröffentlichen
Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Jeder Twenty-Arbeitsbereich kann Marktplatz-Apps direkt über die Benutzeroberfläche durchsuchen, installieren und aktualisieren.
### Anforderungen
* Ein [npm](https://www.npmjs.com)-Konto
* Das Schlüsselwort `twenty-app` in Ihrem `package.json`-Array `keywords` (manuell hinzufügen — es ist in der `create-twenty-app`-Vorlage standardmäßig nicht enthalten)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Marktplatz-Metadaten
Die `defineApplication()`-Konfiguration unterstützt optionale Felder, die steuern, wie Ihre App im Marktplatz erscheint. Verwende `logo` und `galleryImages` um Bilder aus dem `public/` Ordner zu referenzieren:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Siehe das [defineApplication-Akkordeon](/l/de/developers/extend/apps/config/application#marketplace-metadata) auf der Seite Building Apps für die vollständige Liste der Marktplatzfelder (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` usw.).
#### Empfohlene Galerieabmessungen
Der Marktplatz zeigt `galleryImages` in einem fixen `8:5` Container an (z.B. `1600×1000 px`).
<Note>
Galeriebilder mit beliebigem Seitenverhältnis werden vollständig angezeigt und werden nie zugeschnitten, aber irgendetwas wesentlich höher oder enger als `8:5` wird leere Bands auf den Seiten zeigen.
</Note>
#### Maximale Bildgröße
Die Datei `logo` und jede Datei in `galleryImages` dürfen jeweils **10 MB** nicht überschreiten. Größere Dateien werden übersprungen, wenn der Marktplatz deine veröffentlichten Assets erneut hostet, sodass sie nicht angezeigt werden.
### Veröffentlichen
```bash filename="Terminal"
yarn twenty app:publish
```
Um unter einem bestimmten dist-tag zu veröffentlichen (z. B. `beta` oder `next`):
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### So funktioniert die Marktplatz-Erkennung
Der Twenty-Server synchronisiert seinen Marktplatzkatalog **stündlich** aus der npm-Registry.
Sie können die Synchronisierung sofort auslösen, anstatt zu warten:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
Die im Marketplace angezeigten Metadaten stammen aus deiner `defineApplication()`-Konfiguration – siehe oben unter [Marketplace-Metadaten](#marketplace-metadata).
<Note>
Wenn Ihre App keine `aboutDescription` in `defineApplication()` definiert, verwendet der Marktplatz automatisch die `README.md` Ihres Pakets von npm als Inhalt der Über-uns-Seite. Das bedeutet, dass Sie eine einzige README sowohl für npm als auch für den Twenty-Marktplatz pflegen können. Wenn Sie im Marktplatz eine andere Beschreibung möchten, setzen Sie `aboutDescription` explizit.
</Note>
### CI-Veröffentlichung
Der oben beschriebene, erzeugte `publish.yml`-Workflow veröffentlicht mit Provenance bei Versions-Tags automatisch auf npm. Da `yarn twenty app:publish` beim Ausführen in CI `--provenance` und `--access public` für Sie hinzufügt, benötigt der Workflow keine npm-Flags – nur die einmalige Trusted-Publisher-Einrichtung.
Für andere CI-Systeme (GitLab CI, CircleCI usw.) führen Sie zuerst `yarn install` und dann `yarn twenty app:publish` aus. Provenance wird ausgegeben, wenn die Umgebung ein OIDC-Token erstellen kann, und andernfalls automatisch übersprungen.
<Note>
**npm-Provenance** fügt Ihrem npm-Eintrag ein Vertrauensabzeichen hinzu, sodass Nutzer überprüfen können, dass das Paket aus einem bestimmten Commit in einer öffentlichen CI-Pipeline gebaut wurde. Damit können Sie außerdem die Inhaberschaft Ihrer App in einem Twenty-Marktplatz beanspruchen. Siehe die [npm-Provenance-Dokumentation](https://docs.npmjs.com/generating-provenance-statements) für Details.
</Note>
## Apps installieren
Sobald eine App veröffentlicht (npm) oder bereitgestellt (Tarball) wurde, können Arbeitsbereiche sie über die Benutzeroberfläche installieren.
Gehen Sie zur Seite **Einstellungen > Anwendungen** in Twenty, auf der sowohl Marktplatz- als auch per Tarball bereitgestellte Apps durchsucht und installiert werden können.
{/* TODO: add screenshot of the UI when the app is registered */}
Sie können Apps auch über die Befehlszeile installieren:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
Der Server erzwingt bei der Installation semver-Versionierung und spiegelt damit die Regeln beim Bereitstellen wider:
* Die Installation derselben Version, die in Ihrem Arbeitsbereich bereits installiert ist, wird mit einem `APP_ALREADY_INSTALLED`-Fehler abgelehnt.
* Die Installation einer niedrigeren Version als die aktuell installierte wird mit einem `CANNOT_DOWNGRADE_APPLICATION`-Fehler abgelehnt.
Um eine neuere Version zu installieren, stellen Sie sie zuerst bereit oder veröffentlichen Sie sie und führen Sie dann `yarn twenty app:install` erneut aus.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: Synchronisierung & Wiederherstellung
description: Welchen Befehl Sie wann verwenden, wie Sie die Sync-Ausgabe lesen und eine Wiederherstellungsleiter, wenn lokale Metadaten abweichen – bevor es zu einem vollständigen Reset kommt.
icon: compass
---
Die lokale App-Entwicklung dreht sich um **Syncing**: Die CLI baut Ihr Manifest neu auf und der Server wendet nur die Differenz zwischen diesem und den Metadaten an, die sich bereits in Ihrem Workspace befinden. Diese Seite behandelt, zu welchem Befehl Sie greifen sollten, wie Sie lesen, was ein Sync geändert hat, und was Sie – der Reihe nach – tun sollten, wenn der lokale Zustand inkonsistent aussieht.
## Welcher Befehl, wann
<Note>
Für die tägliche lokale Iteration sollten Sie fast immer `yarn twenty dev` verwenden. Bereitstellen und Veröffentlichen sind zum Ausliefern von Releases gedacht, **nicht** für den lokalen Entwicklungszyklus.
</Note>
| Sie möchten … | Befehl | Notizen |
| ------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lokal mit Live-Sync iterieren | `yarn twenty dev` | Überwacht Ihre Dateien und synchronisiert bei jeder Änderung. |
| Einmal synchronisieren und beenden (CI, Skripte, Hooks) | `yarn twenty apply` | Führt einen Build und einen Sync aus und beendet sich anschließend. Fügen Sie `--force` hinzu, um die Bestätigung für destruktive Änderungen zu überspringen. |
| Änderungen **anzeigen, ohne sie anzuwenden** | `yarn twenty plan` | Berechnet und druckt das Diff; schreibt nichts. |
| Die App aus dem Workspace entfernen | `yarn twenty app:uninstall` | Fügen Sie `--yes` hinzu, um die Abfrage zu überspringen. |
| Einen Tarball an einen Server ausliefern | `yarn twenty app:publish --private` | Erfordert eine strikt höhere `package.json`-Version – siehe [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing). |
| Im Marketplace (npm) veröffentlichen | `yarn twenty app:publish` | — |
| Eine bereitgestellte Version installieren/aktualisieren | `yarn twenty app:install` | Installiert die aktuell bereitgestellte Version. |
| Den lokalen Server zurücksetzen und sauber neu starten | `yarn twenty docker:reset` | Löscht **alle** lokalen Daten – letztes Mittel. |
<Note>
`yarn twenty dev --once` und `yarn twenty dev --once --dry-run` funktionieren weiterhin als veraltete Aliase für `yarn twenty apply` und `yarn twenty plan`.
</Note>
### Lokaler Sync benötigt keinen Versionssprung
Die strikt steigende `version`-Regel (`VERSION_ALREADY_EXISTS` beim Deploy, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` bei der Installation) gilt für **`app:publish` / `app:install`** – den Release-Pfad. `yarn twenty dev` synchronisiert Ihr Manifest an Ort und Stelle und erfordert niemals eine Versionsänderung, sodass Sie `package.json` nicht anfassen müssen, um zu iterieren. Wenn Sie die Version erhöhen, um eine lokale Änderung zu testen, verwenden Sie den Release-Pfad, obwohl Sie den Entwicklungszyklus benötigen.
## Die Sync-Ausgabe lesen
Jeder Sync gibt die Metadatenänderungen aus, die angewendet wurden (oder angewendet würden, mit `plan`), im Terraform-Stil – ein Block pro Entity mit ihren Attributen, danach eine zusammenfassende Zeile:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
Dies ist Ihre erste Diagnose: Sie zeigt Ihnen genau, welche Objekte, Felder und Layouts sich geändert haben, sodass Sie bestätigen können, dass ein Sync das Erwartete getan hat, bevor Sie die UI prüfen.
Destruktive Änderungen (`to destroy`) werden zusammen mit dem, was sie entfernen, aufgeführt (z. B. `objectMetadata "auditNote" — drops the table and all its rows`) und erfordern eine interaktive Bestätigung oder `--force` in Skripten.
Wenn ein Sync bei einer einzelnen Entität fehlschlägt, nennt der Fehler die betreffende Entität und ihren `universalIdentifier`, zum Beispiel:
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Verwenden Sie diesen Bezeichner, um die Entität in Ihrem Manifest (und bei Bedarf im Workspace) zu finden, anstatt zu raten, welche in Konflikt steht.
## Änderungen vorab ansehen (Plan)
`yarn twenty plan` baut Ihr Manifest, fragt den Server nach dem Migrationsplan und gibt ihn aus – **ohne irgendetwas anzuwenden**. Dies ist der sichere Weg, um zu beantworten: "Was würde dieser Sync ändern?", bevor Sie sich darauf festlegen.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Ein Plan:
* **Schreibt nichts** – keine Metadatenmigration, kein Update von App-Einträgen, keine Änderungen an Standardrollen/-Tabs und keine API-Client-Generierung.
* Liefert dasselbe **Diff**, das ein echter Sync anwenden würde, sodass Sie erstellte/aktualisierte/gelöschte Entitäten im Voraus prüfen können.
* Ist nützlich vor einer riskanten Änderung, bei der Überprüfung einer KI-generierten Änderung oder in einem Skript, das fehlschlagen soll, wenn eine unerwartete Änderung kurz vor der Anwendung steht.
<Note>
Ein Plan zeigt nur **Metadaten**-Änderungen an und erfordert, dass die App mindestens einmal synchronisiert wurde (damit der Workspace sie kennt). Wenn Sie ihn gegen eine App ausführen, die noch nie synchronisiert wurde, meldet der Server, dass die App nicht installiert ist – führen Sie zuerst einmal `yarn twenty dev` aus.
</Note>
## Wiederherstellungsleiter
Wenn lokale Metadaten falsch aussehen, eskalieren Sie in dieser Reihenfolge und stoppen Sie, sobald Sie nicht mehr blockiert sind. Jeder Schritt ist störender als der vorherige.
1. **Erneut synchronisieren.** Führen Sie `yarn twenty apply` erneut aus. Syncs sind idempotent – das erneute Ausführen eines sauberen Manifests ist sicher und löst oft eine vorübergehende Störung.
2. **Plan ansehen.** Führen Sie `yarn twenty plan` aus, um genau zu sehen, was der nächste Sync zu ändern beabsichtigt, ohne es anzuwenden.
3. **Benannten Fehler lesen.** Wenn ein Sync fehlschlägt, notieren Sie sich den Metadatentyp und den `universalIdentifier` in der Meldung (siehe oben) und lokalisieren Sie diese Entität in Ihrem Manifest. Ein Konflikt weist in der Regel auf einen doppelten oder wiederverwendeten Bezeichner hin.
4. **Deinstallieren und neu installieren.** `yarn twenty app:uninstall`, dann erneut synchronisieren (`yarn twenty dev`). Dies baut die Metadaten der App aus einem sauberen Zustand wieder auf, während der Rest Ihres Workspaces intakt bleibt.
5. **Vollständiger Reset (letztes Mittel).** `yarn twenty docker:reset`, dann erneut seeden und synchronisieren.
<Warning>
`yarn twenty docker:reset` löscht **alle** Daten in Ihrer lokalen Instanz – jeden Workspace, jeden Eintrag und jede App. Verwenden Sie ihn nur, wenn die vorherigen Schritte fehlgeschlagen sind.
</Warning>
<Note>
Auf einen Metadatenfehler gestoßen? Bitte [öffnen Sie ein Issue](https://github.com/twentyhq/twenty/issues/new/choose) und fügen Sie die fehlerhafte Migrationsmeldung (mit ihrem Metadatentyp und `universalIdentifier`), die `Metadata changes`-Ausgabe aus dem Sync sowie die von Ihnen ausgeführten Befehle bei.
</Note>
## Gleichzeitige Syncs auf einem Workspace vermeiden
Syncing wendet Metadatenmigrationen an. Mehrere Sync-, Deploy- oder Installationsvorgänge gleichzeitig gegen **denselben Workspace** auszuführen – zum Beispiel mehrere Terminals oder KI-Agenten, die parallel iterieren – kann diese Migrationen verschachteln und Metadaten in einem teilweise angewendeten Zustand zurücklassen.
Der Server serialisiert Syncs pro Workspace, um dies zu verhindern, aber Sie sollten dennoch sensible Metadatenoperationen über einen **einzelnen** Prozess leiten, anstatt sie gleichzeitig auszulösen. Wenn Sie die Entwicklung mit mehreren Agenten orchestrieren, leiten Sie deren Sync-/Deploy-/Install-Aufrufe durch eine Queue, sodass immer nur einer gleichzeitig läuft.
## Fehler voneinander unterscheiden
Wenn etwas schiefläuft, ermöglichen Ihnen das Metadaten-Diff und benannte Fehler, den Fehler einzuordnen:
* **Manifest-Build-Fehler** – die CLI schlägt vor dem Sync fehl (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); beheben Sie den Quellcode Ihrer App.
* **Sync-/Migrationsfehler** – der Build ist erfolgreich, aber das Anwenden des Diffs schlägt fehl und nennt die Entität und den `universalIdentifier`; beheben Sie die widersprüchlichen Metadaten.
* **Abhängigkeitsgrößenfehler** — die Synchronisierung oder Installation schlägt fehl, weil die Produktions-`dependencies` der App zu groß sind, um sie als Runtime-Layer zu installieren (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`); verschieben Sie Pakete, die Ihre Logikfunktionen zur Laufzeit nicht importieren (UI-Bibliotheken, Dev-Tools), in die `devDependencies`.
* **App-Code-Laufzeitfehler** — die Synchronisierung ist erfolgreich, aber Ihre Logikfunktionen oder Komponenten verhalten sich zur Laufzeit unerwartet; überprüfen Sie die [Funktionsprotokolle](/l/de/developers/extend/apps/operations/cli).
* **Lokaler Instanzzustand** — nichts davon trifft zu und der Workspace sieht immer noch falsch aus; arbeiten Sie die Wiederherstellungsleiter nach unten ab.

View File

@@ -0,0 +1,264 @@
---
title: Tests
description: Vitest-Setup, Integrationstests gegen einen realen Twenty-Server, Typprüfung und CI mit GitHub Actions.
icon: flask
---
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](https://vitest.dev/) 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.
## 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](https://esbuild.github.io/) gebündelt, das deren Abhängigkeiten in die Ausgabe einbettet — zur Laufzeit sind keine `node_modules` erforderlich.
### Ein Paket installieren
```bash filename="Terminal"
yarn add axios
```
Importieren Sie es anschließend in Ihrem Code:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
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:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
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 verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Importierte Pakete werden in das Bundle eingebettet, mit Ausnahme der unten aufgeführten.
**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 npm-Pakete, die in einer Browserumgebung funktionieren. Beachte, dass die Sandbox nur ein *teilweises* DOM implementiert, sodass ein Paket sauber gebaut werden kann und trotzdem zur Laufzeit fehlschlägt; siehe [Aktuelle Einschränkungen](/l/de/developers/extend/apps/layout/front-components#current-limitations).
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.
Frontend-Komponenten können Abhängigkeiten auch über ein [Bundle für gemeinsame Abhängigkeiten](/l/de/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) teilen. Pakete, die im Feld `frontComponentSharedDependencies` der package.json der App aufgeführt sind, werden einmal in eine separate Datei gebaut und zur Laufzeit daraus geladen, anstatt in jede Komponente eingebettet zu werden. Dadurch enthält eine Komponente, die nur gemeinsame Pakete importiert, fast keinen eigenen Abhängigkeitscode.
## Einrichtung
Die erzeugte App enthält bereits Vitest. Wenn Sie es manuell einrichten, installieren Sie die Abhängigkeiten:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Erstellen Sie eine `vitest.config.ts` im Stammverzeichnis Ihrer App:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Erstellen Sie eine globale Setup-Datei, die überprüft, ob der Server erreichbar ist, eine Testkonfiguration für das SDK schreibt (`~/.twenty/config.test.json`) und die App synchronisiert, bevor die Tests ausgeführt werden:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## Programmgesteuerte SDK-APIs
Der Subpfad `twenty-sdk/cli` exportiert Funktionen, die Sie direkt aus Testcode aufrufen können:
| Funktion | Beschreibung |
| -------------- | --------------------------------------------------------------------------- |
| `appBuild` | Die App bauen und optional ein Tarball erstellen |
| `appDeploy` | Ein Tarball auf den Server hochladen |
| `appDevOnce` | Erstellt und synchronisiert die App einmal (entspricht `yarn twenty apply`) |
| `appInstall` | Die App im aktiven Arbeitsbereich installieren |
| `appUninstall` | Die 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:
```ts 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:
```bash filename="Terminal"
yarn test
```
Oder im Watch-Modus während der Entwicklung:
```bash filename="Terminal"
yarn test:watch
```
## Typprüfung
Sie können die Typprüfung Ihrer App auch ohne Tests ausführen:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Dies führt `tsc --noEmit` unter Verwendung der `tsconfig.json` Ihrer App aus und meldet etwaige Typfehler. Vom Scaffolding-Tool erzeugte Apps liefern außerdem ein `yarn typecheck`-Skript mit, das auch Testdateien abdeckt (`tsconfig.spec.json`).
## CI mit GitHub Actions
Das Scaffolding-Tool erzeugt einen einsatzbereiten Workflow unter `.github/workflows/ci.yml`. Bei jedem Push auf `main` und jeder Pull-Request startet es einen kurzlebigen Twenty-Server im Runner (über die Aktion `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`) und führt anschließend `yarn lint`, `yarn typecheck`, `yarn test:unit` und `yarn test` aus, wobei `TWENTY_API_URL` / `TWENTY_API_KEY` auf diesen Server verweisen. Es sind keine Geheimnisse erforderlich, und Sie können die Serverversion über die Umgebungsvariable `TWENTY_VERSION` oben im Workflow fixieren.
Unter [Veröffentlichen → Automatisiertes CI/CD](/l/de/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) finden Sie eine vollständige Schritt-für-Schritt-Anleitung zu den drei eingerichteten Workflows (`ci.yml`, der `cd.yml`-Bereitstellungspipeline und `publish.yml` für die npm-Veröffentlichung).

View File

@@ -0,0 +1,135 @@
---
title: Übersetzungen
description: Veröffentlichen Sie Ihre App in mehreren Sprachen – übersetzen Sie Manifest-Labels und UI-Zeichenketten über einen einzigen Locale-Katalog.
icon: language
---
Twenty-Apps werden auf **Englisch** erstellt: Die Zeichenketten in Ihrem Quellcode und im Manifest
sind der englische Ausgangstext, `en` ist das Quell‑Locale, aus dem Sie übersetzen, und
jedes Locale, das unübersetzt bleibt, fällt darauf zurück.
Ihre App enthält zwei Arten von übersetzbarem Text, und beide laufen durch denselben
`locales/`-Katalog:
* **Manifest-Labels** – Objekt- und Feldnamen, Sichtetitel, Menüpunkte und
andere Zeichenketten, die in den Metadaten Ihrer App deklariert sind.
* **Front-Komponenten-Zeichenketten** – der UI-Text, den Ihre React-Front-Komponenten rendern.
Sie markieren die übersetzbaren Zeichenketten, extrahieren sie in sprachspezifische Kataloge,
übersetzen diese Kataloge, und der Build stellt die richtige Sprache für den
aktuellen Benutzer bereit – ohne zusätzlichen Aufwand.
## Markieren von Front-Komponenten-Zeichenketten
Importieren Sie die Übersetzungshelfer aus `twenty-sdk/front-component`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Wann Sie welches verwenden sollten
* **`Trans`** – statischer Text in JSX. Verwenden Sie die Props `message` und `values`
zur Interpolation, wie in `Trans message="Hi {name}" values={{ name }}`;
eine direkte Interpolation in den Children ist nicht statisch extrahierbar.
* **`useTranslate().t`** – dynamische Zeichenketten innerhalb einer Komponente. Wird neu gerendert, wenn der
Benutzer die Sprache wechselt. Bevorzugen Sie dies innerhalb von Render-Funktionen.
* **`t(...)`** (direkt importiert) – sofortige Übersetzung, die **überall** verwendet werden kann,
einschließlich Event-Handlern, Helfern und im Modul-Scope – nicht nur innerhalb von Render-Funktionen.
* **`msg(...)`** – ein Lazy-Descriptor für Zeichenketten, die als Daten (Konstanten,
Konfiguration) deklariert werden. Lösen Sie ihn später mit `t(descriptor)` auf.
### Kontext
Übergeben Sie `context`, um identische Quellzeichenketten zu unterscheiden,
die sich unterschiedlich übersetzen lassen:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Kontextbezogene Zeichenketten erscheinen in Gebietsschema-Katalogen als Gruppe, die über ihren Kontext geschlüsselt ist: `"\<context>": { "\<message>": "\<translation>" }`. Manifestbeschriftungen sind ihrer Konstruktion nach kontextabhängig – jede von ihnen trägt die Rolle, die sie in den Metadaten spielt (`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), sodass dasselbe Wort sich als Objektname und als Ansichtsname unterschiedlich übersetzen kann.
## Extrahieren und Übersetzen
Führen Sie den Extract-Befehl aus Ihrem App-Verzeichnis aus:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
Die Extraktion sammelt sowohl Ihre Manifest-Labels als auch die `t()`-/`msg()`-/`Trans`-
Zeichenketten aus Ihrem Front-Komponenten-Quellcode in `locales/\<locale>.json`, indiziert nach der Quellzeichenkette. Füllen Sie die Übersetzungen aus:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Kontextfreie Zeichenketten sind flache Einträge; kontextbezogene Zeichenketten – einschließlich jeder Manifestbeschriftung – sind unter ihrem Kontextschlüssel verschachtelt.
Platzhalter wie `{name}` werden zur Laufzeit ersetzt – behalten Sie sie in der
Übersetzung bei. Jede Zeichenkette, die leer bleibt, fällt auf den Quelltext zurück.
## Wie es ausgeführt wird
`twenty dev:build` kompiliert die Kataloge und stellt die richtige Sprache für den aktuellen Benutzer bereit: Manifest-Labels werden serverseitig aufgelöst und Front-Komponenten-Kataloge werden in jedes Komponenten-Bundle eingebunden. Zur Laufzeit liest eine Komponente die
Locale aus ihrem Ausführungskontext (der aktuellen Sprache des Hosts) und löst
jede Zeichenkette gegen ihren Katalog auf, wobei auf die Quelle zurückgegriffen
wird, wenn eine Übersetzung fehlt. Das Umschalten der Sprache im Host rendert `Trans`- und
`useTranslate().t`-Zeichenketten live neu.
Manifest-Bezeichnungen werden bei jeder Synchronisierung kompiliert – bei `twenty dev:build`, `twenty apply`
und der fortlaufenden `twenty dev`-Überwachung –, sodass das Bearbeiten einer Locale-Datei und
das Synchronisieren ausreichen, um den neuen Text zu sehen. Front-Komponentenkataloge werden zur Build-Zeit in
jedes Komponenten-Bundle integriert, sodass das Ändern eines davon weiterhin das
erneute Ausführen von `twenty dev:build` (und ein erneutes Deployment) erfordert.
`Trans`-Text-Children können sich über mehrere Zeilen erstrecken – Leerraum wird auf dieselbe Weise zusammengefasst, wie JSX ihn zusammenfasst, sodass beide zum Schlüssel `Welcome back` extrahiert werden:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,81 @@
---
title: 5. An AI agent
icon: robot
description: Lassen Sie einen Agenten Dokumente mit Hilfe Ihres Tools aus einem Chat generieren.
---
Weil `generate-document` als **Tool** exponiert wird, kann ein KI-Agent es aufrufen.
Fügen wir einen Agenten und eine Fertigkeit hinzu, damit Benutzer einfach *"einen Vorschlag für
Jeffery Griffin"* erstellen können.
## Die Fähigkeit
Eine [skill](/l/de/developers/extend/apps/logic/skills-and-agents) ist wiederverwendbare
Anleitung — Wissen, das Sie Agenten anhängen. Unser lehrt das Modell, wie man das Werkzeug
benutzt.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## Der Agent
Ein [agent](/l/de/developers/extend/apps/logic/skills-and-agents) paßt einen Prompt mit einem
Modell. Setze `responseFormat` explizit, um eine Build-Warnung zu vermeiden.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
Der Agent kann das Werkzeug nur aufrufen, wenn es seine Rolle erlaubt. Wir haben bereits
`canAccessAllTools: true` und `canBeAssignedToAgents: true` über die Rolle der App in
[Chapter 2](/l/de/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Testen
Öffne einen Chat mit dem **Dokumenten-Assistent** und ersuche ihn, ein Dokument für eine
-Person in deinem CRM zu erstellen. Es findet den Datensatz, ruft `generate-document` auf und meldet das erstellte Dokument
zurück — das nun in der Ansicht **Dokumente** erscheint
gleicht dem Kommandomenü und dem Workflow-Pfad.
Das ist die Auszahlung der Logik als Werkzeug: **eine Funktion, viele Eingangstüren** —
Befehlsmenü, HTTP, Workflow-Schritt und jetzt natürliche Sprache.
**Nach diesem Schritt:** ist die App komplett und wirklich nützlich. Zeit bis
es verschickt wird.
<Card title="Weiter: Publizieren →" icon="rocket" href="/l/de/developers/extend/apps/tutorials/document-generator/publishing">
Marktplatz-Metadaten hinzufügen und veröffentlichen.
</Card>

View File

@@ -0,0 +1,307 @@
---
title: 4. Erstelle die UI
icon: table-columns
description: Views, Sidebar Navigation, ein Befehl und Frontkomponenten.
---
Im Moment sind die Objekte nur über Einstellungen erreichbar. Geben wir der App eine
echte Präsenz in der Benutzeroberfläche: Listenansichten, Seitenleisteneinträge, ein Ein-Klick-
**Dokumenten** Befehl generieren, eine Front-Komponente der Eintragsseite für **Vorschau** eines
Dokuments und einen nativen Reiter für den Volltext **Editor** für Vorlagen.
## Ansichten und Navigation
Eine [view](/l/de/developers/extend/apps/layout/views) ist eine gespeicherte Liste eines bestimmten Objekts.
Ein [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items)
bringt diese Ansicht in die Sidebar. Der Server stellt für jedes Objekt bereits automatisch die Standardansicht "Alle ..." bereit, daher sind die von einer App definierten Ansichten zusätzliche, kuratierte Ansichten.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Fügen Sie das gleiche Paar für Vorlagen hinzu. Beide zeigen nun in der Seitenleiste:
<Frame caption="Dokumente und Vorlagen in der Seitenleiste mit dem generierten Dokument.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Dokumentenansicht mit einem generierten Dokument" />
</Frame>
## Eine Frontkomponente
Eine [Frontkomponent](/l/de/developers/extend/apps/layout/front-components) ist eine Reaktions-
Komponente mit Sandkasten in Twenty. Wir liest den ausgewählten Datensatz ein, lädt die
Personen-Vorlagen über `CoreApiClient` und POSTs aus dem letzten
Kapitel.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Stil mit Inline-CSS-Variablen (`var(--t-color-blue)`), nicht aus
`twenty-ui` importiert. Das SDK verspottet das Paket während des Builds, so dass Modul-Level-Importe von
Theme-Konstanten `undefiniert` wären. Siehe
[vollständige Komponente](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Ein Befehl um es zu öffnen
Ein [Commandmenu item](/l/de/developers/extend/apps/layout/command-menu-items) mit
`availabilityType: 'RECORD_SELECTION'` wird angezeigt, wenn eine Person ausgewählt ist, und
öffnet die Komponente in der Seitenleiste.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Den gesamten Fluss testen
Öffne **People**, markiere eine Person und drücke <kbd>⌘K</kbd> / <kbd>Strg K</kbd>.
"Dokument erstellen" erscheint, mit Ihrer App markiert:
<Frame caption="Der Befehl wird angezeigt, wenn eine Person ausgewählt ist.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Befehlsmenü mit Dokument generieren" />
</Frame>
Führen Sie es aus — Ihre Komponente öffnet sich in der Seitenleiste. Wähle eine Vorlage, klicke auf
**Generieren**, und ein neues Datensatzland in **Dokumenten**.
<Frame caption="Die Frontkomponente, das Laden von Vorlagen und das Generieren auf Klick.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Dokumentseite erstellen" />
</Frame>
Jedes generierte Dokument speichert Ihre App als Autor auf:
<Frame caption="Erstellt vom Dokumentengenerator, Status generiert.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Ein generierter Dokumentensatz" />
</Frame>
## Vorschau eines Dokuments auf seiner Aufzeichnungsseite
Eine Frontkomponente ist nicht nur für Kommandomenüs – Sie können sie als **Tab auf einer
Aufnahmeseite einhängen**. Fügen wir dem Dokument-Datensatz einen Tab *Vorschau* hinzu, der den
Markdown-Text als polierte, druckbare Seite darstellt.
Die Komponente liest die aktuelle Datensatz-ID aus ihrem Ausführungskontext, lädt das
-Dokument und gibt sie aus. Frontkomponenten laufen in einer **Sandbox**, die nur eine
Whitelist von HTML-Tags erlaubt — Roh-HTML-Einspritzung (`dangerouslySetInnerHTML`) und
`\<style>` werden blockiert — so dass wir Markdown als React-Elemente mit Inline-
Styles über einen kleinen [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
Helfer machen.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Mounten Sie es mit einem [page layout](/l/de/developers/extend/apps/layout/page-layouts). Ein
`RECORD_PAGE`-Layout fügt der Datensatzansicht eines Objekts Tabs hinzu; ein Tab mit einem einzelnen
`FRONT_COMPONENT`-Widget zeigt die Komponente randlos in voller Breite an:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Öffne jedes Dokument — ein **Vorschau** Tab macht es wunderschön, mit Links zur
freigegebenen Webseite und dem PDF:
<Frame caption="Auf der Registerkarte Vorschau wird das Dokument mit Inline-Styles und Schnelllinks dargestellt.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Front-Komponente des Dokuments in einem Reiter der Datensatzseite" />
</Frame>
## Vorlage mit dem Rich-Text-Editor bearbeiten
Templates benötigen überhaupt keine eigene Komponente. Da der `body` ein
`RICH_TEXT`-Feld ist, stellt Twenty bereits einen vollständigen Rich-Text-Editor dafür bereit – denselben, den auch die Standardobjekte Note und Task verwenden. Wir Oberflächen es nur auf der
Template-Datensatzseite.
Füge einen Tab mit einem `FIELD` Widget im `EDITOR` Anzeigemodus hinzu und zeigt auf das Feld `body`
über `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Ein `RICH_TEXT` Feld speichert sowohl den Editorblock JSON als auch eine Markdown
Projektion. Die Generation-Pipeline liest diese Markdown-Projektion, also
Platzhalter, die PDF, und die freigebbare Webseite arbeiten alle unverändert —
sehen Sie den vollständigen
[`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
`FIELD`-Widgets unterstützen auch andere Darstellungsmodi und Optionen, darunter
das Auflisten von Datensätzen einer verschachtelten Beziehung, die zwei Ebenen entfernt ist — siehe die
[Field-Widget-Referenz](/l/de/developers/extend/apps/layout/page-layouts#field-widgets).
Jetzt schreiben Editoren Vorlagen in einem korrekten Rich-Text-Editor:
<Frame caption="Die Registerkarte Template: Der native Rich-Text-Editor von 20 ist an das Bodyfeld gebunden.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Vorlageneintrag mit dem nativen Rich-Text-Editor-Tab" />
</Frame>
**Nach diesem Schritt:** Dokumente Vorschau wunderschön und Vorlagen sind editierbar
in-app. Lassen Sie als nächstes einen AI Agenten aus einem Chat generieren.
<Card title="Weiter: ein KI-Agent →" icon="robot" href="/l/de/developers/extend/apps/tutorials/document-generator/ai-agent">
Fügen Sie einen Agenten und eine Fertigkeit hinzu, die Ihr Werkzeug aufruft.
</Card>

View File

@@ -0,0 +1,135 @@
---
title: 1. Datenmodell
icon: database
description: Musterdokumente und Vorlagen mit Objekten, Feldern und einer Relation.
---
Unsere App benötigt zwei benutzerdefinierte Objekte: **Dokumentvorlagen** (was geschrieben werden soll) und
**Dokumente** (das generierte Ergebnis). Legen wir sie fest.
Jede Entitäts-Datei mit dem CLI entpacken — es erzeugt eine gültige UUID und den richtigen
-Ordner für Sie:
```bash filename="Terminal"
yarn twenty dev:add object
```
Unten zeigen wir die fertigen Dateien.
<Note>
Jede `*_UNIVERSAL_IDENTIFIER` Konstante lebt in
`src/constants/universal-identifiers.ts` und wird importiert, wo verwendet. Die Snippets
unten lassen diese importieren, um kurz zu sein – halten Sie sie in Ihren eigenen Dateien.
</Note>
## Das Template-Objekt
Eine Vorlage hat einen `name`, einen `body` mit `{{placeholders}}`, und ein `target`, das
sagt, ob es für eine Person oder ein Unternehmen geschrieben wurde. Das Feld `body` ist ein
`RICH_TEXT` Feld. Zwanzig gibt ihm also einen vollwertigen Rich-Text-Editor.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
`SELECT` Option **Werte** muss `UPPER_CASE` (`PERSON`, nicht `person`) sein und die
`defaultValue` ist in extra Anführungszeichen eingewickelt: `` `'PERSON'` ``. Das `label` ist das, was
Benutzer sehen.
</Warning>
## Das Dokumentenobjekt
Das generierte Dokument speichert den gerenderten `content` und einen `status`. Definiere es
auf die gleiche Weise, mit einer `status` Auswahl von `DRAFT` / `GENERATED`. Vollständige Datei:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Verknüpfung mit einer Beziehung
Jedes Dokument sollte auf die Vorlage verweisen, aus der es stammt. Beziehungen sind
**bidirectional** — Sie definieren beide Seiten, jede in ihrer eigenen Felddatei.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
Die andere Seite (`template-documents-relation.field.ts`) ist ein
`RelationType.ONE_TO_MANY` Feld mit dem Namen `documents`, das den entgegengesetzten Weg weist.
Siehe [Relations](/l/de/developers/extend/apps/data/relations) für das vollständige Muster.
## Sehen Sie es in 20
Wenn `yarn twenty dev` läuft, öffnen Sie **Einstellungen → Datenmodell**. Beide Objekte erscheinen
, markiert mit deiner App.
<Frame caption="Beide benutzerdefinierte Objekte, die der Document Generator App gehören.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Datenmodell-Einstellungen zeigen Dokumente und Dokumentvorlagen an" />
</Frame>
Erstellen Sie eine Vorlage zum Testen mit — Name sie *Verkaufsvorschlag*, setzen Sie **Ziel** auf
*Person*, und fügen Sie einen Körper mit ein paar Platzhaltern ein:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Ein Vorlageneintrag. Der Körper behält seine Platzhalter bis ein Dokument generiert ist.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Ein Verkaufsvorschlagvorlagen-Eintrag mit Platzhalterkörper" />
</Frame>
**Nach diesem Schritt:** hast du `documentTemplate` und `document` Objekte, verlinkt von
eine Relation, aus der eine Vorlage generiert werden kann. Als nächstes folgt die Logik, die sie ausfüllt.
<Card title="Weiter: Dokumente generieren →" icon="bolt" href="/l/de/developers/extend/apps/tutorials/document-generator/generating-documents">
Schreibe die logische Funktion, die das Template füllt.
</Card>

View File

@@ -0,0 +1,237 @@
---
title: 2. Dokumente generieren
icon: bolt
description: Eine Logikfunktion, die als KI-Werkzeug und Workflow-Aktion dargestellt wird.
---
Jetzt der Kern: eine [Logikfunktion](/l/de/developers/extend/apps/logic/logic-functions)
, die eine Vorlage und einen Datensatz lädt, die Platzhalter füllt und ein neues
Dokument speichert.
Wir werden die Geschäftslogik einmal als **Handler** schreiben und sie dann durch
mehrere Trigger ausblenden. Dieses Kapitel verbindet zwei davon – ein **AI-Tool** und eine
**Workflow-Aktion**.
## Der Rendering-Helfer
Behalten Sie die reine Logik in ihrer eigenen Datei, so dass es leicht zu Unit-Test ist. Dies flattert einen Eintrag
in `{{dot.path}}` Token und ersetzt diese.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
Da diese Datei keine Nebenwirkungen hat, können Sie sie mit schnellen Einheitstests
(`Garn test:unit`) bedecken. Siehe [Testing](/l/de/developers/extend/apps/operations/testing).
</Tip>
## Der Handler
Der Handler verwendet die generierte [`CoreApiClient`](/l/de/developers/extend/apps/logic/logic-functions)
zum Lesen und Schreiben von CRM-Daten. Es lädt die Vorlage, lädt den Zieldatensatz, füllt den Körper
und erstellt ein "Dokument".
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues` führt eine andere Abfrage für eine Person gegen eine Firma aus und flattert
das Ergebnis — siehe
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## Als Werkzeug und Workflow-Aktion anzeigen
Eine einzelne `defineLogicFunction` kann mehrere Trigger tragen. Hier macht `toolTriggerSettings`
es von KI-Agenten aufrufbar und `workflowActionTriggerSettings` verwandelt es in einen
Schritt im visuellen Workflow-Builder. Beide beschreiben ihre Eingabe mit einem JSON-Schema.
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
Das Eingabeschema ist ein einfaches JSON-Schema, das `templateId` und `recordId` —
siehe [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## Zugriff gewähren
Die logischen Funktionen laufen als Rolle der App. Es muss Vorlagen lesen und Datensätze
und Dokumente erstellen, also erlauben Sie dies in `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE` lässt die Funktion das generierte PDF im nächsten Abschnitt hochladen.
Siehe [Roles](/l/de/developers/extend/apps/config/roles) für feinkörnige Berechtigungen.
## Eine echte PDF-Datei anhängen
Ein gerendertes Textfeld ist nützlich, aber Benutzer wollen ein echtes Dokument. Generieren wir ein
**PDF** und speichern es als herunterladbare Datei.
Gib zuerst das `document` Objekt ein `FILES`-Feld um die PDF zu halten. Apps laden
in ihre **eigenen** Datei-Felder hoch. Daher ist dieses Feld was das Hochladen leitet:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
Rendern Sie nun diese PDF. Eine App ist ein echtes Knoten-Projekt, so dass Sie jedes npm
Paket hinzufügen und es wie überall sonst importieren können. Wir verwenden **[pdf-lib](https://pdf-lib.js.org/)**
um die PDF zu zeichnen und **[marked](https://marked.js.org/)** um den Markdown
Körper zu analysieren — der CLI installiert sie in die Laufzeit der Funktion für Sie:
```bash filename="Terminal"
yarn add pdf-lib marked
```
Der ganze Helfer ist
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Es analysiert Markdown in Tokens mit `markiert. exer`, legt sie dann mit
pdf-lib: echte Überschriften, **fett**/*kursiv* läuft, Kugel und nummerierte Listen,
Blockzitate und Regeln — eine polierte, mehrseitige A4 Darstellung der Vorlage
selbst statt einer Wand aus Text.
<Frame caption="Die generierte PDF: echte Typografie und Markdown Formatierung, Darstellung des Template-Körpers.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Ein poliertes, markierbares PDF" />
</Frame>
<Note>
Die in pdf-lib integrierten Schriftarten verwenden die WinAnsi-Codierung, sodass westeuropäische Akzentzeichen standardmäßig korrekt dargestellt werden; die Hilfsfunktion ordnet typografische Anführungszeichen und Gedankenstriche zu und verwirft Zeichen, die nicht codiert werden können. Das Rendern nicht-lateinischer Skripte (chinesisch, arabisch, kyrillisch) würde bedeuten, dass
eine Unicode-Schriftart einbettet.
</Note>
Dann laden Sie es hoch und speichern Sie die Referenz auf dem Datensatz. `uploadFile` ruft Bytes
in dein app-owned Datei-Feld zurück; die zurückgegebene `id` ist, was du speicherst:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
Das generierte Dokument enthält jetzt ein herunterladbares PDF:
<Frame caption="Die erzeugte PDF, die im Dateifeld des Dokuments gespeichert ist.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Ein Datensatz mit einer generierten PDF-Datei" />
</Frame>
<Note>
`uploadFile` zielt nur auf **app-besitzer** Datei-Felder (Uploads erfordern also immer eine
-App, die das Feld besitzt, plus das `UPLOAD_FILE` Rollenflag). Aus diesem Grund landet das PDF
auf das eigene Feld `file` des Datensatzes — das gleiche Muster wie die
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
verwendet für Aufnahmen.
</Note>
**Nach diesem Schritt:** Jedes generierte Dokument hat eine echte, herunterladbare PDF. Aber
nichts kann den Generator noch von der Oberfläche *aufrufen* – dafür benötigen wir eine HTTP-Route.
<Card title="Weiter: HTTP-Routen →" icon="globe" href="/l/de/developers/extend/apps/tutorials/document-generator/http-routes">
Servieren Sie die Funktion über HTTP und rendern Sie Dokumente als Webseiten.
</Card>

View File

@@ -0,0 +1,149 @@
---
title: 3. HTTP-Routen
icon: globe
description: Trigger die Funktion über HTTP und rendern Sie Dokumente als Webseiten.
---
Der gleiche Handler kann auch HTTP-Anfragen beantworten. Wir werden zwei Routen hinzufügen:
* ein **POST** Endpunkt der UI-Aufrufe, um ein Dokument zu generieren, und
* ein öffentlicher **GET** Endpunkt, der ein Dokument als druckbare Webseite darstellt.
Beide verwenden `httpRouteTriggerSettings`. App-Routen werden über einen `/s/`-Pfad adressiert; auf lokalen
und selbstgehosteten Instanzen wird dieser Pfad von Ihrem Twenty-Server
bereitgestellt (z. B. `http://localhost:2020/s/documents/generate`). In Twenty Cloud löst `RestApiClient` den Pfad über die Funktionen-Domain des Workspaces auf.
## POST-Route — bei Bedarf generieren
Dies verwendet `generateDocumentHandler`, also gibt es keine Logik zu wiederholen — nur ein dünner
-Adapter, der den Request-Körper liest.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
Der Shared-Handler gibt einen vorgeschlagenen `status` bei einem Fehler zurück, so dass die Route
mit einem richtigen `4xx`/`5xx` Code antworten kann. `isAuthRequired: true` bedeutet, dass der Anrufer
ein gültiges Token vorweisen muss — die vorderste Komponente im nächsten Kapitel übergeht automatisch das Zugriffstoken des
Benutzers.
## GET-Route — als Webseite rendern
Um HTML anstelle von JSON zurückzugeben, wickeln Sie den Körper in eine `Response` mit einem
`Content-Type` Header. Diese Route ist öffentlich (`isAuthRequired: falsch`), so dass ein
generiertes Dokument als Link freigegeben werden kann.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` stellt den Markdown-Text in HTML dar (mit [marked](https://marked.js.org/),
bereinigt) und lässt ihn in ein Reinigen druckbare Seite, die nur die Inhalte der Vorlage
zeigt — das gleiche Aussehen wie die PDF und die In-App-Vorschau.
[Siehe den Helfer](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Testen
Mit einer Vorlage und einer Person im Arbeitsbereich rufen Sie die Route an (Benutzen Sie ein Token von
**Einstellungen → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Öffnen Sie das zurückgegebene Dokument in Ihrem Browser:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="Die öffentliche GET-Route macht das Dokument als druckbare Seite.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Eine gerenderte Dokumenten-Webseite" />
</Frame>
<Tip>
Sie können auch Protokolle einer Funktion beim Testen mit
`yarn zwanzig dev:function:logs` streamen oder direkt mit
`yarn zwanzig dev:function:exec` aufrufen.
</Tip>
**Nach diesem Schritt:** Die App kann Dokumente über HTTP generieren und sie als
Webseiten bedienen. Jetzt machen wir es brauchbar ohne `curl`.
<Card title="Nächste: Bauen der UI →" icon="table-columns" href="/l/de/developers/extend/apps/tutorials/document-generator/building-the-ui">
Views, Navigation, ein Kommando und eine Frontkomponente.
</Card>

View File

@@ -0,0 +1,67 @@
---
title: "Tutorial: Dokumentgenerator"
icon: wand-magic-sparkles
description: Erstellen Sie eine echte Twenty-App, die personalisierte Dokumente aus Ihren CRM-Daten generiert.
---
In diesem Tutorial erstellen Sie **Document Generator** – eine App, die wiederverwendbare Vorlagen in personalisierte Dokumente umwandelt, indem sie die bereits in Ihrem CRM vorhandenen Daten nutzt.
Schreiben Sie eine Vorlage einmal mit `{{placeholders}}` und generieren Sie dann mit einem Klick ein ausgefülltes Dokument für jede Person oder jedes Unternehmen – über das Befehlsmenü, über einen KI-Agenten oder über einen Workflow.
<Frame caption="Eine Vorlage, die für eine bestimmte Person generiert wurde und als druckbare Seite geöffnet ist.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Ein generiertes Verkaufsangebot" />
</Frame>
## Was Sie lernen werden
Jedes Kapitel fügt eine Funktion hinzu. Am Ende haben Sie den Großteil des SDK kennengelernt.
| Kapitel | Funktion | Referenz |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [1. Datenmodell](/l/de/developers/extend/apps/tutorials/document-generator/data-model) | Objekte, Felder und eine Relation | [Daten](/l/de/developers/extend/apps/data/overview) |
| [2. Dokumente generieren](/l/de/developers/extend/apps/tutorials/document-generator/generating-documents) | Eine Logikfunktion (KI-Tool + Workflow-Aktion), die eine Markdown-Vorlage ausfüllt und ein fertiges PDF anhängt | [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) |
| [3. HTTP-Routen](/l/de/developers/extend/apps/tutorials/document-generator/http-routes) | Ausliefern von JSON und einer teilbaren HTML-Seite über Routen | [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) |
| [4. Die UI erstellen](/l/de/developers/extend/apps/tutorials/document-generator/building-the-ui) | Ansichten, Navigation, Befehlsmenü und Frontend-Komponenten, die eine Vorschau eines Dokuments anzeigen und eine Vorlage bearbeiten | [Layout](/l/de/developers/extend/apps/layout/overview) |
| [5. Ein KI-Agent](/l/de/developers/extend/apps/tutorials/document-generator/ai-agent) | Agent + Skill | [Skills & Agenten](/l/de/developers/extend/apps/logic/skills-and-agents) |
| [6. Veröffentlichen](/l/de/developers/extend/apps/tutorials/document-generator/publishing) | In den Marketplace veröffentlichen | [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing) |
## Voraussetzungen
Sie sollten den [Quick Start](/l/de/developers/extend/apps/getting-started/quick-start) abgeschlossen haben:
einen lokalen Twenty-Server, der auf Port `2020` läuft, und die CLI, die damit authentifiziert ist.
Falls nicht, erstellen und starten Sie jetzt einen:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Lesen Sie lieber den fertigen Code? Die vollständige App befindet sich unter
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Jedes der folgenden Codebeispiele ist daraus kopiert.
</Note>
## Wie die App zusammenpasst
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Eine Vorlage mit Platzhaltern wird in ein fertiges Dokument mit PDF generiert, ausgelöst über das Befehlsmenü, einen KI-Agenten, einen Workflow oder einen teilbaren Link" />
</Frame>
Sie schreiben eine **Vorlage** einmal in einem Rich-Text-Editor mit `{{placeholders}}`. Die Auswahl einer
Vorlage und eines CRM-Datensatzes füllt die Platzhalter aus und speichert ein fertiges
**Dokument** (mit einer PDF-Datei). Alles andere – das Befehlsmenü, der KI-Agent,
der Workflow-Schritt, der teilbare Link – ist nur eine andere Möglichkeit, denselben
Generator auszulösen.
## Diesen Kreislauf am Laufen halten
Lassen Sie `yarn twenty dev` während des gesamten Tutorials in einem Terminal laufen. Jedes Mal, wenn
Sie eine Datei unter `src/` hinzufügen oder bearbeiten, wird sie innerhalb weniger
Sekunden mit Ihrem Server synchronisiert, sodass Sie beobachten können, wie jede Funktion in der UI erscheint, während Sie sie entwickeln.
<Card title="Beginnen Sie mit der Entwicklung →" icon="database" href="/l/de/developers/extend/apps/tutorials/document-generator/data-model">
Kapitel 1: Dokumente und Vorlagen modellieren.
</Card>

View File

@@ -0,0 +1,137 @@
---
title: 6. Veröffentlichen
icon: rocket
description: Fügen Sie Marktplatz-Metadaten hinzu und veröffentlichen Sie Ihre App.
---
Ihre App funktioniert. Der letzte Schritt ist, es für den Marktplatz zu beschreiben und zu veröffentlichen.
## Marktplatz-Metadaten hinzufügen
Die [Anwendung config](/l/de/developers/extend/apps/config/application) trägt die
Identität, die im Marktplatz erscheint: Autor, Kategorie, Logo und Unterstützung von
Links. Lege ein Logo in `public/` ein und verweise es mit `logo`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/de/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
Die Standardrolle wird mit `defineApplicationRole()` in der eigenen Datei deklariert — Sie
geben hier nicht mehr den `defaultRoleUniversalIdentifier` an.
</Tip>
Füge auch das Schlüsselwort "twenty-app" zu "package.json" hinzu, damit die App entdeckt werden kann:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Galerie Screenshots hinzufügen
Ein Marktplatz-Listing verkauft sich mit Screenshots. Lege ein paar PNGs in
`public/gallery/` und verweise sie mit `galleryImages` — sie rendern als
Galerie auf der Listenseite, in Array-Reihenfolge.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Lead with the payoff: make the first screenshot the finished result (a generated
document), then show how it triggered and authored. Verwende scharfe, hochauflösende
Aufnahmen – sie sind das Erste, was ein Benutzer sieht.
</Tip>
Gib `README.md` die gleiche Behandlung — es ist die Titelseite auf npm und GitHub.
Öffnen Sie mit dem Wertvorschlag und einem Screenshot, führen Sie die Überschrift auf,
und halten Sie dann die Baudetails unter dem Ordner.
## Vor dem Versand prüfen
Führe die gleichen Tore CI aus:
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
Der Plan gibt genau aus, was sich auf dem Server ändern würde, ohne die Änderungen anzuwenden —
eine gute abschließende Plausibilitätsprüfung. Siehe
[Testing](/l/de/developers/extend/apps/operations/testing) und
[Synchronisieren & Wiederherstellen](/l/de/developers/extend/apps/operations/sync-and-recovery).
## Veröffentlichen
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` baut und veröffentlicht standardmäßig in npm ; `--private` lädt stattdessen einen
Tarball in die private Registry eines Zwanzig Servers hoch. Um eine veröffentlichte App
auf dem Marktplatz einer Instanz aufzudecken, löst eine Katalog-Synchronisation aus:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Vollständige Details und die Freigabe-Checkliste:
[Publishing](/l/de/developers/extend/apps/operations/publishing).
## Du hast eine App :party_popper erstellt:
In sechs Kapiteln hast du den Großteil der SDK-Oberfläche verwendet:
* **Objekte, Felder und eine Relation** um die Daten zu modellieren
* Eine **Logikfunktion** als **AI-Tool**, eine **Workflow-Aktion** und **HTTP-Routen**
* **Ansichten, Navigation, Befehl und Frontkomponent** für die Benutzeroberfläche
* Ein **Agent + Fertigkeit** für die Erzeugung natürlicher Sprache
* **Marketplace-Metadaten** und der Veröffentlichungsfluss
Die fertige App ist bei
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Wohin Sie weiter gehen
<CardGroup cols={2}>
<Card title="Datenreferenz" icon="database" href="/l/de/developers/extend/apps/data/overview">
Jeder Feldtyp, Relation und Index-Option.
</Card>
<Card title="Logische Referenz" icon="bolt" href="/l/de/developers/extend/apps/logic/overview">
Cron- und Datenbankereignis-Trigger, der Schlüsselwert-Store, OAuth Verbindungen.
</Card>
<Card title="Layout-Referenz" icon="table-columns" href="/l/de/developers/extend/apps/layout/overview">
Seitenlayouts, Dashboard-Widgets und mehr UI-Oberflächen.
</Card>
<Card title="Operationen" icon="rocket" href="/l/de/developers/extend/apps/operations/overview">
CLI, Tests, Fernbedienungen und CI.
</Card>
</CardGroup>

View File

@@ -0,0 +1,147 @@
---
title: APIs
description: Abfragen und ändern Sie Ihre CRM-Daten programmatisch mit REST oder GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty wurde so entwickelt, dass es entwicklerfreundlich ist und leistungsstarke APIs bietet, die sich an Ihr individuelles Datenmodell anpassen. Wir bieten vier verschiedene API-Typen, um unterschiedlichen Integrationsanforderungen gerecht zu werden.
## Entwickler-Erst-Ansatz
Twenty generiert APIs speziell für Ihr Datenmodell:
* **Keine langen IDs erforderlich**: Verwenden Sie Ihre Objekt- und Feldnamen direkt in Endpunkten
* **Standard- und benutzerdefinierte Objekte werden gleich behandelt**: Ihre benutzerdefinierten Objekte erhalten dieselbe API-Behandlung wie integrierte
* **Dedizierte Endpunkte**: Jedes Objekt und Feld erhält seinen eigenen API-Endpunkt
* **Benutzerdefinierte Dokumentation**: Speziell für das Datenmodell Ihres Arbeitsplatzes generiert
<Note>
Ihre personalisierte API-Dokumentation ist nach dem Erstellen eines API-Schlüssels unter **Einstellungen → API & Webhooks** verfügbar. Da Twenty APIs erzeugt, die Ihrem benutzerdefinierten Datenmodell entsprechen, ist die Dokumentation für Ihren Arbeitsbereich einzigartig.
</Note>
## Die beiden API-Typen
### Core API
Zugriff auf `/rest/` oder `/graphql/`
Arbeiten Sie mit Ihren tatsächlichen **Datensätzen** (den Daten):
* Personen, Unternehmen, Opportunities usw. erstellen, lesen, aktualisieren und löschen
* Daten abfragen und filtern
* Verwalten von Datensatzbeziehungen
### Metadata API
Zugriff auf `/rest/metadata/` oder `/metadata/`
Verwalten Sie Ihren **Arbeitsbereich und Ihr Datenmodell**:
* Objekte und Felder erstellen, ändern oder löschen
* Konfigurieren der Arbeitsbereichseinstellungen
* Beziehungen zwischen Objekten definieren
## REST vs GraphQL
Sowohl Core- als auch Metadata-APIs sind in REST- und GraphQL-Formaten verfügbar:
| Format | Verfügbare Vorgänge |
| ----------- | ------------------------------------------------------------------- |
| **REST** | CRUD, Batch-Vorgänge, Upserts |
| **GraphQL** | Dasselbe plus **Batch-Upserts**, Beziehungsabfragen in einem Aufruf |
Wählen Sie je nach Bedarf — beide Formate greifen auf dieselben Daten zu.
## API-Endpunkte
| Umgebung | Basis-URL |
| ----------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Selbsthosting** | `https://{your-domain}/` |
## Authentifizierung
Jede API-Anfrage erfordert einen API-Schlüssel im Header:
```
Authorization: Bearer YOUR_API_KEY
```
### API-Schlüssel erstellen
1. Gehen Sie zu **Einstellungen → APIs & Webhooks**
2. Klicken Sie auf **+ Schlüssel erstellen**
3. Konfigurieren:
* **Name**: Beschreibender Name für den Schlüssel
* **Ablaufdatum**: Wann der Schlüssel abläuft
4. Klicken Sie auf **Speichern**
5. **Sofort kopieren** — der Schlüssel wird nur einmal angezeigt
<VimeoEmbed videoId="928786722" title="API-Schlüssel erstellen" />
<Warning>
Ihr API-Schlüssel gewährt Zugriff auf sensible Daten. Teilen Sie ihn nicht mit nicht vertrauenswürdigen Diensten. Wenn er kompromittiert wurde, deaktivieren Sie ihn umgehend und erstellen Sie einen neuen.
</Warning>
### Einem API-Schlüssel eine Rolle zuweisen
Für mehr Sicherheit weisen Sie eine spezifische Rolle zu, um den Zugriff zu beschränken:
1. Gehen Sie zu **Einstellungen → Mitglieder → Rollen**
2. Klicken Sie auf die Rolle, die Sie zuweisen möchten
3. Öffnen Sie den Tab **Zuweisungen**
4. Unter **API-Schlüssel** auf **+ API-Schlüssel zuweisen** klicken
5. Wählen Sie den API-Schlüssel aus
Der Schlüssel übernimmt die Berechtigungen dieser Rolle. Siehe [Berechtigungen](/l/de/user-guide/permissions-access/capabilities/permissions) für Details.
### API-Schlüssel verwalten
**Neu generieren**: Einstellungen → APIs & Webhooks → Schlüssel anklicken → **Neu generieren**
**Löschen**: Einstellungen → APIs & Webhooks → Schlüssel anklicken → **Löschen**
## API-Playground
Testen Sie Ihre APIs direkt im Browser mit unserem integrierten Playground — verfügbar für **REST** und **GraphQL**.
### Auf den Playground zugreifen
1. Gehen Sie zu **Einstellungen → APIs & Webhooks**
2. API-Schlüssel erstellen (erforderlich)
3. Klicken Sie auf **REST API** oder **GraphQL API**, um den Playground zu öffnen
### Was Sie erhalten
* **Interaktive Dokumentation**: Für Ihr spezifisches Datenmodell generiert
* **Live-Tests**: Führen Sie echte API-Aufrufe gegen Ihren Arbeitsbereich aus
* **Schema-Explorer**: Verfügbare Objekte, Felder und Beziehungen durchsuchen
* **Request-Builder**: Abfragen mit Autovervollständigung erstellen
Der Playground spiegelt Ihre benutzerdefinierten Objekte und Felder wider, sodass die Dokumentation für Ihren Arbeitsbereich stets korrekt ist.
## Batch-Vorgänge
Sowohl REST als auch GraphQL unterstützen Batch-Vorgänge:
* **Batch-Größe**: Bis zu 60 Datensätze pro Anfrage
* **Vorgänge**: Mehrere Datensätze erstellen, aktualisieren, löschen
**GraphQL-Exklusivfunktionen:**
* **Batch-Upsert**: Erstellen oder Aktualisieren in einem Aufruf
* Verwenden Sie Pluralobjektnamen (z. B. `CreateCompanies` statt `CreateCompany`)
## Rate Limits
API-Anfragen werden gedrosselt, um die Stabilität der Plattform zu gewährleisten:
| Limit | Wert |
| --------------- | ------------------------ |
| **Anfragen** | 100 Aufrufe pro Minute |
| **Batch-Größe** | 60 Datensätze pro Aufruf |
<Tip>
Verwenden Sie Batch-Vorgänge, um den Durchsatz zu maximieren — verarbeiten Sie bis zu 60 Datensätze in einem einzelnen API-Aufruf, statt einzelne Anfragen zu senden.
</Tip>

View File

@@ -0,0 +1,112 @@
---
title: Webhooks
description: Erhalten Sie Benachrichtigungen in Echtzeit, wenn Ereignisse in Ihrem CRM auftreten.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Webhooks übermitteln Daten in Echtzeit an Ihre Systeme, wenn Ereignisse in Twenty auftreten — kein Polling erforderlich. Verwenden Sie sie, um externe Systeme synchron zu halten, Automatisierungen auszulösen oder Benachrichtigungen zu senden.
## Webhook erstellen
1. Gehen Sie zu **Einstellungen → APIs & Webhooks → Webhooks**
2. Klicken Sie auf **+ Webhook erstellen**
3. Geben Sie Ihre Webhook-URL ein (muss öffentlich zugänglich sein)
4. Klicken Sie auf **Speichern**
Der Webhook wird sofort aktiviert und beginnt, Benachrichtigungen zu senden.
<VimeoEmbed videoId="928786708" title="Webhook erstellen" />
### Webhooks verwalten
**Bearbeiten**: Klicken Sie auf den Webhook → URL aktualisieren → **Speichern**
**Löschen**: Klicken Sie auf den Webhook → **Löschen** → Bestätigen
## Ereignisse
Twenty sendet Webhooks für diese Ereignistypen:
| Ereignis | Beispiel |
| -------------------------- | ---------------------------------------------------------- |
| **Datensatz erstellt** | `person.created`, `company.created`, `note.created` |
| **Datensatz aktualisiert** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Datensatz gelöscht** | `person.deleted`, `company.deleted` |
Alle Ereignistypen werden an Ihre Webhook-URL gesendet. Eine Ereignisfilterung kann in zukünftigen Versionen hinzugefügt werden.
## Payload-Format
Jeder Webhook sendet eine HTTP-POST-Anfrage mit einem JSON-Body:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Feld | Beschreibung |
| ------------- | -------------------------------------------------------------------- |
| `ereignis` | Was passiert ist (z. B. `person.created`) |
| `daten` | Der vollständige Datensatz, der erstellt/aktualisiert/gelöscht wurde |
| `zeitstempel` | Wann das Ereignis auftrat (UTC) |
<Note>
Antworten Sie mit einem **2xx-HTTP-Status** (200-299), um den Empfang zu bestätigen. Nicht-2xx-Antworten werden als Zustellfehler protokolliert.
</Note>
## Webhook-Validierung
Twenty signiert jede Webhook-Anfrage zu Sicherheitszwecken. Validieren Sie die Signaturen, um sicherzustellen, dass die Anfragen authentisch sind.
### Header
| Kopfzeile | Beschreibung |
| ---------------------------- | ----------------------- |
| `X-Twenty-Webhook-Signature` | HMAC-SHA256-Signatur |
| `X-Twenty-Webhook-Timestamp` | Zeitstempel der Anfrage |
### Validierungsschritte
1. Den Zeitstempel aus `X-Twenty-Webhook-Timestamp` abrufen
2. Zeichenfolge erstellen: `{timestamp}:{JSON payload}`
3. Den HMAC-SHA256-Hash mit Ihrem Webhook-Geheimnis berechnen
4. Mit `X-Twenty-Webhook-Signature` vergleichen
### Beispiel (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## Webhooks vs Workflows
| Methode | Richtung | Anwendungsfall |
| ---------------------------- | -------- | -------------------------------------------------------------------------------- |
| **Webhooks** | OUT | Externe Systeme automatisch über jede Datensatzänderung benachrichtigen |
| **Workflow + HTTP-Anfrage** | OUT | Daten mit benutzerdefinierter Logik (Filter, Transformationen) nach außen senden |
| **Workflow-Webhook-Trigger** | IN | Daten aus externen Systemen in Twenty empfangen |
Für den Empfang externer Daten siehe [Webhook-Trigger einrichten](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,32 @@
---
title: Erweitern
description: Erweitern Sie die Funktionalität von Twenty mit APIs, Webhooks und benutzerdefinierten Apps.
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="KI" />
</Frame>
## Überblick
Twenty ist darauf ausgelegt, erweiterbar zu sein. Verwenden Sie unsere APIs, Webhooks und das App-Framework, um sich in Ihre vorhandenen Tools zu integrieren und benutzerdefinierte Funktionalität zu entwickeln.
## Was Sie tun können
* **APIs**: Abfragen und ändern Sie Ihre CRM-Daten programmatisch mit REST oder GraphQL
* **Webhooks**: Erhalten Sie Benachrichtigungen in Echtzeit, wenn Ereignisse in Twenty auftreten
* **Apps**: Erstellen Sie benutzerdefinierte Anwendungen, die die Funktionalität von Twenty erweitern
## Erste Schritte
<CardGroup cols={2}>
<Card title="APIs" icon="code" href="/l/de/developers/extend/api">
Verbinden Sie sich programmgesteuert mit Twenty
</Card>
<Card title="Webhooks" icon="bell" href="/l/de/developers/extend/webhooks">
Erhalten Sie Benachrichtigungen über Ereignisse in Echtzeit
</Card>
<Card title="Apps" icon="puzzle-piece" href="/l/de/developers/extend/apps/getting-started/quick-start">
Erstellen Sie Anpassungen als Code
</Card>
</CardGroup>

View File

@@ -0,0 +1,189 @@
---
title: OAuth
icon: key
description: Autorisierungscode-Flow mit PKCE und Client-Anmeldedaten für Server-zu-Server-Zugriff.
---
Twenty implementiert OAuth 2.0 mit Autorisierungscode + PKCE für benutzerorientierte Apps und Client-Anmeldedaten für Server-zu-Server-Zugriff. Clients werden dynamisch über [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) registriert — keine manuelle Einrichtung in einem Dashboard.
## Wann Sie OAuth verwenden sollten
| Szenario | Authentifizierungsmethode |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Interne Skripte, Automatisierung | [API-Schlüssel](/l/de/developers/extend/api#authentication) |
| Externe App, die im Namen eines Benutzers handelt | **OAuth — Autorisierungscode** |
| Server-zu-Server, kein Benutzerkontext | **OAuth — Client-Anmeldedaten** |
| Twenty-App mit UI-Erweiterungen | [Apps](/l/de/developers/extend/apps/getting-started/quick-start) (OAuth wird automatisch gehandhabt) |
## Einen Client registrieren
Twenty unterstützt die **dynamische Client-Registrierung** gemäß [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). Keine manuelle Einrichtung erforderlich — registrieren Sie den Client programmgesteuert:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**Antwort:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
Speichern Sie das `client_secret` sicher — es kann später nicht mehr abgerufen werden.
</Warning>
## Geltungsbereiche
| Geltungsbereich | Zugriff |
| --------------- | ------------------------------------------------------------ |
| `api` | Voller Lese-/Schreibzugriff auf die Core- und Metadaten-APIs |
| `profile` | Profilinformationen des authentifizierten Benutzers lesen |
Fordern Sie Geltungsbereiche als durch Leerzeichen getrennte Zeichenfolge an: `scope=api profile`
## Autorisierungscode-Flow
Verwenden Sie diesen Flow, wenn Ihre App im Namen eines Twenty-Benutzers handelt.
### 1. Leiten Sie den Benutzer zur Autorisierung weiter
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| Parameter | Erforderlich | Beschreibung |
| ----------------------- | ------------ | -------------------------------------------------------------- |
| `client_id` | Ja | Ihre registrierte Client-ID |
| `response_type` | Ja | Muss `code` sein |
| `redirect_uri` | Ja | Muss einer registrierten Redirect-URI entsprechen |
| `scope` | Nein | Durch Leerzeichen getrennte Geltungsbereiche (Standard: `api`) |
| `state` | Empfohlen | Zufällige Zeichenfolge zur Verhinderung von CSRF-Angriffen |
| `code_challenge` | Empfohlen | PKCE-Challenge (SHA-256-Hash des Verifiers, base64url-codiert) |
| `code_challenge_method` | Empfohlen | Muss bei Verwendung von PKCE `S256` sein |
Der Benutzer sieht einen Zustimmungsbildschirm und stimmt dem Zugriff zu oder lehnt ihn ab.
### 2. Den Callback verarbeiten
Nach der Autorisierung leitet Twenty zurück zu Ihrer `redirect_uri` weiter:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
Überprüfen Sie, dass `state` mit dem übereinstimmt, was Sie gesendet haben.
### 3. Tauschen Sie den Code gegen Token aus
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**Antwort:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. Verwenden Sie das Zugriffstoken
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. Aktualisieren, wenn abgelaufen
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## Client-Anmeldedaten-Flow
Für Server-zu-Server-Integrationen ohne Benutzerinteraktion:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
Das zurückgegebene Token hat Zugriff auf Arbeitsbereichsebene und ist an keinen bestimmten Benutzer gebunden.
## Server-Ermittlung
Twenty veröffentlicht seine OAuth-Konfiguration an einem standardisierten Discovery-Endpunkt:
```
GET /.well-known/oauth-authorization-server
```
Dies liefert alle Endpunkte, unterstützte Grant-Typen, Geltungsbereiche und Fähigkeiten — nützlich, um generische OAuth-Clients zu erstellen.
## Zusammenfassung der API-Endpunkte
| Endpunkt | Zweck |
| ----------------------------------------- | ----------------------------------- |
| `/.well-known/oauth-authorization-server` | Ermittlung von Servermetadaten |
| `/oauth/register` | Dynamische Client-Registrierung |
| `/oauth/authorize` | Benutzerautorisierung |
| `/oauth/token` | Token-Austausch und -Aktualisierung |
| Umgebung | Basis-URL |
| ----------------- | ------------------------ |
| **Cloud** | `https://api.twenty.com` |
| **Selbsthosting** | `https://{your-domain}` |
## OAuth vs. API-Schlüssel
| | API-Schlüssel | OAuth |
| ------------------------------------- | ------------------------------- | ----------------------------------------- |
| **Einrichtung** | In den Einstellungen generieren | Client registrieren, Flow implementieren |
| **Benutzerkontext** | Keiner (Arbeitsbereichsebene) | Berechtigungen eines bestimmten Benutzers |
| **Am besten geeignet für** | Skripte, interne Tools | Externe Apps, Multi-User-Integrationen |
| **Token-Rotation** | Manuell | Automatisch über Refresh-Tokens |
| **Geltungsbereichsbasierter Zugriff** | Voller API-Zugriff | Granular über Geltungsbereiche |

View File

@@ -0,0 +1,117 @@
---
title: Webhooks
icon: satellite-dish
description: Lassen Sie sich benachrichtigen, wenn sich Datensätze ändern — HTTP POST an Ihren Endpunkt bei jeder Erstellung, Aktualisierung oder Löschung.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty sendet einen HTTP POST an Ihre URL, wenn ein Datensatz erstellt, aktualisiert oder gelöscht wird. Alle Objekttypen sind abgedeckt, einschließlich benutzerdefinierter Objekte.
## Webhook erstellen
1. Gehen Sie zu **Einstellungen → APIs & Webhooks → Webhooks**
2. Klicken Sie auf **+ Webhook erstellen**
3. Geben Sie Ihre Webhook-URL ein (muss öffentlich zugänglich sein)
4. Klicken Sie auf **Speichern**
Der Webhook wird sofort aktiviert und beginnt, Benachrichtigungen zu senden.
<VimeoEmbed videoId="928786708" title="Webhook erstellen" />
### Webhooks verwalten
**Bearbeiten**: Klicken Sie auf den Webhook → URL aktualisieren → **Speichern**
**Löschen**: Klicken Sie auf den Webhook → **Löschen** → Bestätigen
## Ereignisse
Twenty sendet Webhooks für diese Ereignistypen:
| Ereignis | Beispiel |
| -------------------------- | ---------------------------------------------------------- |
| **Datensatz erstellt** | `person.created`, `company.created`, `note.created` |
| **Datensatz aktualisiert** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Datensatz gelöscht** | `person.deleted`, `company.deleted` |
Alle Ereignistypen werden an Ihre Webhook-URL gesendet. Eine Ereignisfilterung kann in zukünftigen Versionen hinzugefügt werden.
## Payload-Format
Jeder Webhook sendet eine HTTP-POST-Anfrage mit einem JSON-Body:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Feld | Beschreibung |
| ----------- | -------------------------------------------------------------------- |
| `event` | Was passiert ist (z. B. `person.created`) |
| `data` | Der vollständige Datensatz, der erstellt/aktualisiert/gelöscht wurde |
| `timestamp` | Wann das Ereignis auftrat (UTC) |
<Note>
Antworten Sie mit einem **2xx-HTTP-Status** (200-299), um den Empfang zu bestätigen. Nicht-2xx-Antworten werden als Zustellfehler protokolliert.
</Note>
## Webhook-Validierung
Twenty signiert jede Webhook-Anfrage zu Sicherheitszwecken. Validieren Sie die Signaturen, um sicherzustellen, dass die Anfragen authentisch sind.
### Header
| Header | Beschreibung |
| ---------------------------- | ----------------------- |
| `X-Twenty-Webhook-Signature` | HMAC-SHA256-Signatur |
| `X-Twenty-Webhook-Timestamp` | Zeitstempel der Anfrage |
### Validierungsschritte
1. Den Zeitstempel aus `X-Twenty-Webhook-Timestamp` abrufen
2. Zeichenfolge erstellen: `{timestamp}:{JSON payload}`
3. Den HMAC-SHA256-Hash mit Ihrem Webhook-Geheimnis berechnen
4. Mit `X-Twenty-Webhook-Signature` vergleichen
### Beispiel (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooks vs Workflows
| Methode | Richtung | Anwendungsfall |
| ---------------------------- | -------- | -------------------------------------------------------------------------------- |
| **Webhooks** | OUT | Externe Systeme automatisch über jede Datensatzänderung benachrichtigen |
| **Workflow + HTTP-Anfrage** | OUT | Daten mit benutzerdefinierter Logik (Filter, Transformationen) nach außen senden |
| **Workflow-Webhook-Trigger** | IN | Daten aus externen Systemen in Twenty empfangen |
Für den Empfang externer Daten siehe [Webhook-Trigger einrichten](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,28 @@
---
title: Entwickler
description: Entwickeln Sie Apps, nutzen Sie die API, hosten Sie selbst oder tragen Sie zur Codebasis bei.
---
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={3}>
<Card href="/l/de/developers/extend/apps/getting-started/quick-start" img="/images/user-guide/halftone/dev-apps.png">
<CardTitle>Apps</CardTitle>
Erweitern Sie Twenty mit benutzerdefinierten Objekten, serverseitiger Logik, UI-Komponenten und KI-Agenten — alles als TypeScript-Pakete.
</Card>
<Card href="/l/de/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API</CardTitle>
REST- und GraphQL-APIs, Webhooks und OAuth.
</Card>
<Card href="/l/de/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>Selbst hosten</CardTitle>
Betreiben Sie Twenty auf Ihrer eigenen Infrastruktur.
</Card>
<Card href="/l/de/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
<CardTitle>Mitwirken</CardTitle>
Richten Sie das Monorepo lokal ein und reichen Sie PRs ein.
</Card>
</CardGroup>

View File

@@ -0,0 +1,262 @@
---
title: Docker Compose
icon: docker
---
<Warning>
Docker-Container sind für produktives Hosting oder Selbsthosting vorgesehen. Zum Mitwirken siehe [Lokale Einrichtung](/l/de/developers/contribute/capabilities/local-setup).
</Warning>
## Überblick
Diese Anleitung enthält Schritt-für-Schritt-Anweisungen, um die Twenty-Anwendung mit Docker Compose zu installieren und zu konfigurieren. Das Ziel ist, den Prozess übersichtlich zu gestalten und häufige Fallstricke zu vermeiden, die Ihre Einrichtung unterbrechen könnten.
**Wichtig:** Ändern Sie nur die in dieser Anleitung explizit erwähnten Einstellungen. Andere Konfigurationen zu ändern, kann zu Problemen führen.
Siehe die Dokumentation [Umgebungsvariablen einrichten](/l/de/developers/self-host/capabilities/setup) zur erweiterten Konfiguration. Alle Umgebungsvariablen müssen in der Datei `docker-compose.yml` auf Server- und/oder Worker-Ebene deklariert werden, je nach Variable.
## Systemanforderungen
* RAM: Stellen Sie sicher, dass Ihre Umgebung mindestens 2 GB RAM hat. Unzureichender Speicher kann dazu führen, dass Prozesse abstürzen.
* Docker & Docker Compose: Stellen Sie sicher, dass beide installiert und aktuell sind.
## Option 1: Einzeiliges Skript
Installieren Sie die neueste stabile Version von Twenty mit einem einzigen Befehl:
```bash
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
Um eine spezifische Version oder Zweig zu installieren:
```bash
VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
* Ersetzen Sie x.y.z mit der gewünschten Versionsnummer.
* Ersetzen Sie branch-name durch den Namen des Zweigs, den Sie installieren möchten.
## Option 2: Manuelle Schritte
Befolgen Sie diese Schritte für eine manuelle Einrichtung.
### Schritt 1: Einrichten der Umgebungsdatei
1. **Erstellen Sie die .env Datei**
Kopieren Sie die Beispielumgebungsdatei in eine neue .env-Datei in Ihrem Arbeitsverzeichnis:
```bash
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
2. **Einen Verschlüsselungsschlüssel generieren**
Führen Sie den folgenden Befehl aus, um eine eindeutige Zufallszeichenfolge zu generieren:
```bash
openssl rand -base64 32
```
**Wichtig:** Bewahren Sie diesen Wert geheim auf / teilen Sie ihn nicht. Der Verlust von `ENCRYPTION_KEY` bedeutet den Verlust des Zugriffs auf alle in der Datenbank gespeicherten Secrets (OAuth-Tokens, Anwendungsvariablen, TOTP-Secrets usw.).
3. **Aktualisieren Sie die `.env`**
Ersetzen Sie den Platzhalterwert in Ihrer .env-Datei durch das generierte Token:
```ini
ENCRYPTION_KEY=random_string
```
Weitere Informationen findest du im [Leitfaden zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation) mit Anweisungen zur Rotation ohne Ausfallzeit.
4. **Setzen Sie das Postgres-Passwort**
Aktualisieren Sie den Wert `PG_DATABASE_PASSWORD` in der .env-Datei mit einem starken Passwort ohne Sonderzeichen.
```ini
PG_DATABASE_PASSWORD=my_strong_password
```
### Schritt 2: Beschaffen Sie die Docker Compose-Datei
Laden Sie die `docker-compose.yml`-Datei in Ihr Arbeitsverzeichnis herunter:
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml
```
### Schritt 3: Anwendung starten
Starten Sie die Docker-Container:
```bash
docker compose up -d
```
### Schritt 4: Auf die Anwendung zugreifen
Wenn Sie twentyCRM auf Ihrem eigenen Computer hosten, öffnen Sie Ihren Browser und navigieren Sie zu [http://localhost:3000](http://localhost:3000).
Wenn Sie es auf einem Server hosten, prüfen Sie, ob der Server läuft und ob alles in Ordnung ist mit
```bash
curl http://localhost:3000
```
## Konfiguration
### Twenty für externen Zugriff freigeben
Standardmäßig läuft Twenty auf `localhost` an Port `3000`. Die UI bestimmt den API-Endpunkt aus dem eigenen Origin des Browsers, sodass sie von jedem Hostnamen geladen wird, der den Container erreicht – setze dennoch `SERVER_URL` auf deine öffentliche URL: Der Server verwendet sie für generierte Links (E-Mails, OAuth-Callbacks) und um HTTPS für sichere Cookies zu erkennen.
#### Erläuterung von `SERVER_URL`
* **Protokoll:** Verwenden Sie `http` oder `https` je nach Konfiguration.
* Verwenden Sie `http`, wenn Sie kein SSL eingerichtet haben.
* Verwenden Sie `https`, wenn Sie SSL konfiguriert haben.
* **Domain/IP:** Dies ist der Domainname oder die IP-Adresse, unter der Ihre Anwendung zugänglich ist.
* **Port:** Geben Sie die Portnummer an, wenn Sie nicht die Standardports verwenden (`80` für `http`, `443` für `https`).
### SSL-Anforderungen
SSL (HTTPS) ist erforderlich, damit bestimmte Browserfunktionen ordnungsgemäß funktionieren. Obwohl diese Funktionen möglicherweise während der lokalen Entwicklung funktionieren (da Browser localhost anders behandeln), ist eine ordnungsgemäße SSL-Einrichtung erforderlich, wenn Twenty auf einer regulären Domain gehostet wird.
Zum Beispiel erfordert die Clipboard-API möglicherweise einen sicheren Kontext - einige Funktionen wie Kopierknöpfe in der gesamten Anwendung funktionieren möglicherweise nicht ohne aktiviertes HTTPS.
Wir empfehlen dringend, Twenty hinter einem Reverse-Proxy mit SSL-Beendigung für optimale Sicherheit und Funktionalität zu konfigurieren.
#### Konfiguration der `SERVER_URL`
1. **Bestimmen Sie Ihre Zugriffs-URL**
* **Ohne Reverse-Proxy (Direkter Zugriff):**
Wenn Sie direkt ohne Reverse-Proxy auf die Anwendung zugreifen:
```ini
SERVER_URL=http://your-domain-or-ip:3000
```
* **Mit Reverse-Proxy (Standard-Ports):**
Wenn Sie einen Reverse-Proxy wie Nginx oder Traefik verwenden und SSL konfiguriert haben:
```ini
SERVER_URL=https://your-domain-or-ip
```
* **Mit Reverse-Proxy (Benutzerdefinierte Ports):**
Wenn Sie nicht standardisierte Ports verwenden:
```ini
SERVER_URL=https://your-domain-or-ip:custom-port
```
2. **Aktualisieren Sie die `.env` Datei**
Öffnen Sie Ihre `.env`-Datei und aktualisieren Sie die `SERVER_URL`:
```ini
SERVER_URL=http(s)://your-domain-or-ip:your-port
```
**Beispiele:**
* Direkter Zugriff ohne SSL:
```ini
SERVER_URL=http://123.45.67.89:3000
```
* Zugriff über Domain mit SSL:
```ini
SERVER_URL=https://mytwentyapp.com
```
3. **Starten Sie die Anwendung neu**
Damit die Änderungen wirksam werden, starten Sie die Docker-Container neu:
```bash
docker compose down
docker compose up -d
```
#### Überlegungen
* **Reverse Proxy-Konfiguration:**
Stellen Sie sicher, dass Ihr Reverse-Proxy Anfragen an den richtigen internen Port weiterleitet (`3000` standardmäßig). Konfigurieren Sie die SSL-Terminierung und alle erforderlichen Header.
* **Firewall-Einstellungen:**
Öffnen Sie die notwendigen Ports in Ihrer Firewall, um externen Zugriff zu ermöglichen.
* **Konsistenz:**
Die `SERVER_URL` muss mit der Art und Weise übereinstimmen, wie Nutzer in ihren Browsern auf Ihre Anwendung zugreifen.
#### Persistenz
* **Daten-Volumes:**
Die Docker-Compose-Konfiguration verwendet Volumes, um Daten für die Datenbank und den Serverspeicher persistent zu speichern.
* **Zustandslose Umgebungen:**
Wenn Sie in einer zustandslosen Umgebung bereitstellen (z. B. bei bestimmten Cloud-Diensten), konfigurieren Sie externen Speicher, um Daten persistent zu speichern.
## Sicherung und Wiederherstellung
Regelmäßige Sicherungen schützen Ihre CRM-Daten vor Verlust.
### Datenbanksicherung erstellen
```bash
docker exec twenty-postgres pg_dump -U postgres twenty > backup_$(date +%Y%m%d).sql
```
### Tägliche Sicherungen automatisieren
Zu Ihrer Crontab hinzufügen (`crontab -e`):
```bash
0 2 * * * docker exec twenty-postgres pg_dump -U postgres twenty > /backups/twenty_$(date +\%Y\%m\%d).sql
```
### Aus Sicherung wiederherstellen
1. Anwendung stoppen:
```bash
docker compose stop server worker
```
2. Datenbank wiederherstellen:
```bash
docker exec -i twenty-postgres psql -U postgres twenty < backup_20240115.sql
```
3. Dienste neu starten:
```bash
docker compose up -d
```
### Best Practices für Sicherungen
* **Wiederherstellungen regelmäßig testen** — überprüfen Sie, dass die Sicherungen tatsächlich funktionieren
* **Sicherungen extern aufbewahren** — Cloud-Speicher verwenden (S3, GCS usw.)
* **Sensible Daten verschlüsseln** — Sicherungen mit Verschlüsselung schützen
* **Mehrere Kopien aufbewahren** — tägliche, wöchentliche und monatliche Sicherungen vorhalten
## Fehlerbehebung
Sollten Sie auf ein Problem stoßen, schauen Sie sich die [Fehlerbehebung](/l/de/developers/self-host/capabilities/troubleshooting) für Lösungen an.
## Managed Hosting
<Tip>
Möchten Sie Twenty lieber nicht selbst betreiben und warten? [Finden Sie einen zertifizierten Twenty-Hosting-Partner](https://twenty.com/partners/list?categories=HOSTING\&ref=docs-docker-compose), der Ihre Instanz Ende-zu-Ende bereitstellt, hostet und verwaltet. *(Oder wenden Sie sich an unser Team: [contact@twenty.com](mailto:contact@twenty.com).)*
</Tip>

View File

@@ -0,0 +1,60 @@
---
title: Schlüsselrotation
icon: rotate
---
Twenty hat zwei unabhängige Schlüsselfamilien:
* **JWT-Signaturschlüssel** — asymmetrische ES256-Schlüsselpaare (`kid`-markiert), die in `core."signingKey"` gespeichert sind und zum Signieren und Verifizieren von Access-/Refresh-Tokens verwendet werden.
* **Verschlüsselungsschlüssel im Ruhezustand** — `ENCRYPTION_KEY`, wird verwendet, um OAuth-Tokens, Anwendungsvariablen, die privaten Schlüssel der Signierschlüssel, sensible Konfigurationswerte und TOTP-Geheimnisse innerhalb eines `enc:v2:`-Umschlags zu verschlüsseln.
`APP_SECRET` ist ein veraltetes Secret, das aus Gründen der Abwärtskompatibilität beibehalten wird: Wenn `ENCRYPTION_KEY` nicht gesetzt ist, fungiert es als Fallback für At-Rest-Verschlüsselung / Session-Cookie und verifiziert weiterhin bereits bestehende HS256-Access-Tokens. Es wird als veraltet markiert werden.
## JWT-Signierschlüssel
Jeder Schlüssel enthält einen `publicKey` (unbefristet aufbewahrt, damit er zuvor ausgestellte Tokens verifizieren kann), einen verschlüsselten `privateKey` (nur verwendet, solange der Schlüssel aktuell ist), ein `isCurrent`-Flag (zu jedem Zeitpunkt genau eine Zeile) und ein optionales `revokedAt`.
### Aktuellen Schlüssel rotieren
Setzen Sie `SIGNING_KEY_ROTATION_DAYS`, um die Funktion zu aktivieren: Ein täglicher Cronjob erstellt dann einen neuen aktuellen Schlüssel, sobald der bestehende Schlüssel älter als dieser Schwellenwert ist. Frühere Schlüssel werden *nicht* widerrufen, daher werden Tokens, die darunter signiert wurden, weiterhin verifiziert. Lassen Sie die Variable ungesetzt, um die automatische Rotation zu deaktivieren.
<Note>Die automatische Rotation ist ab Version v2.6 verfügbar.</Note>
### Einen Schlüssel widerrufen (nur bei Leak / Notfall)
**Settings → Admin Panel → Signing keys → Revoke** in einer nicht aktuellen Zeile. Löscht das verschlüsselte private Material, setzt `revokedAt` und weist jedes vorhandene Token zurück, das unter diesem `kid` signiert wurde.
## `ENCRYPTION_KEY` rotieren
<Note>Der unten beschriebene Befehl `secret-encryption:rotate` ist ab v2.6+ enthalten.</Note>
Jeder verschlüsselte Wert wird als `enc:v2:\<keyId>:\<payload>` verpackt, wobei `\<keyId>` ein 8-stelliges Hex-Präfix ist, das aus dem Rohschlüssel abgeleitet wird. Die Rotation erfolgt online und ist fortsetzbar.
1. **Einen neuen Schlüssel generieren**: `openssl rand -base64 32`.
2. **Beide Schlüssel nebeneinander konfigurieren** in `.env` und dann neu starten:
```ini
ENCRYPTION_KEY=NEW_VALUE
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
```
Neue Schreibvorgänge verwenden den neuen Schlüssel, vorhandene Zeilen werden weiterhin über den Fallback entschlüsselt.
3. **Vorhandene Zeilen erneut verschlüsseln**:
```bash
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
```
Der Befehl durchläuft sechs Sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Ein SQL-Filter überspringt Zeilen, die bereits auf dem neuen `\<keyId>` sind, sodass der Befehl idempotent ist: Unterbrechen und bei Bedarf erneut ausführen. Beendet sich mit einem von Null verschiedenen Exit-Code, wenn irgendeine Zeile fehlschlägt — zum Wiederholen erneut ausführen.
| Flag | Beschreibung |
| ---------------------------------------- | ---------------------------------------------------------------------------- |
| `-s, --site \<site>` | Auf eine einzelne Site beschränken. |
| `-b, --batch-size \<n>` | Zeilen pro Batch (Standard `200`, Maximum `5000`). |
| `-d, --dry-run` | Entschlüsseln + erneut im Speicher verschlüsseln, das `UPDATE` überspringen. |
4. **Den Fallback entfernen**, sobald `--dry-run` null verbleibende Zeilen anzeigt: `FALLBACK_ENCRYPTION_KEY` entfernen und neu starten.
## Legacy-Unterstützung für `APP_SECRET`
Ältere Instanzen, die niemals `ENCRYPTION_KEY` gesetzt haben, verwenden `APP_SECRET` als At-Rest-Verschlüsselungsschlüssel (und als Session-Cookie-Secret, das daraus abgeleitet wird). Dieser Pfad wird aus Gründen der Abwärtskompatibilität beibehalten, ist aber **veraltet** — setze einen dedizierten `ENCRYPTION_KEY` und folge dem oben beschriebenen Rotationsverfahren, um davon zu migrieren. `APP_SECRET` selbst bleibt in Verwendung, um Legacy-HS256-Access-Tokens zu verifizieren.

View File

@@ -0,0 +1,414 @@
---
title: Einrichtung
icon: gear
---
# Konfigurationsverwaltung
<Warning>
**Erstinstallation?** Folgen Sie dem [Docker Compose-Installationshandbuch](/l/de/developers/self-host/capabilities/docker-compose), um Twenty zum Laufen zu bringen, und kehren Sie dann hierher zurück, um die Konfiguration fortzusetzen.
</Warning>
Twenty bietet **zwei Konfigurationsmodi**, um unterschiedlichen Implementierungsbedürfnissen gerecht zu werden:
**Zugriff auf das Admin-Panel:** Nur Benutzer mit Admin-Berechtigungen (`canAccessFullAdminPanel: true`) können auf die Konfigurationsoberfläche zugreifen.
## 1. Admin-Panel-Konfiguration (Standard)
```bash
IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
```
**Die meiste Konfiguration erfolgt über die Benutzeroberfläche** nach der Installation:
1. Greifen Sie auf Ihre Twenty-Instanz zu (normalerweise `http://localhost:3000`)
2. Gehen Sie zu **Einstellungen / Admin-Panel / Konfigurationsvariablen**
3. Konfigurieren Sie Integrationen, E-Mail, Speicherung und mehr
4. Änderungen werden sofort wirksam (innerhalb von 15 Sekunden für Mehrcontainer-Bereitstellungen)
<Warning>
**Mehr-Container-Bereitstellungen:** Bei Verwendung der Datenbankkonfiguration (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`) lesen sowohl Server- als auch Worker-Container aus derselben Datenbank. Änderungen im Admin-Panel wirken sich auf beide Container aus, wodurch die Notwendigkeit entfällt, Umgebungsvariablen zwischen den Containern zu duplizieren (außer Infrastrukturvariablen).
</Warning>
**Was Sie über das Admin-Panel konfigurieren können:**
* **Authentifizierung** - Google/Microsoft OAuth, Passwort-Einstellungen
* **E-Mail** - SMTP-Einstellungen, Vorlagen, Verifizierung
* **Speicherung** - S3-Konfiguration, lokale Speicherpfade
* **Integrationen** - Gmail, Google Kalender, Microsoft-Dienste
* **Arbeitsablauf & Ratenbegrenzung** - Ausführungslimits, API-Drosselung
* **Und vieles mehr...**
![Konfigurationsvariablen des Admin-Panels](/images/user-guide/setup/admin-panel-config-variables.png)
<Warning>
Jede Variable ist mit Beschreibungen in Ihrem Admin-Panel dokumentiert unter **Einstellungen → Admin-Panel → Konfigurationsvariablen**.
Einige Infrastruktureinstellungen wie Datenbankverbindungen (`PG_DATABASE_URL`), Server-URLs (`SERVER_URL`) und Secrets (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) können nur über die `.env`-Datei konfiguriert werden.
[Vollständige technische Referenz →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
</Warning>
## Verschlüsselungsschlüssel
Twenty verwendet zwei ausschließlich über Umgebungsvariablen konfigurierbare Verschlüsselungsschlüssel:
| Variable | Zweck | Erforderlich |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `ENCRYPTION_KEY` | Primärer Schlüssel, der für die Verschlüsselung von Secrets im Ruhezustand verwendet wird (OAuth-Tokens, Anwendungsvariablen, private Schlüssel von Signierschlüsseln, TOTP-Secrets, sensible Konfigurationswerte). | Ja, für neue Installationen (Legacy-Installationen können stattdessen auf `APP_SECRET` zurückgreifen – siehe unten). |
| `FALLBACK_ENCRYPTION_KEY` | Nur zur Verifizierung verwendeter Schlüssel. Während einer Rotation auf den *vorherigen* `ENCRYPTION_KEY` gesetzt, damit vorhandene Zeilen weiterhin entschlüsselt werden können. | Nur während der Rotation |
Aus Gründen der Abwärtskompatibilität greift Twenty, falls `ENCRYPTION_KEY` nicht gesetzt ist, für die Verschlüsselung im Ruhezustand auf `APP_SECRET` zurück – entsprechend dem Legacy-Verhalten älterer Deployments. Neue Installationen sollten immer einen eigenen `ENCRYPTION_KEY` setzen.
Werte mit `openssl rand -base64 32` generieren und sie an einem sicheren Ort aufbewahren (einen Secrets-Manager, eine versiegelte Konfiguration usw.). Der Verlust des `ENCRYPTION_KEY` bedeutet den Verlust des Zugriffs auf jedes in der Datenbank gespeicherte Secret.
Um `ENCRYPTION_KEY` ohne Downtime zu rotieren, siehe die [Anleitung zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation).
## 2. Nur-Umgebungs-Konfiguration
```bash
IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
```
**Alle Konfiguration wird über `.env`-Dateien verwaltet:**
1. Setzen Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` in Ihrer `.env`-Datei
2. Fügen Sie alle Konfigurationsvariablen zu Ihrer `.env`-Datei hinzu
3. Starten Sie Container neu, damit Änderungen wirksam werden
4. Im Admin-Panel werden aktuelle Werte angezeigt, können jedoch nicht geändert werden
## Multi-Workspace-Modus
Standardmäßig läuft Twenty im **Single-Workspace-Modus** — ideal für die meisten selbstgehosteten Bereitstellungen, bei denen Sie eine CRM-Instanz für Ihre Organisation benötigen.
### Single-Workspace-Modus (Standard)
```bash
IS_MULTIWORKSPACE_ENABLED=false # default
```
* Ein Workspace pro Twenty-Instanz
* Der erste Benutzer wird automatisch zum Admin mit vollen Berechtigungen (`canImpersonate` und `canAccessFullAdminPanel`).
* Neue Registrierungen werden deaktiviert, nachdem der erste Workspace erstellt wurde.
* Einfache URL-Struktur: `https://your-domain.com`
### Multi-Workspace-Modus aktivieren
```bash
IS_MULTIWORKSPACE_ENABLED=true
DEFAULT_SUBDOMAIN=app # default value
```
Aktivieren Sie den Multi-Workspace-Modus für SaaS-ähnliche Bereitstellungen, bei denen mehrere unabhängige Teams eigene Workspaces auf derselben Twenty-Instanz benötigen.
**Wesentliche Unterschiede zum Single-Workspace-Modus:**
* Auf derselben Instanz können mehrere Workspaces erstellt werden.
* Jeder Workspace erhält seine eigene Subdomain (z. B. `sales.your-domain.com`, `marketing.your-domain.com`).
* Benutzer registrieren sich und melden sich unter `{DEFAULT_SUBDOMAIN}.your-domain.com` an (z. B. `app.your-domain.com`).
* Keine automatischen Admin-Berechtigungen — der erste Benutzer in jedem Workspace ist ein normaler Benutzer.
* Workspacespezifische Einstellungen wie Subdomain und benutzerdefinierte Domain werden in den Workspace-Einstellungen verfügbar.
<Warning>
**Nur-Umgebungs-Einstellung:** `IS_MULTIWORKSPACE_ENABLED` kann nur über die `.env`-Datei konfiguriert werden und erfordert einen Neustart. Dies kann nicht über das Admin-Panel geändert werden.
</Warning>
### DNS-Konfiguration für Multi-Workspace
Wenn Sie den Multi-Workspace-Modus verwenden, konfigurieren Sie Ihr DNS mit einem Wildcard-Eintrag, um die dynamische Erstellung von Subdomains zu ermöglichen:
```
*.your-domain.com -> your-server-ip
```
Dadurch wird das automatische Subdomain-Routing für neue Workspaces ohne manuelle DNS-Konfiguration ermöglicht.
### Einschränkung der Workspace-Erstellung
Im Multi-Workspace-Modus möchten Sie möglicherweise einschränken, wer neue Workspaces erstellen darf:
```bash
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
```
Wenn aktiviert, können nur Benutzer mit `canAccessFullAdminPanel` zusätzliche Workspaces erstellen. Benutzer können bei der Erstregistrierung weiterhin ihren ersten Workspace erstellen.
## Gmail- & Google Kalender-Integration
### Erstellen Sie ein Projekt auf Google Cloud
1. Gehen Sie zum [Google Cloud-Konsole](https://console.cloud.google.com/)
2. Erstellen Sie ein neues Projekt oder wählen Sie ein vorhandenes aus
3. Aktivieren Sie diese APIs:
* [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
* [Google Kalender API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
* [People API](https://console.cloud.google.com/apis/library/people.googleapis.com)
### OAuth konfigurieren
1. Gehen Sie zu [Anmeldedaten](https://console.cloud.google.com/apis/credentials)
2. Erstellen Sie eine OAuth 2.0-Client-ID
3. Fügen Sie diese Weiterleitungs-URIs hinzu:
* `https://{your-domain}/auth/google/redirect` (für SSO)
* `https://{your-domain}/auth/google-apis/get-access-token` (für Integrationen)
### In Twenty konfigurieren
1. Gehen Sie zu **Einstellungen → Admin-Panel → Konfigurationsvariablen**
2. Finden Sie den Abschnitt **Google Auth**
3. Setzen Sie diese Variablen:
* `MESSAGING_PROVIDER_GMAIL_ENABLED=true`
* `CALENDAR_PROVIDER_GOOGLE_ENABLED=true`
* `AUTH_GOOGLE_CLIENT_ID={client-id}`
* `AUTH_GOOGLE_CLIENT_SECRET={client-secret}`
* `AUTH_GOOGLE_CALLBACK_URL=https://{your-domain}/auth/google/redirect`
* `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
<Warning>
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
</Warning>
**Erforderliche Scopes** (automatisch konfiguriert):
[Siehe relevanten Quellcode](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
* `https://www.googleapis.com/auth/calendar.events`
* `https://www.googleapis.com/auth/gmail.readonly`
* `https://www.googleapis.com/auth/profile.emails.read`
### Wenn Ihre Anwendung im Testmodus ist
Wenn Ihre Anwendung im Testmodus ist, müssen Sie Testbenutzer zu Ihrem Projekt hinzufügen.
Fügen Sie im [OAuth-Zustimmungsbildschirm](https://console.cloud.google.com/apis/credentials/consent) Ihre Testbenutzer dem Abschnitt "Testbenutzer" hinzu.
## Microsoft 365-Integration
<Warning>
Benutzer müssen eine [Microsoft 365-Lizenz](https://admin.microsoft.com/Adminportal/Home) besitzen, um die Kalender- und Messaging-API verwenden zu können. Ohne eine solche Lizenz können sie ihr Konto nicht mit Twenty synchronisieren.
</Warning>
### Erstellen Sie ein Projekt in Microsoft Azure
Sie müssen ein Projekt in [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) erstellen und die Anmeldeinformationen erhalten.
### APIs aktivieren
Aktivieren Sie diese APIs im "Berechtigungen"-Bereich der Microsoft Azure-Konsole:
* Microsoft Graph: Mail.ReadWrite
* Microsoft Graph: Mail.Send
* Microsoft Graph: Kalender.Read
* Microsoft Graph: Benutzer.Read
* Microsoft Graph: openid
* Microsoft Graph: email
* Microsoft Graph: profil
* Microsoft Graph: offline_access
<Note>
"Mail.ReadWrite" und "Mail.Send" sind nur erforderlich, wenn Sie E-Mails mit unseren Workflow-Aktionen senden möchten. Sie können stattdessen "Mail.Read" verwenden, wenn Sie nur E-Mails empfangen möchten.
</Note>
<Note>
"Calendars.ReadWrite" ist erforderlich, wenn Sie Kalenderereignisse mit unseren Workflow-Aktionen erstellen möchten. Wenn Sie nur Kalenderereignisse synchronisieren möchten, verwenden Sie "Calendars.Read".
</Note>
### Autorisierte Redirect-URIs
Sie müssen die folgenden Weiterleitungs-URIs zu Ihrem Projekt hinzufügen:
* `https://{your-domain}/auth/microsoft/redirect`, falls Sie das Microsoft-SSO verwenden möchten
* `https://{your-domain}/auth/microsoft-apis/get-access-token`
### In Twenty konfigurieren
1. Gehen Sie zu **Einstellungen → Admin-Panel → Konfigurationsvariablen**
2. Finden Sie den Abschnitt **Microsoft Auth**
3. Setzen Sie diese Variablen:
* `MESSAGING_PROVIDER_MICROSOFT_ENABLED=true`
* `CALENDAR_PROVIDER_MICROSOFT_ENABLED=true`
* `AUTH_MICROSOFT_ENABLED=true`
* `AUTH_MICROSOFT_CLIENT_ID={client-id}`
* `AUTH_MICROSOFT_CLIENT_SECRET={client-secret}`
* `AUTH_MICROSOFT_CALLBACK_URL=https://{your-domain}/auth/microsoft/redirect`
* `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
<Warning>
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
</Warning>
### Scopes konfigurieren
[Siehe relevanten Quellcode](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
* 'openid'
* 'e-Mail'
* 'profil'
* 'offline_access'
* 'Mail.ReadWrite'
* 'Mail.Send'
* 'Kalender.Read'
### Wenn Ihre Anwendung im Testmodus ist
Wenn Ihre Anwendung im Testmodus ist, müssen Sie Testbenutzer zu Ihrem Projekt hinzufügen.
Fügen Sie Ihre Testbenutzer dem Abschnitt "Benutzer und Gruppen" hinzu.
## Hintergrundaufgaben für Kalender & Messaging
Nachdem Sie Gmail-, Google Kalender- oder Microsoft 365-Integrationen konfiguriert haben, müssen Sie die Hintergrundaufgaben starten, die Daten synchronisieren.
Registrieren Sie die folgenden wiederkehrenden Aufgaben in Ihrem Worker-Container:
```bash
# from your worker container
yarn command:prod cron:messaging:messages-import
yarn command:prod cron:messaging:message-list-fetch
yarn command:prod cron:calendar:calendar-event-list-fetch
yarn command:prod cron:calendar:calendar-events-import
yarn command:prod cron:messaging:ongoing-stale
yarn command:prod cron:calendar:ongoing-stale
yarn command:prod cron:workflow:automated-cron-trigger
```
## E-Mail-Konfiguration
1. Gehen Sie zu **Einstellungen → Admin-Panel → Konfigurationsvariablen**
2. Finden Sie den Abschnitt **E-Mail**
3. Konfigurieren Sie Ihre SMTP-Einstellungen:
<Tabs>
<Tab title="Gmail">
Sie müssen ein [App-Passwort](https://support.google.com/accounts/answer/185833) bereitstellen.
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.gmail.com
* EMAIL_SMTP_PORT=465
* EMAIL_SMTP_USER=gmail_email_address
* EMAIL_SMTP_PASSWORD='gmail_app_password'
</Tab>
<Tab title="Office365">
Beachten Sie, dass, wenn Sie die Zwei-Faktor-Authentifizierung aktiviert haben, ein [App-Passwort](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9) bereitgestellt werden muss.
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.office365.com
* EMAIL_SMTP_PORT=587
* EMAIL_SMTP_USER=office365_email_address
* EMAIL_SMTP_PASSWORD='office365_password'
</Tab>
<Tab title="smtp4dev">
**smtp4dev** ist ein Fake-SMTP-Mailserver für Entwicklung und Tests.
* Führen Sie das smtp4dev-Image aus: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
* Rufen Sie die smtp4dev-Benutzeroberfläche hier auf: [http://localhost:8090](http://localhost:8090)
* Setzen Sie die folgenden Variablen:
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=localhost
* EMAIL_SMTP_PORT=2525
</Tab>
</Tabs>
<Warning>
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
</Warning>
## S3-Speicher
<Warning>
Standardmäßig speichert Twenty hochgeladene Dateien auf dem lokalen Dateisystem. Für Produktionsbereitstellungen verwenden Sie S3 oder einen S3-kompatiblen Dienst (MinIO, DigitalOcean Spaces usw.). um sicherzustellen, dass Dateien Container-Neustarts überdauern und über mehrere Serverinstanzen skaliert werden können.
</Warning>
Setzen Sie `STORAGE_TYPE=S_3` und konfigurieren Sie die Variablen `STORAGE_S3_*` über das Admin-Panel oder `.env`. Siehe die [Referenz zu config-variables.ts](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts) für die vollständige Liste der S3-Variablen.
Wenn Sie S3 mit CORS-abhängigen Funktionen verwenden (z. B. Dateidownloads im Browser), stellen Sie sicher, dass Ihr Bucket in seiner CORS-Konfiguration die Origin Ihres Twenty-Frontends zulässt.
## Logikfunktionen & Code-Interpreter
Twenty unterstützt Logikfunktionen für Workflows und den Code-Interpreter für KI-Datenanalyse. Beide führen vom Benutzer bereitgestellten Code aus und erfordern aus Sicherheitsgründen eine explizite Konfiguration.
### Sicherheits-Standardeinstellungen
**In Produktion (NODE_ENV=production):** Sowohl Logikfunktionen als auch der Code-Interpreter sind standardmäßig **deaktiviert**. Sie müssen sie, wenn Sie diese Funktionen benötigen, explizit mit `LOGIC_FUNCTION_TYPE` und `CODE_INTERPRETER_TYPE` aktivieren.
**In der Entwicklung (NODE_ENV=development):** Beide sind der Einfachheit halber beim lokalen Betrieb standardmäßig **LOCAL**.
<Warning>
**Sicherheitshinweis:** Der lokale Treiber (`LOGIC_FUNCTION_TYPE=LOCAL` oder `CODE_INTERPRETER_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktionsbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, verwenden Sie `LOGIC_FUNCTION_TYPE=LAMBDA` oder `CODE_INTERPRETER_TYPE=E2B` (mit Sandbox-Isolierung), oder lassen Sie sie deaktiviert.
</Warning>
### Logikfunktionen - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | ------------------------------ | -------------------------------------------------- | ---------------------------------- |
| Deaktiviert | `LOGIC_FUNCTION_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
| Lokal | `LOGIC_FUNCTION_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
### Logikfunktionen - Empfohlene Konfiguration
**Für die Entwicklung:**
```bash
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Für den Produktivbetrieb (AWS):**
```bash
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Zum Deaktivieren von Logikfunktionen:**
```bash
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Code-Interpreter - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | -------------------------------- | ------------------------------------------ | ------------------------ |
| Deaktiviert | `CODE_INTERPRETER_TYPE=DISABLED` | KI-Codeausführung deaktivieren | N/A |
| Lokal | `CODE_INTERPRETER_TYPE=LOCAL` | Nur für die Entwicklung | Niedrig (keine Sandbox) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produktion mit Ausführung in einer Sandbox | Hoch (isolierte Sandbox) |
<Note>
Bei Verwendung von `LOGIC_FUNCTION_TYPE=DISABLED` oder `CODE_INTERPRETER_TYPE=DISABLED` führt jeder Ausführungsversuch zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne diese Funktionen betreiben möchten.
</Note>
## API und Frontend auf getrennten Hosts
Nur relevant, wenn Sie das Frontend nicht vom Backend ausliefern lassen. Same-Origin-
Bereitstellungen benötigen hier nichts.
Geben Sie alle Browser-Ursprünge an, die mit der API kommunizieren, kommagetrennt. `SERVER_URL`
und `FRONTEND_URL` sind bereits vertrauenswürdig. Der Server protokolliert eine Warnung, wenn ein Browser
diese aus nicht deklariertem Ursprung aufruft.
```bash
AUTH_COOKIE_ALLOWED_ORIGINS=https://app.example.com
```
<Warning>
Das Frontend sendet Anmeldeinformationen auf jeder Anfrage, und Browser lehnen eine
Anmeldebestätigung von nicht angemeldeter Herkunft ab, so dass die App nicht geladen werden kann, bis
Sie dies gesetzt haben.
</Warning>
Wenn sich die beiden Hosts auf unterschiedlichen registrierbaren Domains befinden (nicht nur auf unterschiedlichen Subdomains), setzen Sie außerdem `AUTH_COOKIE_SAME_SITE=none`, was HTTPS erfordert.
Wenn Multiworkspace aktiviert ist, listet hier nicht jede Workspace-Subdomain auf.
Stattdessen Route API Pfade (`/graphql`, `/metadata`, `/rest`, `/auth/`,
`/client-config`, `/file`) in das Backend jedes Hostnamens des Arbeitsbereichs, also bleibt jeder
Arbeitsbereich gleich — so läuft 20 Cloud.

View File

@@ -0,0 +1,254 @@
---
title: Fehlerbehebung
icon: wrench
---
## Fehlerbehebung
Wenn Sie bei der Einrichtung der Entwicklungsumgebung, dem Upgrade Ihrer Instanz oder dem Self-Hosting auf Probleme stoßen, finden Sie hier einige Lösungen für häufige Probleme.
### Selbsthosting
#### Erste Installation führt zu `password authentication failed for user "postgres"`
🚨 **WICHTIG: Diese Lösung ist NUR für Neuinstallationen** 🚨
Wenn Sie bereits eine bestehende Twenty-Instanz mit Produktionsdaten haben, **NICHT** diesen Schritten folgen, da diese permanent Ihre Datenbank löschen!
Bei der ersten Installation von Twenty möchten Sie möglicherweise das Standard-Datenbankpasswort ändern.
Das während der ersten Installation gesetzte Passwort wird dauerhaft im Datenbank-Volumen gespeichert. Wenn Sie später versuchen, dieses Passwort in Ihrer Konfiguration zu ändern, ohne das alte Volumen zu entfernen, erhalten Sie Authentifizierungsfehler, da die Datenbank noch das ursprüngliche Passwort verwendet.
⚠️ WARNUNG: Die folgenden Schritte werden ALLE Datenbankdaten PERMANENT LÖSCHEN! ⚠️
Fahren Sie nur fort, wenn dies eine Neuinstallation ohne wichtige Daten ist.
Um den `PG_DATABASE_PASSWORD` zu aktualisieren, müssen Sie auf:
```sh
# Update the PG_DATABASE_PASSWORD in .env
docker compose down --volumes
docker compose up -d
```
#### CR-Zeilenumbrüche gefunden [Windows]
Dies geschieht aufgrund von Windows-Zeilenenden in Kombination mit Ihrer Git-Konfiguration. Versuchen Sie Folgendes auszuführen:
```
git config --global core.autocrlf false
```
Löschen Sie dann das Repository und klonen Sie es erneut.
#### Fehlendes Metadaten-Schema
Während der Twenty-Installation müssen Sie Ihre Postgres-Datenbank mit den richtigen Schemata, Erweiterungen und Benutzern bereitstellen.
Wenn Sie diese Bereitstellung erfolgreich durchführen, sollten Sie `default` und `metadata` Schemata in Ihrer Datenbank haben.
Wenn Sie dies nicht tun, stellen Sie sicher, dass nicht mehr als eine PostgreSQL-Instanz auf Ihrem Computer läuft.
#### Modul 'twenty-emails' oder seine entsprechenden Typdeklarationen nicht gefunden.
Du musst das »21«-Paket erstellen (`npx nx nx nx führen Sie einundzwanzig:build`) bevor du die Datenbank-Initialisierung ausführst.
#### Fehlendes Twenty-x Paket
Stellen Sie sicher, dass Sie `yarn` im Root-Verzeichnis ausführen und dann `npx nx server:dev twenty-server` ausführen. Wenn das immer noch nicht funktioniert, versuchen Sie, das fehlende Paket manuell zu erstellen.
#### Lint beim Speichern funktioniert nicht
Normalerweise sollte dies sofort mit der installierten Oxc-Erweiterung (`oxc.oxc-vscode`) funktionieren. Wenn dies nicht funktioniert, versuchen Sie dies zu Ihren VS Code Einstellungen zu hinzufügen (dev Containerbereich):
```
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
}
```
#### Beim Ausführen von `npx nx start` oder `npx nx nx nx start`, wird ein Fehler außerhalb des Speichers angezeigt
Führen Sie nur die Services aus, die Sie benötigen, anstatt `npx nx start`. Wenn Sie zum Beispiel am Server arbeiten, führen Sie nur `npx nx worker twenty-server` aus.
**Wenn es nicht funktioniert:**
Wenn Sie versucht haben, nur `npx nx run twenty-server:start` auf WSL zu starten und es mit dem untenstehenden Speicherfehler fehlschlägt:
`FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`
Eine Workaround ist es, den folgenden Befehl in deinem Terminal auszuführen, oder füge ihn zu deinem `.bashrc`-Profil hinzu, damit er automatisch angewendet wird:
`export NODE_OPTIONS="--max-old-space-size=8192"`
Das Flag --max-old-space-size=8192 setzt ein oberes Limit von 8GB für den Node.js-Heap; der Verbrauch steigt mit dem Bedarf der Anwendung.
Referenz: https://stackoverflow.com/questions/56982005/where-do-i-set-node-options-max-old-space-size-2048
**Wenn es nicht funktioniert:**
Investigate, welche Prozesse den Großteil des Arbeitsspeichers deiner Maschine in Anspruch nehmen. Bei Twenty haben wir festgestellt, dass einige VS Code Erweiterungen viel RAM in Anspruch nahmen, so dass wir sie vorübergehend deaktiviert haben.
**Wenn es nicht funktioniert:**
Ein Neustart des Rechners hilft, Geisterprozesse zu bereinigen.
#### Beim Ausführen von `npx nx start` gibt es seltsame [0] und [1] in den Logs
Das ist zu erwarten, da der Befehl `npx nx start` im Hintergrund weitere Befehle ausführt.
#### Es werden keine E-Mails gesendet
Meistens liegt das daran, dass der `worker` nicht im Hintergrund läuft. Versuchen Sie auszuführen
```
npx nx worker twenty-server
```
#### Mein Microsoft 365-Konto kann nicht mit 20 verbunden werden
Meistens liegt das daran, dass Ihr Administrator die Microsoft 365-Lizenz für Ihr Konto nicht aktiviert hat. Überprüfen Sie [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
Wenn Sie einen Fehlercode `AADSTS50020` haben, bedeutet dies wahrscheinlich, dass Sie ein privates Microsoft-Konto verwenden. Dies wird derzeit nicht unterstützt. Weitere Informationen [hier](https://learn.microsoft.com/fr-fr/troubleshoot/entra/entra-id/app-integration/error-code-aadsts50020-user-account-identity-provider-does-not-exist)
#### Während du `yarn` ausführst, erscheinen Warnungen in der Konsole
Warnungen informieren über das Ziehen zusätzlicher Abhängigkeiten, die nicht explizit in `package.json` angegeben sind. Solange kein schwerwiegender Fehler auftritt, sollte alles wie erwartet funktionieren.
#### Wenn ein Benutzer auf die Login-Seite zugreift, erscheint in den Logs ein Fehler über einen nicht autorisierten Benutzer, der versucht, auf einen Arbeitsbereich zuzugreifen
Das wird erwartet, da der Benutzer nicht autorisiert ist, wenn er ausgeloggt ist, da seine Identität nicht verifiziert wurde.
#### Wie überprüfen Sie, ob Ihr Worker läuft?
* Gehen Sie zu [webhook-test.com](https://webhook-test.com/) und kopieren Sie **Ihre einzigartige Webhook-URL**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Webhook-Test" />
</div>
* Öffnen Sie Ihre Twenty-App, navigieren Sie zu `/settings`, und aktivieren Sie den **Erweitert**-Schalter unten links auf dem Bildschirm.
* Erstellen Sie einen neuen Webhook.
* Fügen Sie **Ihre einzigartige Webhook-URL** in das Feld **Endpoint-Url** in Twenty ein. Stellen Sie die **Filter** auf `Companies` und `Created` ein.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Webhook-Einstellungen" />
</div>
* Gehen Sie zu `/objects/companies` und erstellen Sie einen neuen Unternehmenseintrag.
* Kehren Sie zu [webhook-test.com](https://webhook-test.com/) zurück und prüfen Sie, ob ein neuer **POST-Anfrage** empfangen wurde.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Webhook-Test-Ergebnis" />
</div>
* Wenn eine **POST-Anfrage** empfangen wird, läuft Ihr Worker erfolgreich. Andernfalls müssen Sie Ihren Worker beheben.
#### Frontend startet nicht und gibt den Fehler TS5042 zurück: Option 'project' kann nicht mit Quelldateien auf der Befehlszeile gemischt werden
Kommentieren Sie das Checker-Plugin in `packages/twenty-ui/vite-config.ts`, wie im folgenden Beispiel angezeigt:
```
plugins: [
react({ jsxImportSource: 'react' }),
tsconfigPaths(),
svgr(),
dts(dtsConfig),
// checker(checkersConfig),
wyw({
include: [
'**/OverflowingTextWithTooltip.tsx',
'**/Chip.tsx',
'**/Tag.tsx',
'**/Avatar.tsx',
'**/AvatarChip.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
],
```
#### Admin-Panel nicht zugänglich
Führen Sie `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` im Datenbank-Container aus, um Zugriff auf das Admin-Panel zu erhalten.
#### Beim Ausführen eines Workflows schlägt die Workflow-Ausführung fehl mit "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
In der Produktion sind Logikfunktionen standardmäßig deaktiviert. Setzen Sie die Umgebungsvariable `LOGIC_FUNCTION_TYPE` auf `LOCAL` oder `LAMBDA`, um sie zu aktivieren. Dies kann über Umgebungsvariablen oder über die Datenbankvariablen des Admin-Panels konfiguriert werden. Details finden Sie im [Leitfaden zur Einrichtung von Logikfunktionen](/l/de/developers/self-host/capabilities/setup#logic-functions-available-drivers).
#### Wenn ich eine Nachricht im AI-Chat eingebe, antwortet sie nicht sofort und ich muss die Seite aktualisieren, um die Antwort zu sehen
Das bedeutet, dass Ihrem Reverse Proxy die Konfiguration für Stream-basierte API-Antworten fehlt. Unten ist ein Beispiel Nginx Konfiguration:
```
location ~* ^/api/.*stream|\/graphql\/stream {
proxy_pass http://twenty_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
gzip off;
#chunked_transfer_encoding off;
add_header X-Accel-Buffering no always;
}
```
### 1-Klick-Docker komponieren
#### Kann mich nicht einloggen
Wenn Sie sich nach der Einrichtung nicht anmelden können:
1. Führen Sie die folgenden Befehle aus:
```bash
docker exec -it twenty-server-1 yarn
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
```
2. Starten Sie die Docker-Container neu:
```bash
docker compose down
docker compose up -d
```
Beachte, dass der Befehl `database:reset` deine Datenbank komplett löschen und von Grund auf neu erstellen wird.
#### Verbindungsprobleme hinter einem Reverse-Proxy
Wenn Sie Twenty hinter einem Reverse-Proxy ausführen und Verbindungsprobleme haben:
1. **SERVER_URL überprüfen:**
Stellen Sie sicher, dass `SERVER_URL` in Ihrer `.env`-Datei mit Ihrer externen Zugriffs-URL übereinstimmt, einschließlich `https`, falls SSL aktiviert ist.
2. **Reverse-Proxy-Einstellungen prüfen:**
* Stellen Sie sicher, dass Ihr Reverse-Proxy Anfragen korrekt an den Twenty-Server weiterleitet.
* Stellen Sie sicher, dass Header wie `X-Forwarded-For` und `X-Forwarded-Proto` korrekt gesetzt sind.
3. **Dienste neu starten:**
Starten Sie nach Änderungen sowohl den Reverse-Proxy als auch die Twenty-Container neu.
#### Fehler beim Hochladen eines Bildes – Berechtigung verweigert
Das Ändern des Datenordner-Eigentums auf dem Host von root zu einem anderen Benutzer und Gruppe löst dieses Problem.
#### Fehler „Datei nicht gefunden“ beim Testen der Logic-Funktion im Workflow
Führen Sie den Befehl `yarn command:prod application:rebuild-default-deps` in Ihrem twenty-server-Container aus, um das Problem mit fehlenden Abhängigkeiten zu beheben.
## Hilfe erhalten
Wenn Sie auf Probleme stoßen, die in diesem Leitfaden nicht behandelt werden:
* Protokolle überprüfen:
Überprüfen Sie die Container-Logs auf Fehlermeldungen:
```bash
docker compose logs
```
* Unterstützung aus der Community:
Wenden Sie sich an die [Twenty Community](https://github.com/twentyhq/twenty/issues) oder [Support-Kanäle](https://discord.gg/cx5n4Jzs57) für Unterstützung.

View File

@@ -0,0 +1,106 @@
---
title: Upgrade-Anleitung
icon: arrow-up-right-dots
---
## Allgemeine Richtlinien
**Sichern Sie Ihre Datenbank immer, bevor Sie den Upgrade-Vorgang starten**, indem Sie Folgendes ausführen:
```bash
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
```
Aus Sicherung wiederherstellen:
```bash
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
```
Wenn Sie Docker Compose verwenden, befolgen Sie diese Schritte:
1. Twenty stoppen: `docker compose down`
2. Ändern Sie den Wert von `TAG` in der Datei `.env` neben Ihrer `docker-compose.yml`
3. Twenty starten: `docker compose up -d`
Der Server führt beim Start alle erforderlichen Upgrade-Migrationen automatisch aus. Kein manueller Befehl erforderlich.
## Upgrade auf v2.34+ — mindestens PostgreSQL 15
Twenty v2.34 erfordert PostgreSQL 15 oder neuer. Die offizielle Docker-Compose-Konfiguration verwendet PostgreSQL 16. Wenn Ihre Instanz eine Verbindung zu einer extern verwalteten Datenbank herstellt, führen Sie deren Upgrade durch, bevor Sie den v2.34-Server starten.
## Versionsübergreifende Upgrades (v1.23+)
Ab **v1.23** unterstützt Twenty versionsübergreifende Upgrades. Sie können direkt von jeder unterstützten Version auf die neueste Version wechseln, ohne jede Zwischenversion durchlaufen zu müssen.
Zum Beispiel wird ein Upgrade direkt von v1.23 auf v2.0 vollständig unterstützt.
## Upgrade auf v2.5+ – Verschlüsselungsumschlag für ruhende Daten
Ab **v2.5** speichert Twenty Secrets im Ruhezustand (OAuth-Tokens, Anwendungsvariablen, private Signaturschlüssel, sensible Konfigurationswerte, TOTP-Secrets) in einem versionierten Umschlag `enc:v2:`, der mit `ENCRYPTION_KEY` (oder `APP_SECRET`, falls `ENCRYPTION_KEY` nicht gesetzt ist) verschlüsselt wird.
Beim ersten Start mit v2.5 werden langsame Upgrade-Befehle ausgeführt, die den neuen Umschlag mit bestehenden Zeilen befüllen. Sie sind idempotent – ein Unterbrechen und Neustarten des Servers setzt an der Stelle fort, an der er aufgehört hat –, aber sie können bei großen Datenbanken eine Weile dauern. Sie können den Fortschritt mit `upgrade:status` überwachen.
Sie sollten **vor** dem Upgrade auf v2.5 einen dedizierten `ENCRYPTION_KEY` setzen, damit beim Nachbefüllen die Zeilen von Anfang an unter diesem Schlüssel geschrieben werden. Ein Schlüsselwechsel nach der Nachbefüllung erfordert eine [Rotation](/l/de/developers/self-host/capabilities/key-rotation).
## Rotation von Secrets und Signaturschlüsseln
Für alltägliche operative Aufgaben wie das Rotieren von `ENCRYPTION_KEY`, das Rotieren des JWT-Signaturschlüssels oder das Widerrufen eines kompromittierten Signaturschlüssels siehe den dedizierten [Leitfaden zur Schlüsselrotation](/l/de/developers/self-host/capabilities/key-rotation).
## Upgrade-Status prüfen
Mit dem Befehl `upgrade:status` können Sie den aktuellen Status Ihrer Instanz und der Workspace-Migrationen prüfen. Er ist nützlich zum Debuggen von Upgrade-Problemen oder beim Erstellen einer Support-Anfrage.
Führen Sie ihn im Server-Container aus:
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
```
Beispielausgabe:
```sh
APP_VERSION: v1.23.0
Instance
Inferred version: 1.23.0
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:43:58.823Z
Workspace
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
Inferred version: 1.23.0
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:44:09.361Z
Summary
Instance: Up to date
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
```
### Optionen
| Flag | Beschreibung |
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `-w, --workspace-id <id>` | Auf einen bestimmten Workspace filtern. Kann mehrfach übergeben werden. |
| `-f, --failed-only` | Workspaces auf dem neuesten Stand ausblenden, nur zurückliegende und fehlgeschlagene Workspaces anzeigen. |
## Fehlerbehebung
Wenn das Upgrade in einigen Workspaces fehlschlägt, wird der Server nicht über den fehlschlagenden Schritt hinaus fortfahren. Ein Neustart des Servers (`docker compose up -d`) führt das Upgrade ab der zuletzt erreichten Stelle erneut aus.
Um Probleme schnell zu identifizieren, führen Sie Folgendes aus:
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
```
Dies zeigt nur Workspaces, die zurückliegen oder fehlgeschlagen sind, zusammen mit der Fehlermeldung für jeden Fehlschlag.
## Vor v1.23
Wenn Ihre Instanz älter als v1.23 ist, müssen Sie schrittweise über jede größere getaggte Version ein Upgrade durchführen (von v1.6 auf v1.7, dann von v1.7 auf v1.8 usw.), bis Sie v1.23 erreichen. Danach können Sie direkt auf die neueste Version wechseln.

View File

@@ -0,0 +1,26 @@
---
title: Selbsthosting
description: Stellen Sie Twenty auf Ihrer eigenen Infrastruktur bereit und verwalten Sie es.
---
<Frame>
<img src="/images/user-guide/what-is-twenty/20.png" alt="KI" />
</Frame>
## Überblick
Twenty kann auf Ihrer eigenen Infrastruktur im Selbsthosting betrieben werden, sodass Sie die volle Kontrolle über Ihre Daten und Ihre Bereitstellung haben.
## Warum Selbsthosting?
* **Datenhoheit**: Behalten Sie alle CRM-Daten auf Ihren eigenen Servern
* **Compliance**: Erfüllen Sie regulatorische Anforderungen an die Datenresidenz
* **Anpassung**: Voller Zugriff, um die Plattform zu ändern und zu erweitern
## Erste Schritte
<CardGroup cols={2}>
<Card title="Docker Compose" icon="docker" href="/l/de/developers/self-host/capabilities/docker-compose">
Schnelle Einrichtung mit Docker
</Card>
</CardGroup>