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,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).