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